fix(cli): resolve build errors and clean up stale API references
All checks were successful
Security Scan / security (pull_request) Successful in 18s

Remove orphaned daemon_cmd_test.go referencing undefined AddDaemonCommand/
DaemonCommandConfig symbols. Update docs to reflect current API types
(CommandSetup, core.Service). Restore .gitignore entries for dist/, .env,
and coverage artefacts. Extract appendLocales helper to deduplicate locale
registration. Fix test reset to clear registeredLocales for proper isolation.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-21 22:56:10 +00:00
parent 92da6e8a73
commit bcbc25974e
8 changed files with 67 additions and 170 deletions

44
.gitignore vendored
View file

@ -1,28 +1,28 @@
wails3
.task
vendor/
.idea
node_modules/
.idea/
.vscode/
.DS_Store
*.log
.env
.env.*.local
.core/
# Build artefacts
dist/
bin/
/core
/cli
# Go
vendor/
go.work.sum
coverage/
coverage.out
coverage.html
*.cache
/coverage.txt
bin/
dist/
tasks
/cli
/core
local.test
/i18n-validate
.angular/
coverage.txt
patch_cov.*
go.work.sum
.kb
.core/
.idea/
# Environment / secrets
.env
.env.*.local
# OS / tooling
.task
*.cache
node_modules/

View file

