836 lines
19 KiB
Go
836 lines
19 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"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"
|
|
)
|
|
|
|
const version = "0.1.0-dev"
|
|
|
|
func main() {
|
|
if len(os.Args) < 2 {
|
|
fmt.Println("Verstak", version)
|
|
os.Exit(0)
|
|
}
|
|
|
|
switch os.Args[1] {
|
|
case "--version", "-v":
|
|
fmt.Println("Verstak", version)
|
|
case "--help", "-h":
|
|
usage()
|
|
case "init":
|
|
runInit(os.Args[2:])
|
|
case "node":
|
|
runNode(os.Args[2:])
|
|
case "action":
|
|
runAction(os.Args[2:])
|
|
case "log":
|
|
runLog(os.Args[2:])
|
|
case "index":
|
|
runIndex(os.Args[2:])
|
|
case "sync":
|
|
runSync(os.Args[2:])
|
|
case "plugin":
|
|
runPlugin(os.Args[2:])
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", os.Args[1])
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func usage() {
|
|
fmt.Println("Verstak — local-first working vault")
|
|
fmt.Println()
|
|
fmt.Println("Usage: verstak <command> [flags]")
|
|
fmt.Println()
|
|
fmt.Println("Commands:")
|
|
fmt.Println(" init Initialize a new vault")
|
|
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")
|
|
}
|
|
|
|
// --- init ---
|
|
|
|
func runInit(args []string) {
|
|
vaultPath := vaultPathFromFlags(args)
|
|
abs, err := filepath.Abs(vaultPath)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if err := vault.Init(abs); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Init failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Println("Vault initialized at", abs)
|
|
fmt.Println(" .verstak/index.db")
|
|
fmt.Println(" .verstak/config.yml")
|
|
fmt.Println(" spaces/")
|
|
}
|
|
|
|
// --- node ---
|
|
|
|
func runNode(args []string) {
|
|
if len(args) == 0 {
|
|
nodeUsage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
sub := args[0]
|
|
rest := args[1:]
|
|
|
|
switch sub {
|
|
case "create":
|
|
runNodeCreateCmd(rest)
|
|
case "list":
|
|
runNodeListCmd(rest)
|
|
case "get":
|
|
runNodeGetCmd(rest)
|
|
case "move":
|
|
runNodeMoveCmd(rest)
|
|
case "delete":
|
|
runNodeDeleteCmd(rest)
|
|
case "--help", "-h":
|
|
nodeUsage()
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "Unknown node command: %s\n", sub)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func runNodeCreateCmd(args []string) {
|
|
typ, _ := stringFlag(args, "--type")
|
|
title, _ := stringFlag(args, "--title")
|
|
parentID, _ := stringFlag(args, "--parent")
|
|
vaultPath, _ := stringFlag(args, "--vault")
|
|
|
|
if typ == "" || title == "" {
|
|
fmt.Fprintln(os.Stderr, "Error: --type and --title required")
|
|
nodeUsage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
abs, _ := filepath.Abs(vaultPath)
|
|
if err := runNodeCreate(abs, parentID, typ, title); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Create failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func runNodeListCmd(args []string) {
|
|
parentID, _ := stringFlag(args, "--parent")
|
|
vaultPath, _ := stringFlag(args, "--vault")
|
|
|
|
abs, _ := filepath.Abs(vaultPath)
|
|
if err := runNodeList(abs, parentID); err != nil {
|
|
fmt.Fprintf(os.Stderr, "List failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func runNodeGetCmd(args []string) {
|
|
id, _ := stringFlag(args, "--id")
|
|
vaultPath, _ := stringFlag(args, "--vault")
|
|
|
|
if id == "" {
|
|
fmt.Fprintln(os.Stderr, "Error: --id required")
|
|
os.Exit(1)
|
|
}
|
|
|
|
abs, _ := filepath.Abs(vaultPath)
|
|
if err := runNodeGet(abs, id); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Get failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func runNodeMoveCmd(args []string) {
|
|
id, _ := stringFlag(args, "--id")
|
|
parentID, _ := stringFlag(args, "--parent")
|
|
sortStr, _ := stringFlag(args, "--sort")
|
|
vaultPath, _ := stringFlag(args, "--vault")
|
|
|
|
if id == "" {
|
|
fmt.Fprintln(os.Stderr, "Error: --id required")
|
|
os.Exit(1)
|
|
}
|
|
sort, _ := strconv.Atoi(sortStr)
|
|
|
|
abs, _ := filepath.Abs(vaultPath)
|
|
if err := runNodeMove(abs, id, parentID, sort); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Move failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func runNodeDeleteCmd(args []string) {
|
|
id, _ := stringFlag(args, "--id")
|
|
vaultPath, _ := stringFlag(args, "--vault")
|
|
|
|
if id == "" {
|
|
fmt.Fprintln(os.Stderr, "Error: --id required")
|
|
os.Exit(1)
|
|
}
|
|
|
|
abs, _ := filepath.Abs(vaultPath)
|
|
if err := runNodeDelete(abs, id); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Delete failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// --- flag parsing ---
|
|
|
|
func stringFlag(args []string, name string) (string, bool) {
|
|
for i := 0; i < len(args); i++ {
|
|
if args[i] == name && i+1 < len(args) {
|
|
return args[i+1], true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func vaultPathFromFlags(args []string) string {
|
|
if v, ok := stringFlag(args, "--vault"); ok {
|
|
return v
|
|
}
|
|
return "."
|
|
}
|
|
|
|
// --- action ---
|
|
|
|
func runAction(args []string) {
|
|
if len(args) == 0 {
|
|
actionUsage()
|
|
os.Exit(1)
|
|
}
|
|
sub := args[0]
|
|
rest := args[1:]
|
|
switch sub {
|
|
case "add":
|
|
runActionAdd(rest)
|
|
case "list":
|
|
runActionList(rest)
|
|
case "run":
|
|
runActionRun(rest)
|
|
case "delete":
|
|
runActionDelete(rest)
|
|
case "--help", "-h":
|
|
actionUsage()
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "Unknown action command: %s\n", sub)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func actionUsage() {
|
|
fmt.Println("verstak action — manage actions")
|
|
fmt.Println()
|
|
fmt.Println("Usage: verstak action <command> [options]")
|
|
fmt.Println()
|
|
fmt.Println("Commands:")
|
|
fmt.Println(" add --node ID --kind KIND --title TITLE Add action")
|
|
fmt.Println(" list --node ID List actions for node")
|
|
fmt.Println(" run --id ID Run action")
|
|
fmt.Println(" delete --id ID Delete action")
|
|
fmt.Println()
|
|
fmt.Println("Kinds: open_url, open_file, open_folder, run_command, run_script, open_terminal, launch_app")
|
|
}
|
|
|
|
func openActionDB(args []string) (*actions.Service, string, func()) {
|
|
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)
|
|
}
|
|
svc := actions.NewService(db)
|
|
return svc, abs, func() { db.Close() }
|
|
}
|
|
|
|
func runActionAdd(args []string) {
|
|
nodeID, _ := stringFlag(args, "--node")
|
|
kind, _ := stringFlag(args, "--kind")
|
|
title, _ := stringFlag(args, "--title")
|
|
cmd, _ := stringFlag(args, "--command")
|
|
url, _ := stringFlag(args, "--url")
|
|
workingDir, _ := stringFlag(args, "--working-dir")
|
|
vaultPath, _ := stringFlag(args, "--vault")
|
|
|
|
if nodeID == "" || kind == "" || title == "" {
|
|
fmt.Fprintln(os.Stderr, "Error: --node, --kind and --title required")
|
|
actionUsage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
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)
|
|
}
|
|
defer db.Close()
|
|
|
|
svc := actions.NewService(db)
|
|
rec, err := svc.Create(nodeID, kind, title, cmd, workingDir, url, nil, false, false)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Add failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("created\t%s\t%s\t%s\t%s\n", rec.ID, rec.Kind, rec.NodeID, rec.Title)
|
|
}
|
|
|
|
func runActionList(args []string) {
|
|
nodeID, _ := stringFlag(args, "--node")
|
|
vaultPath, _ := stringFlag(args, "--vault")
|
|
|
|
if nodeID == "" {
|
|
fmt.Fprintln(os.Stderr, "Error: --node required")
|
|
os.Exit(1)
|
|
}
|
|
|
|
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)
|
|
}
|
|
defer db.Close()
|
|
|
|
svc := actions.NewService(db)
|
|
list, err := svc.ListByNode(nodeID)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "List failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
for _, a := range list {
|
|
fmt.Printf("%s\t%s\t%v\t%s\n", a.ID, a.Kind, a.ConfirmRequired, a.Title)
|
|
}
|
|
}
|
|
|
|
func runActionRun(args []string) {
|
|
id, _ := stringFlag(args, "--id")
|
|
vaultPath, _ := stringFlag(args, "--vault")
|
|
|
|
if id == "" {
|
|
fmt.Fprintln(os.Stderr, "Error: --id required")
|
|
os.Exit(1)
|
|
}
|
|
|
|
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)
|
|
}
|
|
defer db.Close()
|
|
|
|
svc := actions.NewService(db)
|
|
rec, err := svc.Get(id)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Get failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if rec.ConfirmRequired {
|
|
fmt.Printf("Action: %s\n", rec.Title)
|
|
fmt.Printf("Kind: %s\n", actions.KindLabel(rec.Kind))
|
|
if rec.Command != "" {
|
|
fmt.Printf("Cmd: %s\n", rec.Command)
|
|
}
|
|
if rec.URL != "" {
|
|
fmt.Printf("URL: %s\n", rec.URL)
|
|
}
|
|
fmt.Print("Run? [y/N] ")
|
|
var input string
|
|
fmt.Scanln(&input)
|
|
if strings.ToLower(input) != "y" {
|
|
fmt.Println("Cancelled")
|
|
return
|
|
}
|
|
}
|
|
|
|
result, err := svc.Run(id)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Run failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if result.ExitCode != 0 {
|
|
fmt.Printf("exit=%d\n", result.ExitCode)
|
|
}
|
|
if result.Output != "" {
|
|
fmt.Print(result.Output)
|
|
}
|
|
fmt.Println("done")
|
|
}
|
|
|
|
func runActionDelete(args []string) {
|
|
id, _ := stringFlag(args, "--id")
|
|
vaultPath, _ := stringFlag(args, "--vault")
|
|
|
|
if id == "" {
|
|
fmt.Fprintln(os.Stderr, "Error: --id required")
|
|
os.Exit(1)
|
|
}
|
|
|
|
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)
|
|
}
|
|
defer db.Close()
|
|
|
|
svc := actions.NewService(db)
|
|
if err := svc.Delete(id); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Delete failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Println("deleted")
|
|
}
|
|
|
|
// --- log ---
|
|
|
|
func runLog(args []string) {
|
|
if len(args) == 0 {
|
|
fmt.Println("verstak log — manage worklog entries")
|
|
fmt.Println()
|
|
fmt.Println("Usage: verstak log <command> [options]")
|
|
fmt.Println()
|
|
fmt.Println("Commands:")
|
|
fmt.Println(" add --node ID --time MIN --text TEXT Add worklog entry")
|
|
fmt.Println(" list --node ID List entries for node")
|
|
fmt.Println(" report --node ID Copy report to stdout")
|
|
os.Exit(1)
|
|
}
|
|
switch args[0] {
|
|
case "add":
|
|
runLogAdd(args[1:])
|
|
case "list":
|
|
runLogList(args[1:])
|
|
case "report":
|
|
runLogReport(args[1:])
|
|
case "--help", "-h":
|
|
runLog(nil)
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "Unknown log command: %s\n", args[0])
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func runLogAdd(args []string) {
|
|
nodeID, _ := stringFlag(args, "--node")
|
|
minutesStr, _ := stringFlag(args, "--time")
|
|
text, _ := stringFlag(args, "--text")
|
|
vaultPath, _ := stringFlag(args, "--vault")
|
|
|
|
if nodeID == "" || text == "" {
|
|
fmt.Fprintln(os.Stderr, "Error: --node and --text required")
|
|
os.Exit(1)
|
|
}
|
|
minutes := 0
|
|
if minutesStr != "" {
|
|
fmt.Sscanf(minutesStr, "%d", &minutes)
|
|
}
|
|
|
|
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)
|
|
}
|
|
defer db.Close()
|
|
|
|
svc := worklog.NewService(db)
|
|
e, err := svc.Add(nodeID, text, "", minutes, true, false)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Add failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("logged\t%s\t%dm\t%s\n", e.ID, minutes, e.Summary)
|
|
}
|
|
|
|
func runLogList(args []string) {
|
|
nodeID, _ := stringFlag(args, "--node")
|
|
vaultPath, _ := stringFlag(args, "--vault")
|
|
|
|
if nodeID == "" {
|
|
fmt.Fprintln(os.Stderr, "Error: --node required")
|
|
os.Exit(1)
|
|
}
|
|
|
|
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)
|
|
}
|
|
defer db.Close()
|
|
|
|
svc := worklog.NewService(db)
|
|
entries, err := svc.ListByNode(nodeID)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "List failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
for _, e := range entries {
|
|
dur := "—"
|
|
if e.Minutes != nil {
|
|
dur = fmt.Sprintf("%dm", *e.Minutes)
|
|
}
|
|
approx := ""
|
|
if e.Approximate {
|
|
approx = " ~"
|
|
}
|
|
fmt.Printf("%s\t%s%s\t%s\n", e.Date, dur, approx, e.Summary)
|
|
}
|
|
}
|
|
|
|
func runLogReport(args []string) {
|
|
nodeID, _ := stringFlag(args, "--node")
|
|
vaultPath, _ := stringFlag(args, "--vault")
|
|
|
|
if nodeID == "" {
|
|
fmt.Fprintln(os.Stderr, "Error: --node required")
|
|
os.Exit(1)
|
|
}
|
|
|
|
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)
|
|
}
|
|
defer db.Close()
|
|
|
|
svc := worklog.NewService(db)
|
|
report, err := svc.Report(nodeID)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Report failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Print(report)
|
|
}
|
|
|
|
// --- index ---
|
|
|
|
func runIndex(args []string) {
|
|
if len(args) == 0 {
|
|
fmt.Println("verstak index — manage search index")
|
|
fmt.Println()
|
|
fmt.Println("Usage: verstak index <command> [options]")
|
|
fmt.Println()
|
|
fmt.Println("Commands:")
|
|
fmt.Println(" rebuild Rebuild FTS5 search index")
|
|
os.Exit(1)
|
|
}
|
|
switch args[0] {
|
|
case "rebuild":
|
|
runIndexRebuild(args[1:])
|
|
case "--help", "-h":
|
|
runIndex(nil)
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "Unknown index command: %s\n", args[0])
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func runIndexRebuild(args []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)
|
|
}
|
|
defer db.Close()
|
|
|
|
svc := search.NewService(db)
|
|
if err := svc.Rebuild(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Rebuild failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Index all node titles.
|
|
rows, err := db.Query(
|
|
`SELECT id, title, type, section FROM nodes WHERE deleted_at IS NULL`)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Query nodes: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer rows.Close()
|
|
|
|
count := 0
|
|
for rows.Next() {
|
|
var id, title, nodeType, section string
|
|
if err := rows.Scan(&id, &title, &nodeType, §ion); err != nil {
|
|
continue
|
|
}
|
|
tags := section
|
|
if tags != "" {
|
|
tags = "section:" + tags
|
|
}
|
|
svc.Index(id, title, "", "", tags, nodeType)
|
|
count++
|
|
}
|
|
|
|
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 <command> [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
|
|
}
|
|
|
|
_, _, lastSeq, _, _ := syncSvc.GetState()
|
|
for i := range unpushed {
|
|
unpushed[i].LastSeenServerSeq = lastSeq
|
|
}
|
|
|
|
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))
|
|
if len(result.Conflicts) > 0 {
|
|
fmt.Printf("WARNING: %d conflict(s) detected\n", len(result.Conflicts))
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
_, _, lastSeq, _, err := syncSvc.GetState()
|
|
if err != nil {
|
|
lastSeq = 0
|
|
}
|
|
|
|
result, err := client.Pull(lastSeq)
|
|
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 seq: %d)\n", len(result.Ops), result.ServerSequence)
|
|
}
|
|
|
|
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) {
|
|
vaultPath, _ := stringFlag(args, "--vault")
|
|
|
|
if len(args) == 0 || args[0] == "--help" || args[0] == "-h" {
|
|
fmt.Println("verstak plugin — manage plugins")
|
|
fmt.Println()
|
|
fmt.Println("Usage: verstak plugin <command> [options]")
|
|
fmt.Println()
|
|
fmt.Println("Commands:")
|
|
fmt.Println(" list List discovered plugins")
|
|
fmt.Println(" enable NAME Enable a plugin")
|
|
fmt.Println(" disable NAME Disable a plugin")
|
|
fmt.Println(" templates List available templates")
|
|
os.Exit(0)
|
|
}
|
|
|
|
abs, _ := filepath.Abs(vaultPath)
|
|
|
|
switch args[0] {
|
|
case "list":
|
|
mgr := plugins.NewManager(abs)
|
|
mgr.Discover()
|
|
pp := mgr.Plugins()
|
|
if len(pp) == 0 {
|
|
fmt.Println("No plugins found.")
|
|
fmt.Println("Put plugins in .verstak/plugins/<name>/plugin.json")
|
|
return
|
|
}
|
|
for _, p := range pp {
|
|
status := "on"
|
|
if !p.Active {
|
|
status = "off"
|
|
}
|
|
fmt.Printf("%-20s %-6s %s\n", p.Meta.Name, status, p.Meta.Description)
|
|
}
|
|
case "enable", "disable":
|
|
if len(args) < 2 {
|
|
fmt.Fprintln(os.Stderr, "Error: plugin name required")
|
|
os.Exit(1)
|
|
}
|
|
mgr := plugins.NewManager(abs)
|
|
mgr.Discover()
|
|
name := args[1]
|
|
if args[0] == "enable" {
|
|
mgr.Enable(name)
|
|
} else {
|
|
mgr.Disable(name)
|
|
}
|
|
fmt.Printf("%s %s\n", args[0]+"d", name)
|
|
case "templates":
|
|
mgr := plugins.NewManager(abs)
|
|
mgr.Discover()
|
|
tmpls := mgr.Templates()
|
|
if len(tmpls) == 0 {
|
|
fmt.Println("No templates found.")
|
|
return
|
|
}
|
|
for _, t := range tmpls {
|
|
fmt.Printf("%-20s %-10s %s\n", t.Name, fmt.Sprintf("[%s]", t.Plugin), t.Description)
|
|
}
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "Unknown plugin command: %s\n", args[0])
|
|
os.Exit(1)
|
|
}
|
|
}
|