Skip to content

Commit e177418

Browse files
author
Virgil
committed
Merge pull request '[agent/claude] Fix core/cli to unblock core/agent build. Remove core.WithNa...' (#8) from agent/fix-core-cli-to-unblock-core-agent-build into main
2 parents 92da6e8 + bcbc259 commit e177418

8 files changed

Lines changed: 65 additions & 168 deletions

File tree

.gitignore

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
1-
wails3
2-
.task
3-
vendor/
4-
.idea
5-
node_modules/
1+
.idea/
2+
.vscode/
63
.DS_Store
74
*.log
8-
.env
9-
.env.*.local
10-
coverage/
11-
coverage.out
12-
coverage.html
13-
*.cache
14-
/coverage.txt
15-
bin/
5+
.core/
6+
7+
# Build artefacts
168
dist/
17-
tasks
18-
/cli
9+
bin/
1910
/core
20-
local.test
21-
/i18n-validate
22-
.angular/
11+
/cli
2312

24-
patch_cov.*
13+
# Go
14+
vendor/
2515
go.work.sum
26-
.kb
27-
.core/
28-
.idea/
16+
coverage/
17+
coverage.out
18+
coverage.html
19+
coverage.txt
20+
21+
# Environment / secrets
22+
.env
23+
.env.*.local
24+
25+
# OS / tooling
26+
.task
27+
*.cache
28+
node_modules/

docs/pkg/cli/daemon.md

Lines changed: 3 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: Daemon process management, PID files, health checks, and execution
55

66
# Daemon Mode
77

8-
The framework provides both low-level daemon primitives and a high-level command group that adds `start`, `stop`, `status`, and `run` subcommands to your CLI.
8+
The framework provides execution mode detection and signal handling for daemon processes.
99

1010
## Execution Modes
1111

@@ -29,63 +29,9 @@ cli.IsStdinTTY() // stdin is a terminal?
2929
cli.IsStderrTTY() // stderr is a terminal?
3030
```
3131

32-
## Adding Daemon Commands
32+
## Simple Daemon
3333

34-
`AddDaemonCommand` registers a command group with four subcommands:
35-
36-
```go
37-
func AddMyCommands(root *cli.Command) {
38-
cli.AddDaemonCommand(root, cli.DaemonCommandConfig{
39-
Name: "daemon", // Command group name (default: "daemon")
40-
Description: "Manage the worker", // Short description
41-
PIDFile: "/var/run/myapp.pid",
42-
HealthAddr: ":9090",
43-
RunForeground: func(ctx context.Context, daemon *process.Daemon) error {
44-
// Your long-running service logic here.
45-
// ctx is cancelled on SIGINT/SIGTERM.
46-
return runWorker(ctx)
47-
},
48-
})
49-
}
50-
```
51-
52-
This creates:
53-
54-
- `myapp daemon start` -- Re-executes the binary as a background process with `CORE_DAEMON=1`
55-
- `myapp daemon stop` -- Sends SIGTERM to the daemon, waits for shutdown (30s timeout, then SIGKILL)
56-
- `myapp daemon status` -- Reports whether the daemon is running and queries health endpoints
57-
- `myapp daemon run` -- Runs in the foreground (for development or process managers like systemd)
58-
59-
### Custom Persistent Flags
60-
61-
Add flags that apply to all daemon subcommands:
62-
63-
```go
64-
cli.AddDaemonCommand(root, cli.DaemonCommandConfig{
65-
// ...
66-
Flags: func(cmd *cli.Command) {
67-
cli.PersistentStringFlag(cmd, &configPath, "config", "c", "", "Config file")
68-
},
69-
ExtraStartArgs: func() []string {
70-
return []string{"--config", configPath}
71-
},
72-
})
73-
```
74-
75-
`ExtraStartArgs` passes additional flags when re-executing the binary as a daemon.
76-
77-
### Health Endpoints
78-
79-
When `HealthAddr` is set, the daemon serves:
80-
81-
- `GET /health` -- Liveness check (200 if server is up, 503 if health checks fail)
82-
- `GET /ready` -- Readiness check (200 if `daemon.SetReady(true)` has been called)
83-
84-
The `start` command waits up to 5 seconds for the health endpoint to become available before reporting success.
85-
86-
## Simple Daemon (Manual)
87-
88-
For cases where you do not need the full command group:
34+
Use `cli.Context()` for cancellation-aware daemon loops:
8935

9036
```go
9137
func runDaemon(cmd *cli.Command, args []string) error {
@@ -117,15 +63,3 @@ cli.Init(cli.Options{
11763
```
11864

11965
No manual signal handling is needed in commands. Use `cli.Context()` for cancellation-aware operations.
120-
121-
## DaemonCommandConfig Reference
122-
123-
| Field | Type | Description |
124-
|-------|------|-------------|
125-
| `Name` | `string` | Command group name (default: `"daemon"`) |
126-
| `Description` | `string` | Short description for help text |
127-
| `PIDFile` | `string` | PID file path (default flag value) |
128-
| `HealthAddr` | `string` | Health check listen address (default flag value) |
129-
| `RunForeground` | `func(ctx, daemon) error` | Service logic for foreground/daemon mode |
130-
| `Flags` | `func(cmd)` | Registers custom persistent flags |
131-
| `ExtraStartArgs` | `func() []string` | Additional args for background re-exec |

docs/pkg/cli/getting-started.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,10 @@ If a command returns an `*ExitError`, the process exits with that code. All othe
5757
This is the preferred way to register commands. It wraps your registration function in a Core service that participates in the lifecycle:
5858

5959
```go
60-
func WithCommands(name string, register func(root *Command)) core.Option
60+
func WithCommands(name string, register func(root *Command), localeFS ...fs.FS) CommandSetup
6161
```
6262

63-
During startup, the Core framework calls your function with the root cobra command. Your function adds subcommands to it:
63+
During `Main()`, the CLI calls your function with the Core instance. Internally it retrieves the root cobra command and passes it to your register function:
6464

6565
```go
6666
func AddScoreCommands(root *cli.Command) {
@@ -98,18 +98,17 @@ func main() {
9898
}
9999
```
100100

101-
Where `Commands()` returns a slice of framework options:
101+
Where `Commands()` returns a slice of `CommandSetup` functions:
102102

103103
```go
104104
package lemcmd
105105

106106
import (
107-
"forge.lthn.ai/core/go/pkg/core"
108107
"forge.lthn.ai/core/cli/pkg/cli"
109108
)
110109

111-
func Commands() []core.Option {
112-
return []core.Option{
110+
func Commands() []cli.CommandSetup {
111+
return []cli.CommandSetup{
113112
cli.WithCommands("score", addScoreCommands),
114113
cli.WithCommands("gen", addGenCommands),
115114
cli.WithCommands("data", addDataCommands),
@@ -141,7 +140,7 @@ If you need more control over the lifecycle:
141140
cli.Init(cli.Options{
142141
AppName: "myapp",
143142
Version: "1.0.0",
144-
Services: []core.Option{...},
143+
Services: []core.Service{...},
145144
OnReload: func() error { return reloadConfig() },
146145
})
147146
defer cli.Shutdown()

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ require (
1515
)
1616

1717
require (
18+
forge.lthn.ai/core/go v0.3.2 // indirect
1819
forge.lthn.ai/core/go-inference v0.1.7 // indirect
1920
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
2021
github.com/charmbracelet/colorprofile v0.4.3 // indirect

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
forge.lthn.ai/core/go v0.3.3 h1:kYYZ2nRYy0/Be3cyuLJspRjLqTMxpckVyhb/7Sw2gd0=
2-
forge.lthn.ai/core/go v0.3.3/go.mod h1:Cp4ac25pghvO2iqOu59t1GyngTKVOzKB5/VPdhRi9CQ=
1+
dappco.re/go/core v0.4.7 h1:KmIA/2lo6rl1NMtLrKqCWfMlUqpDZYH3q0/d10dTtGA=
2+
dappco.re/go/core v0.4.7/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A=
3+
forge.lthn.ai/core/go v0.3.2 h1:VB9pW6ggqBhe438cjfE2iSI5Lg+62MmRbaOFglZM+nQ=
4+
forge.lthn.ai/core/go v0.3.2/go.mod h1:f7/zb3Labn4ARfwTq5Bi2AFHY+uxyPHozO+hLb54eFo=
35
forge.lthn.ai/core/go-i18n v0.1.7 h1:aHkAoc3W8fw3RPNvw/UszQbjyFWXHszzbZgty3SwyAA=
46
forge.lthn.ai/core/go-i18n v0.1.7/go.mod h1:0VDjwtY99NSj2iqwrI09h5GUsJeM9s48MLkr+/Dn4G8=
57
forge.lthn.ai/core/go-inference v0.1.7 h1:9Dy6v03jX5ZRH3n5iTzlYyGtucuBIgSe+S7GWvBzx9Q=

pkg/cli/commands.go

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,7 @@ func WithCommands(name string, register func(root *Command), localeFS ...fs.FS)
2222
if root, ok := c.App().Runtime.(*cobra.Command); ok {
2323
register(root)
2424
}
25-
// Register locale FS if provided
26-
if len(localeFS) > 0 && localeFS[0] != nil {
27-
registeredCommandsMu.Lock()
28-
registeredLocales = append(registeredLocales, localeFS[0])
29-
registeredCommandsMu.Unlock()
30-
}
25+
appendLocales(localeFS...)
3126
}
3227
}
3328

@@ -49,18 +44,33 @@ var (
4944
// }
5045
func RegisterCommands(fn CommandRegistration, localeFS ...fs.FS) {
5146
registeredCommandsMu.Lock()
52-
defer registeredCommandsMu.Unlock()
5347
registeredCommands = append(registeredCommands, fn)
48+
attached := commandsAttached && instance != nil && instance.root != nil
49+
root := instance
50+
registeredCommandsMu.Unlock()
51+
52+
appendLocales(localeFS...)
53+
54+
// If commands already attached (CLI already running), attach immediately
55+
if attached {
56+
fn(root.root)
57+
}
58+
}
59+
60+
// appendLocales appends non-nil locale filesystems to the registry.
61+
func appendLocales(localeFS ...fs.FS) {
62+
var nonempty []fs.FS
5463
for _, lfs := range localeFS {
5564
if lfs != nil {
56-
registeredLocales = append(registeredLocales, lfs)
65+
nonempty = append(nonempty, lfs)
5766
}
5867
}
59-
60-
// If commands already attached (CLI already running), attach immediately
61-
if commandsAttached && instance != nil && instance.root != nil {
62-
fn(instance.root)
68+
if len(nonempty) == 0 {
69+
return
6370
}
71+
registeredCommandsMu.Lock()
72+
registeredLocales = append(registeredLocales, nonempty...)
73+
registeredCommandsMu.Unlock()
6474
}
6575

6676
// RegisteredLocales returns all locale filesystems registered by command packages.

pkg/cli/commands_test.go

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,16 @@ import (
1212
// resetGlobals clears the CLI singleton and command registry for test isolation.
1313
func resetGlobals(t *testing.T) {
1414
t.Helper()
15-
t.Cleanup(func() {
16-
// Restore clean state after each test.
17-
registeredCommandsMu.Lock()
18-
registeredCommands = nil
19-
commandsAttached = false
20-
registeredCommandsMu.Unlock()
21-
if instance != nil {
22-
Shutdown()
23-
}
24-
instance = nil
25-
once = sync.Once{}
26-
})
15+
doReset()
16+
t.Cleanup(doReset)
17+
}
2718

19+
// doReset clears all package-level state. Only safe from a single goroutine
20+
// with no concurrent RegisterCommands calls in flight (i.e. test setup/teardown).
21+
func doReset() {
2822
registeredCommandsMu.Lock()
2923
registeredCommands = nil
24+
registeredLocales = nil
3025
commandsAttached = false
3126
registeredCommandsMu.Unlock()
3227
if instance != nil {

pkg/cli/daemon_cmd_test.go

Lines changed: 0 additions & 44 deletions
This file was deleted.

0 commit comments

Comments
 (0)