@ -5,7 +5,7 @@ description: Daemon process management, PID files, health checks, and execution
# Daemon Mode
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.
The framework provides execution mode detection and signal handling for daemon processes.
## Execution Modes
@ -29,63 +29,9 @@ cli.IsStdinTTY() // stdin is a terminal?
cli.IsStderrTTY() // stderr is a terminal?
```
## Adding Daemon Commands
## Simple Daemon
`AddDaemonCommand` registers a command group with four subcommands:
```go
func AddMyCommands(root *cli.Command) {
cli.AddDaemonCommand(root, cli.DaemonCommandConfig{
Name: "daemon", // Command group name (default: "daemon")
Description: "Manage the worker", // Short description
PIDFile: "/var/run/myapp.pid",
HealthAddr: ":9090",
RunForeground: func(ctx context.Context, daemon *process.Daemon) error {
// Your long-running service logic here.
// ctx is cancelled on SIGINT/SIGTERM.
return runWorker(ctx)
},
})
}
```
This creates:
- `myapp daemon start` -- Re-executes the binary as a background process with `CORE_DAEMON=1`
- `myapp daemon stop` -- Sends SIGTERM to the daemon, waits for shutdown (30s timeout, then SIGKILL)
- `myapp daemon status` -- Reports whether the daemon is running and queries health endpoints
- `myapp daemon run` -- Runs in the foreground (for development or process managers like systemd)
### Custom Persistent Flags
Add flags that apply to all daemon subcommands:
```go
cli.AddDaemonCommand(root, cli.DaemonCommandConfig{
// ...
Flags: func(cmd *cli.Command) {
cli.PersistentStringFlag(cmd, &configPath, "config", "c", "", "Config file")
},
ExtraStartArgs: func() []string {
return []string{"--config", configPath}
},
})
```
`ExtraStartArgs` passes additional flags when re-executing the binary as a daemon.
### Health Endpoints
When `HealthAddr` is set, the daemon serves:
- `GET /health` -- Liveness check (200 if server is up, 503 if health checks fail)
- `GET /ready` -- Readiness check (200 if `daemon.SetReady(true)` has been called)
The `start` command waits up to 5 seconds for the health endpoint to become available before reporting success.
## Simple Daemon (Manual)
For cases where you do not need the full command group:
Use `cli.Context()` for cancellation-aware daemon loops:
```go
func runDaemon(cmd *cli.Command, args []string) error {
@ -117,15 +63,3 @@ cli.Init(cli.Options{
```
No manual signal handling is needed in commands. Use `cli.Context()` for cancellation-aware operations.
## DaemonCommandConfig Reference
| Field | Type | Description |
|-------|------|-------------|
| `Name` | `string` | Command group name (default: `"daemon"`) |
| `Description` | `string` | Short description for help text |
| `PIDFile` | `string` | PID file path (default flag value) |
| `HealthAddr` | `string` | Health check listen address (default flag value) |
| `RunForeground` | `func(ctx, daemon) error` | Service logic for foreground/daemon mode |
| `Flags` | `func(cmd)` | Registers custom persistent flags |
| `ExtraStartArgs` | `func() []string` | Additional args for background re-exec |

View file

@ -57,10 +57,10 @@ If a command returns an `*ExitError`, the process exits with that code. All othe
This is the preferred way to register commands. It wraps your registration function in a Core service that participates in the lifecycle:
```go
func WithCommands(name string, register func(root *Command)) core.Option
func WithCommands(name string, register func(root *Command), localeFS ...fs.FS) CommandSetup
```
During startup, the Core framework calls your function with the root cobra command. Your function adds subcommands to it:
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:
```go
func AddScoreCommands(root *cli.Command) {
@ -98,18 +98,17 @@ func main() {
}
```
Where `Commands()` returns a slice of framework options:
Where `Commands()` returns a slice of `CommandSetup` functions:
```go
package lemcmd
import (
"forge.lthn.ai/core/go/pkg/core"
"forge.lthn.ai/core/cli/pkg/cli"
)
func Commands() []core.Option {
return []core.Option{
func Commands() []cli.CommandSetup {
return []cli.CommandSetup{
cli.WithCommands("score", addScoreCommands),
cli.WithCommands("gen", addGenCommands),
cli.WithCommands("data", addDataCommands),
@ -141,7 +140,7 @@ If you need more control over the lifecycle:
cli.Init(cli.Options{
AppName: "myapp",
Version: "1.0.0",
Services: []core.Option{...},
Services: []core.Service{...},
OnReload: func() error { return reloadConfig() },
})
defer cli.Shutdown()

1
go.mod
View file

@ -15,6 +15,7 @@ require (
)
require (
forge.lthn.ai/core/go v0.3.2 // indirect
forge.lthn.ai/core/go-inference v0.1.7 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect

6
go.sum
View file

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

View file

@ -22,12 +22,7 @@ func WithCommands(name string, register func(root *Command), localeFS ...fs.FS)
if root, ok := c.App().Runtime.(*cobra.Command); ok {
register(root)
}
// Register locale FS if provided
if len(localeFS) > 0 && localeFS[0] != nil {
registeredCommandsMu.Lock()
registeredLocales = append(registeredLocales, localeFS[0])
registeredCommandsMu.Unlock()
}
appendLocales(localeFS...)
}
}
@ -49,19 +44,34 @@ var (
// }
func RegisterCommands(fn CommandRegistration, localeFS ...fs.FS) {
registeredCommandsMu.Lock()
defer registeredCommandsMu.Unlock()
registeredCommands = append(registeredCommands, fn)
for _, lfs := range localeFS {
if lfs != nil {
registeredLocales = append(registeredLocales, lfs)
attached := commandsAttached && instance != nil && instance.root != nil
root := instance
registeredCommandsMu.Unlock()
appendLocales(localeFS...)
// If commands already attached (CLI already running), attach immediately
if attached {
fn(root.root)
}
}
// If commands already attached (CLI already running), attach immediately
if commandsAttached && instance != nil && instance.root != nil {
fn(instance.root)
// appendLocales appends non-nil locale filesystems to the registry.
func appendLocales(localeFS ...fs.FS) {
var nonempty []fs.FS
for _, lfs := range localeFS {
if lfs != nil {
nonempty = append(nonempty, lfs)
}
}
if len(nonempty) == 0 {
return
}
registeredCommandsMu.Lock()
registeredLocales = append(registeredLocales, nonempty...)
registeredCommandsMu.Unlock()
}
// RegisteredLocales returns all locale filesystems registered by command packages.
func RegisteredLocales() []fs.FS {

View file

@ -12,21 +12,16 @@ import (
// resetGlobals clears the CLI singleton and command registry for test isolation.
func resetGlobals(t *testing.T) {
t.Helper()
t.Cleanup(func() {
// Restore clean state after each test.
registeredCommandsMu.Lock()
registeredCommands = nil
commandsAttached = false
registeredCommandsMu.Unlock()
if instance != nil {
Shutdown()
doReset()
t.Cleanup(doReset)
}
instance = nil
once = sync.Once{}
})
// doReset clears all package-level state. Only safe from a single goroutine
// with no concurrent RegisterCommands calls in flight (i.e. test setup/teardown).
func doReset() {
registeredCommandsMu.Lock()
registeredCommands = nil
registeredLocales = nil
commandsAttached = false
registeredCommandsMu.Unlock()
if instance != nil {

View file

@ -1,44 +0,0 @@
package cli
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAddDaemonCommand_RegistersSubcommands(t *testing.T) {
root := &Command{Use: "test"}
AddDaemonCommand(root, DaemonCommandConfig{
Name: "daemon",
PIDFile: "/tmp/test-daemon.pid",
HealthAddr: "127.0.0.1:0",
})
// Should have the daemon command
daemonCmd, _, err := root.Find([]string{"daemon"})
require.NoError(t, err)
require.NotNil(t, daemonCmd)
// Should have subcommands
var subNames []string
for _, sub := range daemonCmd.Commands() {
subNames = append(subNames, sub.Name())
}
assert.Contains(t, subNames, "start")
assert.Contains(t, subNames, "stop")
assert.Contains(t, subNames, "status")
assert.Contains(t, subNames, "run")
}
func TestDaemonCommandConfig_DefaultName(t *testing.T) {
root := &Command{Use: "test"}
AddDaemonCommand(root, DaemonCommandConfig{})
// Should default to "daemon"
daemonCmd, _, err := root.Find([]string{"daemon"})
require.NoError(t, err)
require.NotNil(t, daemonCmd)
}