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:
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user