fix(cli): resolve build errors and clean up stale API references
All checks were successful
Security Scan / security (pull_request) Successful in 18s
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:
parent
92da6e8a73
commit
bcbc25974e
8 changed files with 67 additions and 170 deletions
44
.gitignore
vendored
44
.gitignore
vendored
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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
1
go.mod
|
|
@ -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
6
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue