From 1abe8c4fa0e00c5a2b5bd3a79af9e861054642df Mon Sep 17 00:00:00 2001 From: mirivlad Date: Mon, 1 Jun 2026 22:56:05 +0800 Subject: [PATCH] cli: add sync push/pull/status commands --- cmd/verstak/main.go | 161 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/cmd/verstak/main.go b/cmd/verstak/main.go index 3f1b131..f55013f 100644 --- a/cmd/verstak/main.go +++ b/cmd/verstak/main.go @@ -8,9 +8,11 @@ import ( "strings" "verstak/internal/core/actions" + "verstak/internal/core/config" "verstak/internal/core/plugins" "verstak/internal/core/search" "verstak/internal/core/storage" + syncsvc "verstak/internal/core/sync" "verstak/internal/core/vault" "verstak/internal/core/worklog" ) @@ -38,6 +40,8 @@ func main() { runLog(os.Args[2:]) case "index": runIndex(os.Args[2:]) + case "sync": + runSync(os.Args[2:]) case "plugin": runPlugin(os.Args[2:]) default: @@ -56,6 +60,7 @@ func usage() { fmt.Println(" node Manage nodes") fmt.Println(" action Manage actions") fmt.Println(" --version Show version") + fmt.Println(" sync Sync with server (push/pull/status)") fmt.Println(" --help Show this help") } @@ -597,6 +602,162 @@ func runIndexRebuild(args []string) { fmt.Printf("indexed %d nodes\n", count) } +// --- sync --- + +func runSync(args []string) { + if len(args) == 0 { + fmt.Println("verstak sync — synchronize with server") + fmt.Println() + fmt.Println("Usage: verstak sync [options]") + fmt.Println() + fmt.Println("Commands:") + fmt.Println(" push Push local changes to server") + fmt.Println(" pull Pull remote changes from server") + fmt.Println(" status Show sync status") + os.Exit(0) + } + switch args[0] { + case "push": + runSyncPush(args[1:]) + case "pull": + runSyncPull(args[1:]) + case "status": + runSyncStatus(args[1:]) + case "--help", "-h": + runSync(nil) + default: + fmt.Fprintf(os.Stderr, "Unknown sync command: %s\n", args[0]) + os.Exit(1) + } +} + +func openSyncDB(args []string) (*storage.DB, string) { + vaultPath, _ := stringFlag(args, "--vault") + abs, _ := filepath.Abs(vaultPath) + dbPath := filepath.Join(abs, ".verstak", "index.db") + db, err := storage.Open(dbPath) + if err != nil { + fmt.Fprintf(os.Stderr, "Open vault: %v\n", err) + os.Exit(1) + } + return db, abs +} + +func runSyncPush(args []string) { + db, abs := openSyncDB(args) + defer db.Close() + + cfg, err := config.Load(abs) + if err != nil || cfg.Sync.ServerURL == "" || cfg.Sync.APIKey == "" { + fmt.Fprintln(os.Stderr, "Sync not configured. Use 'verstak sync configure' or GUI settings.") + os.Exit(1) + } + + deviceID := cfg.Sync.DeviceID + if deviceID == "" { + deviceID = "cli-" + abs[:8] + } + + syncSvc := syncsvc.NewService(db, deviceID) + client := syncsvc.NewClient(cfg.Sync.ServerURL, cfg.Sync.APIKey, deviceID, abs) + + unpushed, err := syncSvc.GetUnpushedOps() + if err != nil { + fmt.Fprintf(os.Stderr, "Get ops: %v\n", err) + os.Exit(1) + } + + if len(unpushed) == 0 { + fmt.Println("Nothing to push.") + return + } + + result, err := client.Push(unpushed) + if err != nil { + fmt.Fprintf(os.Stderr, "Push failed: %v\n", err) + os.Exit(1) + } + + if err := syncSvc.MarkPushed(result.Accepted); err != nil { + fmt.Fprintf(os.Stderr, "Mark pushed: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Pushed %d ops, accepted %d\n", len(unpushed), len(result.Accepted)) +} + +func runSyncPull(args []string) { + db, abs := openSyncDB(args) + defer db.Close() + + cfg, err := config.Load(abs) + if err != nil || cfg.Sync.ServerURL == "" || cfg.Sync.APIKey == "" { + fmt.Fprintln(os.Stderr, "Sync not configured.") + os.Exit(1) + } + + deviceID := cfg.Sync.DeviceID + if deviceID == "" { + deviceID = "cli-" + abs[:8] + } + + syncSvc := syncsvc.NewService(db, deviceID) + client := syncsvc.NewClient(cfg.Sync.ServerURL, cfg.Sync.APIKey, deviceID, abs) + + _, _, lastRev, _, err := syncSvc.GetState() + if err != nil { + lastRev = 0 + } + + result, err := client.Pull(lastRev) + if err != nil { + fmt.Fprintf(os.Stderr, "Pull failed: %v\n", err) + os.Exit(1) + } + + var opIDs []string + for _, op := range result.Ops { + fmt.Printf(" %s\t%s\t%s\t%s\n", op.OpType, op.EntityType, op.EntityID, op.PayloadJSON) + opIDs = append(opIDs, op.OpID) + } + + if len(opIDs) > 0 { + syncSvc.MarkApplied(opIDs) + } + + fmt.Printf("Pulled %d ops (server rev: %d)\n", len(result.Ops), result.ServerRevision) +} + +func runSyncStatus(args []string) { + db, abs := openSyncDB(args) + defer db.Close() + + cfg, err := config.Load(abs) + configured := err == nil && cfg.Sync.ServerURL != "" && cfg.Sync.APIKey != "" + serverURL := "" + deviceID := "" + if cfg != nil { + serverURL = cfg.Sync.ServerURL + deviceID = cfg.Sync.DeviceID + } + + unpushed := 0 + if configured { + if deviceID == "" { + deviceID = "cli-" + abs[:8] + } + syncSvc := syncsvc.NewService(db, deviceID) + ops, _ := syncSvc.GetUnpushedOps() + unpushed = len(ops) + } + + fmt.Println("Sync Status") + fmt.Println(" Configured:", configured) + fmt.Println(" Server:", serverURL) + fmt.Println(" Device:", deviceID) + fmt.Println(" Unpushed ops:", unpushed) +} + // --- plugin --- func runPlugin(args []string) {