-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcommand.go
More file actions
208 lines (183 loc) · 5.52 KB
/
command.go
File metadata and controls
208 lines (183 loc) · 5.52 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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
// 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
import (
"sync"
)
// CommandAction is the function signature for command handlers.
//
// func(opts core.Options) core.Result
type CommandAction func(Options) Result
// CommandLifecycle is implemented by commands that support managed lifecycle.
// Basic commands only need an action. Daemon commands implement Start/Stop/Signal
// via go-process.
type CommandLifecycle interface {
Start(Options) Result
Stop() Result
Restart() Result
Reload() Result
Signal(string) Result
}
// Command is the DTO for an executable operation.
type Command struct {
Name string
Description string // i18n key — derived from path if empty
Path string // "deploy/to/homelab"
Action CommandAction // business logic
Lifecycle CommandLifecycle // optional — provided by go-process
Flags Options // declared flags
Hidden bool
commands map[string]*Command // child commands (internal)
mu sync.RWMutex
}
// 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.Options{{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)
}
// Start delegates to the lifecycle implementation if available.
func (cmd *Command) Start(opts Options) Result {
if cmd.Lifecycle != nil {
return cmd.Lifecycle.Start(opts)
}
return cmd.Run(opts)
}
// Stop delegates to the lifecycle implementation.
func (cmd *Command) Stop() Result {
if cmd.Lifecycle != nil {
return cmd.Lifecycle.Stop()
}
return Result{}
}
// Restart delegates to the lifecycle implementation.
func (cmd *Command) Restart() Result {
if cmd.Lifecycle != nil {
return cmd.Lifecycle.Restart()
}
return Result{}
}
// Reload delegates to the lifecycle implementation.
func (cmd *Command) Reload() Result {
if cmd.Lifecycle != nil {
return cmd.Lifecycle.Reload()
}
return Result{}
}
// Signal delegates to the lifecycle implementation.
func (cmd *Command) Signal(sig string) Result {
if cmd.Lifecycle != nil {
return cmd.Lifecycle.Signal(sig)
}
return Result{}
}
// --- Command Registry (on Core) ---
// commandRegistry holds the command tree.
type commandRegistry struct {
commands map[string]*Command
mu sync.RWMutex
}
// 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 {
c.commands.mu.RLock()
cmd, ok := c.commands.commands[path]
c.commands.mu.RUnlock()
return Result{cmd, ok}
}
if path == "" || HasPrefix(path, "/") || HasSuffix(path, "/") || Contains(path, "//") {
return Result{E("core.Command", Concat("invalid command path: \"", path, "\""), nil), false}
}
c.commands.mu.Lock()
defer c.commands.mu.Unlock()
if existing, exists := c.commands.commands[path]; exists && (existing.Action != nil || existing.Lifecycle != nil) {
return Result{E("core.Command", Concat("command \"", path, "\" already registered"), nil), false}
}
cmd := &command[0]
cmd.Name = pathName(path)
cmd.Path = path
if cmd.commands == nil {
cmd.commands = make(map[string]*Command)
}
// Preserve existing subtree when overwriting a placeholder parent
if existing, exists := c.commands.commands[path]; exists {
for k, v := range existing.commands {
if _, has := cmd.commands[k]; !has {
cmd.commands[k] = v
}
}
}
c.commands.commands[path] = cmd
// Build parent chain — "deploy/to/homelab" creates "deploy" and "deploy/to" if missing
parts := Split(path, "/")
for i := len(parts) - 1; i > 0; i-- {
parentPath := JoinPath(parts[:i]...)
if _, exists := c.commands.commands[parentPath]; !exists {
c.commands.commands[parentPath] = &Command{
Name: parts[i-1],
Path: parentPath,
commands: make(map[string]*Command),
}
}
c.commands.commands[parentPath].commands[parts[i]] = cmd
cmd = c.commands.commands[parentPath]
}
return Result{OK: true}
}
// Commands returns all registered command paths.
//
// paths := c.Commands()
func (c *Core) Commands() []string {
if c.commands == nil {
return nil
}
c.commands.mu.RLock()
defer c.commands.mu.RUnlock()
var paths []string
for k := range c.commands.commands {
paths = append(paths, k)
}
return paths
}
// 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]
}