Add tm_server with SQLite, Svelte UI, and task-master TUI sync.

Introduces the web backend (auth, RBAC, project tokens, revisions),
YouTrack-or-taskmaster backend choice in the TUI, and a first-pass
embedded Svelte UI. The frontend is functional but needs a major rework.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-27 22:23:11 +01:00
co-authored by Cursor
parent 98d2253f44
commit 4adfcdb95a
67 changed files with 7188 additions and 129 deletions
+122
View File
@@ -0,0 +1,122 @@
package syncbackend
import (
"context"
"fmt"
"task/internal/project"
"task/internal/ticket"
"task/internal/ytsync"
)
// RemoteProject is a project discovered on a remote backend.
type RemoteProject struct {
Key string `json:"key"`
Name string `json:"name"`
ID string `json:"id,omitempty"`
}
// Connection credentials for a remote instance or project.
type Connection struct {
Kind string
BaseURL string
Token string
Scope string
}
// ProjectConfig is the per-local-project sync settings.
type ProjectConfig struct {
Kind string
BaseURL string
Token string
ProjectKey string
Query string
}
// Result reuses ytsync.Result for pull/push summaries.
type Result = ytsync.Result
// Backend syncs tickets with an external system.
type Backend interface {
Kind() string
ListProjects(ctx context.Context, conn Connection) ([]RemoteProject, error)
Pull(ctx context.Context, projectID string, cfg ProjectConfig, store ticket.Store, timeStore ticket.TimeStore) (Result, error)
Push(ctx context.Context, projectID string, cfg ProjectConfig, store ticket.Store, timeStore ticket.TimeStore) (Result, error)
}
// Registry maps kind → Backend.
type Registry struct {
backends map[string]Backend
}
// NewRegistry returns a registry with built-in backends.
func NewRegistry() *Registry {
r := &Registry{backends: map[string]Backend{}}
r.Register(&NoneBackend{})
r.Register(&YouTrackBackend{})
return r
}
// Register adds a backend.
func (r *Registry) Register(b Backend) {
r.backends[b.Kind()] = b
}
// Get returns a backend by kind.
func (r *Registry) Get(kind string) (Backend, error) {
if kind == "" {
kind = "none"
}
b, ok := r.backends[kind]
if !ok {
return nil, fmt.Errorf("unknown sync backend %q", kind)
}
return b, nil
}
// NoneBackend disables sync.
type NoneBackend struct{}
func (NoneBackend) Kind() string { return "none" }
func (NoneBackend) ListProjects(context.Context, Connection) ([]RemoteProject, error) {
return nil, fmt.Errorf("no remote projects for kind none")
}
func (NoneBackend) Pull(context.Context, string, ProjectConfig, ticket.Store, ticket.TimeStore) (Result, error) {
return Result{}, fmt.Errorf("sync disabled")
}
func (NoneBackend) Push(context.Context, string, ProjectConfig, ticket.Store, ticket.TimeStore) (Result, error) {
return Result{}, fmt.Errorf("sync disabled")
}
// YouTrackBackend wraps existing ytsync.
type YouTrackBackend struct{}
func (YouTrackBackend) Kind() string { return "youtrack" }
func (YouTrackBackend) ListProjects(ctx context.Context, conn Connection) ([]RemoteProject, error) {
return ListYouTrackProjects(ctx, conn.BaseURL, conn.Token)
}
func (YouTrackBackend) Pull(ctx context.Context, projectID string, cfg ProjectConfig, store ticket.Store, timeStore ticket.TimeStore) (Result, error) {
yt := toYouTrack(cfg)
s := &ytsync.YouTrackSyncer{Store: store, TimeStore: timeStore}
return s.Pull(ctx, projectID, yt)
}
func (YouTrackBackend) Push(ctx context.Context, projectID string, cfg ProjectConfig, store ticket.Store, timeStore ticket.TimeStore) (Result, error) {
yt := toYouTrack(cfg)
s := &ytsync.YouTrackSyncer{Store: store, TimeStore: timeStore}
return s.Push(ctx, projectID, yt)
}
func toYouTrack(cfg ProjectConfig) project.YouTrack {
return project.YouTrack{
BaseURL: cfg.BaseURL,
Token: cfg.Token,
ProjectKey: cfg.ProjectKey,
Query: cfg.Query,
}
}
+29
View File
@@ -0,0 +1,29 @@
package syncbackend
import (
"context"
"task/internal/youtrack"
)
// ListYouTrackProjects lists projects via a master/instance token.
func ListYouTrackProjects(ctx context.Context, baseURL, token string) ([]RemoteProject, error) {
client := &youtrack.Client{BaseURL: baseURL, Token: token}
projects, err := client.ListProjects(ctx)
if err != nil {
return nil, err
}
out := make([]RemoteProject, 0, len(projects))
for _, p := range projects {
name := p.Name
if name == "" {
name = p.ShortName
}
out = append(out, RemoteProject{
Key: p.ShortName,
Name: name,
ID: p.ID,
})
}
return out, nil
}