step 3: nodes repository + CRUD + CLI node commands
- types.go: TypeSpace/Case/Folder/Note/... + Slugify() - node.go: Node struct, Meta, IsDeleted/IsRoot helpers - repository.go: full CRUD (Create, Get, GetActive, ListChildren, ListRoots, UpdateTitle, Move, SoftDelete) + Meta KV (MetaSet, MetaGet, MetaList) - node_cmd.go: thin wrappers around repository - main.go: verstak node create/list/get/move/delete subcommands - repository_test.go: 12 tests covering all CRUD paths Acceptance: go build pass, go test pass (12 tests), CLI create+list+get+move+delete all working.
This commit is contained in:
+140
-15
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"verstak/internal/core/vault"
|
||||
)
|
||||
@@ -22,7 +23,9 @@ func main() {
|
||||
case "--help", "-h":
|
||||
usage()
|
||||
case "init":
|
||||
runInit()
|
||||
runInit(os.Args[2:])
|
||||
case "node":
|
||||
runNode(os.Args[2:])
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", os.Args[1])
|
||||
os.Exit(1)
|
||||
@@ -35,34 +38,156 @@ func usage() {
|
||||
fmt.Println("Usage: verstak <command> [flags]")
|
||||
fmt.Println()
|
||||
fmt.Println("Commands:")
|
||||
fmt.Println(" init --vault PATH Initialize a new vault at PATH")
|
||||
fmt.Println(" --version Show version")
|
||||
fmt.Println(" --help Show this help")
|
||||
fmt.Println(" init Initialize a new vault")
|
||||
fmt.Println(" node Manage nodes")
|
||||
fmt.Println(" --version Show version")
|
||||
fmt.Println(" --help Show this help")
|
||||
}
|
||||
|
||||
func runInit() {
|
||||
vaultPath := "."
|
||||
// Parse --vault flag
|
||||
for i := 2; i < len(os.Args); i++ {
|
||||
if os.Args[i] == "--vault" && i+1 < len(os.Args) {
|
||||
vaultPath = os.Args[i+1]
|
||||
break
|
||||
}
|
||||
}
|
||||
// --- 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 "."
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
// Command helpers for the node subcommand.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"verstak/internal/core/nodes"
|
||||
"verstak/internal/core/storage"
|
||||
)
|
||||
|
||||
func runNodeCreate(vault, parentID, typ, title string) error {
|
||||
dbPath := filepath.Join(vault, ".verstak", "index.db")
|
||||
db, err := storage.Open(dbPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open db: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
repo := nodes.NewRepository(db)
|
||||
n, err := repo.Create(parentID, typ, title)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("created\t%s\t%s\t%s\n", n.ID, n.Type, n.Title)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runNodeGet(vault, id string) error {
|
||||
dbPath := filepath.Join(vault, ".verstak", "index.db")
|
||||
db, err := storage.Open(dbPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open db: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
repo := nodes.NewRepository(db)
|
||||
n, err := repo.GetActive(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("id=%s\n", n.ID)
|
||||
fmt.Printf("parent=%v\n", ptrStr(n.ParentID))
|
||||
fmt.Printf("type=%s\n", n.Type)
|
||||
fmt.Printf("title=%s\n", n.Title)
|
||||
fmt.Printf("slug=%s\n", n.Slug)
|
||||
fmt.Printf("revision=%d\n", n.Revision)
|
||||
fmt.Printf("created=%s\n", n.CreatedAt)
|
||||
fmt.Printf("updated=%s\n", n.UpdatedAt)
|
||||
meta, _ := repo.MetaList(n.ID)
|
||||
for _, m := range meta {
|
||||
fmt.Printf("meta:%s=%s\n", m.Key, m.Value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runNodeList(vault, parentID string) error {
|
||||
dbPath := filepath.Join(vault, ".verstak", "index.db")
|
||||
db, err := storage.Open(dbPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open db: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
repo := nodes.NewRepository(db)
|
||||
var list []nodes.Node
|
||||
if parentID == "" {
|
||||
list, err = repo.ListRoots(false)
|
||||
} else {
|
||||
list, err = repo.ListChildren(parentID, false)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, n := range list {
|
||||
fmt.Printf("%s\t%s\t%s\n", n.ID, n.Type, n.Title)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runNodeMove(vault, id, parentID string, sortOrder int) error {
|
||||
dbPath := filepath.Join(vault, ".verstak", "index.db")
|
||||
db, err := storage.Open(dbPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open db: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
repo := nodes.NewRepository(db)
|
||||
if err := repo.Move(id, parentID, sortOrder); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("moved")
|
||||
return nil
|
||||
}
|
||||
|
||||
func runNodeDelete(vault, id string) error {
|
||||
dbPath := filepath.Join(vault, ".verstak", "index.db")
|
||||
db, err := storage.Open(dbPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open db: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
repo := nodes.NewRepository(db)
|
||||
if err := repo.SoftDelete(id); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("deleted")
|
||||
return nil
|
||||
}
|
||||
|
||||
func nodeUsage() {
|
||||
fmt.Println("verstak node — manage nodes in the vault")
|
||||
fmt.Println()
|
||||
fmt.Println("Usage: verstak node <command> [options]")
|
||||
fmt.Println()
|
||||
fmt.Println("Commands:")
|
||||
fmt.Println(" create --type TYPE --title TITLE [--parent ID] Create a node")
|
||||
fmt.Println(" list [--parent ID] List root or children")
|
||||
fmt.Println(" get --id ID Show node details")
|
||||
fmt.Println(" move --id ID [--parent ID] [--sort N] Move node")
|
||||
fmt.Println(" delete --id ID Soft-delete node")
|
||||
fmt.Println()
|
||||
fmt.Println("Types: space, case, folder, note, document, file, action, recipe, secret, worklog, link")
|
||||
}
|
||||
|
||||
func ptrStr(p *string) string {
|
||||
if p == nil {
|
||||
return ""
|
||||
}
|
||||
return *p
|
||||
}
|
||||
Reference in New Issue
Block a user