Add YouTrack pull/push sync with project settings and time tracking.
CI / test-and-build (push) Failing after 1m19s
CI / demo (push) Skipped

Wire manual Y menu and CLI sync for issues and work items, persist
per-project credentials, and keep local edits until an explicit push.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-24 00:17:51 +02:00
co-authored by Cursor
parent 19ae1f1b62
commit 98d2253f44
23 changed files with 2874 additions and 44 deletions
+79 -4
View File
@@ -1,8 +1,10 @@
package main
import (
"context"
"fmt"
"os"
"strings"
tea "github.com/charmbracelet/bubbletea"
@@ -12,6 +14,7 @@ import (
"task/internal/ticket"
"task/internal/ui"
"task/internal/version"
"task/internal/ytsync"
)
func main() {
@@ -23,6 +26,12 @@ func main() {
case "help", "--help", "-h":
printHelp()
return
case "sync":
if err := runSync(os.Args[2:]); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
return
default:
fmt.Fprintf(os.Stderr, "unknown command: %s\n", os.Args[1])
printHelp()
@@ -61,17 +70,83 @@ func run() error {
return nil
}
func runSync(args []string) error {
cfg, err := config.LoadOrCreate()
if err != nil {
return fmt.Errorf("config: %w", err)
}
projectID := cfg.ActiveProject
action := ""
for i := 0; i < len(args); i++ {
switch args[i] {
case "pull", "push":
if action != "" {
return fmt.Errorf("specify only one of pull/push")
}
action = args[i]
case "--project", "-p":
if i+1 >= len(args) {
return fmt.Errorf("%s requires a project id", args[i])
}
i++
projectID = strings.TrimSpace(args[i])
case "--help", "-h":
fmt.Printf("Usage: %s sync <pull|push> [--project ID]\n", config.CliName)
return nil
default:
return fmt.Errorf("unknown sync argument: %s (use pull or push)", args[i])
}
}
if action == "" {
return fmt.Errorf("usage: %s sync <pull|push> [--project ID]", config.CliName)
}
if projectID == "" {
return fmt.Errorf("project id required")
}
store := ticket.NewJSONStore(cfg.DataDir)
timeStore := ticket.NewJSONTimeStore(cfg.DataDir)
projects := project.NewDirStore(cfg.DataDir)
if err := projects.Ensure(projectID); err != nil {
return err
}
switch action {
case "push":
res, err := ytsync.PushProject(context.Background(), projects, store, timeStore, projectID)
if err != nil {
return err
}
fmt.Printf("pushed %s: tickets +%d ~%d, time %d\n",
projectID, res.Created, res.Updated, res.TimePushed)
default:
res, err := ytsync.PullProject(context.Background(), projects, store, timeStore, projectID)
if err != nil {
return err
}
fmt.Printf("pulled %s: tickets +%d ~%d, time %d (%d issues)\n",
projectID, res.Created, res.Updated, res.TimePulled, res.Total)
}
return nil
}
func printHelp() {
fmt.Printf(`%s — offline-first terminal task manager
Usage:
%s Start the TUI
%s version Print build version
%s help Show this help
%s Start the TUI
%s sync pull [-p ID] Pull issues from YouTrack
%s sync push [-p ID] Push local changes to YouTrack
%s version Print build version
%s help Show this help
Env:
TASK_MASTER_HOME Override home for config/data (dev/demo sandbox)
TASK_MASTER_DATA_DIR Override data directory only
`, config.CliName, config.CliName, config.CliName, config.CliName)
YouTrack:
In the TUI: p → project → e to set remote URL, token, project key.
Then Y → Pull or Push (manual only; edits stay local until you push).
`, config.CliName, config.CliName, config.CliName, config.CliName, config.CliName, config.CliName)
}