-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcommand.go
More file actions
162 lines (146 loc) · 4.98 KB
/
Copy pathcommand.go
File metadata and controls
162 lines (146 loc) · 4.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
// SPDX-License-Identifier: EUPL-1.2
// Command is a DTO representing an executable operation.
// Commands don't know if they're root, child, or nested — the tree
// structure comes from composition via path-based registration.
//
// Register a command:
//
// c.Command("deploy", func(opts core.Options) core.Result {
// return core.Result{"deployed", true}
// })
//
// Register a nested command:
//
// c.Command("deploy/to/homelab", handler)
//
// Description is an i18n key — derived from path if omitted:
//
// "deploy" → "cmd.deploy.description"
// "deploy/to/homelab" → "cmd.deploy.to.homelab.description"
package core
// CommandAction is the function signature for command handlers.
//
// func(opts core.Options) core.Result
type CommandAction func(Options) Result
// Command is the DTO for an executable operation.
// Commands are declarative — they carry enough information for multiple consumers:
//
// - core.Cli() runs the Action
//
// - core/cli adds rich help, completion, man pages
//
// - go-process wraps Managed commands with lifecycle (PID, health, signals)
//
// c.Command("serve", core.Command{
// Action: handler,
// Managed: "process.daemon", // go-process provides start/stop/restart
// })
//
// Usage:
//
// cmd := core.Command{Action: func(opts core.Options) core.Result {
// return core.Result{Value: core.Sprintf("deploy %s", opts.String("target")), OK: true}
// }}
// r := cmd.Run(core.NewOptions(core.Option{Key: "target", Value: "homelab"}))
// if !r.OK { return r }
type Command struct {
Name string
Description string // i18n key — derived from path if empty
Path string // "deploy/to/homelab"
Action CommandAction // business logic
Managed string // "" = one-shot, "process.daemon" = managed lifecycle
Flags Options // declared flags
Hidden bool
}
// I18nKey returns the i18n key for this command's description.
//
// cmd with path "deploy/to/homelab" → "cmd.deploy.to.homelab.description"
func (cmd *Command) I18nKey() string {
if cmd.Description != "" {
return cmd.Description
}
path := cmd.Path
if path == "" {
path = cmd.Name
}
return Concat("cmd.", Replace(path, "/", "."), ".description")
}
// Run executes the command's action with the given options.
//
// result := cmd.Run(core.NewOptions(core.Option{Key: "target", Value: "homelab"}))
func (cmd *Command) Run(opts Options) Result {
if cmd.Action == nil {
return Result{E("core.Command.Run", Concat("command \"", cmd.Path, "\" is not executable"), nil), false}
}
return cmd.Action(opts)
}
// IsManaged returns true if this command has a managed lifecycle.
//
// if cmd.IsManaged() { /* go-process handles start/stop */ }
func (cmd *Command) IsManaged() bool {
return cmd.Managed != ""
}
// --- Command Registry (on Core) ---
// CommandRegistry holds the command tree. Embeds Registry[*Command]
// for thread-safe named storage with insertion order.
//
// registry := &core.CommandRegistry{Registry: core.NewRegistry[*core.Command]()}
// registry.Set("deploy/to/homelab", &core.Command{Name: "homelab"})
type CommandRegistry struct {
*Registry[*Command]
}
// Command gets or registers a command by path.
//
// c.Command("deploy", Command{Action: handler})
// r := c.Command("deploy")
func (c *Core) Command(path string, command ...Command) Result {
if len(command) == 0 {
return c.commands.Get(path)
}
if path == "" || HasPrefix(path, "/") || HasSuffix(path, "/") || Contains(path, "//") {
return Result{E("core.Command", Concat("invalid command path: \"", path, "\""), nil), false}
}
// Check for duplicate executable command (existence-check — must see disabled)
if r := c.commands.GetIncludingDisabled(path); r.OK {
existing := r.Value.(*Command)
if existing.Action != nil || existing.IsManaged() {
return Result{E("core.Command", Concat("command \"", path, "\" already registered"), nil), false}
}
}
cmd := &command[0]
cmd.Name = pathName(path)
cmd.Path = path
c.commands.Set(path, cmd)
// Ensure ancestor paths exist as placeholders so help/listing render the
// tree. Children are not tracked on the Command — the flat path-keyed
// registry IS the tree; derive a node's children with
// c.commands.List("<path>/*"). Overwriting a placeholder with a real
// command therefore can't lose its subtree (children are independent
// flat entries).
parts := Split(path, "/")
for i := len(parts) - 1; i > 0; i-- {
parentPath := JoinPath(parts[:i]...)
if !c.commands.GetIncludingDisabled(parentPath).OK {
c.commands.Set(parentPath, &Command{
Name: parts[i-1],
Path: parentPath,
})
}
}
return Result{OK: true}
}
// Commands returns all registered command paths in registration order.
//
// paths := c.Commands()
func (c *Core) Commands() []string {
if c.commands == nil {
return nil
}
return c.commands.Names()
}
// pathName extracts the last segment of a path.
// "deploy/to/homelab" → "homelab"
func pathName(path string) string {
parts := Split(path, "/")
return parts[len(parts)-1]
}