step 7: actions — table, service, CLI, GUI tab + confirm dialog

- Migration 005: actions table (node_id, title, kind, command, args_json,
  working_dir, url, confirm_required, capture_output)
- ActionService: Create, Get, ListByNode, Delete, Run
  Run dispatches: open_url/file/folder (xdg-open), run_command/script
  (exec.Command), open_terminal, launch_app
- CLI: verstak action add/list/run/delete
  'run' shows confirm prompt for confirm_required actions
- GUI 'Действия' tab: button list with kind label, confirm_required
  opens editor overlay with action info + confirm, delete button
- 7 unit tests for ActionService

Acceptance: go build ./... pass, go test ./... pass.
This commit is contained in:
2026-05-31 01:52:23 +08:00
parent 9ee6df0d3f
commit dae53fcbba
7 changed files with 801 additions and 4 deletions
+204
View File
@@ -5,7 +5,10 @@ import (
"os"
"path/filepath"
"strconv"
"strings"
"verstak/internal/core/actions"
"verstak/internal/core/storage"
"verstak/internal/core/vault"
)
@@ -26,6 +29,8 @@ func main() {
runInit(os.Args[2:])
case "node":
runNode(os.Args[2:])
case "action":
runAction(os.Args[2:])
default:
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", os.Args[1])
os.Exit(1)
@@ -40,6 +45,7 @@ func usage() {
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(" --help Show this help")
}
@@ -191,3 +197,201 @@ func vaultPathFromFlags(args []string) string {
}
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")
}