refactor(cli): register commands through Core framework lifecycle

Replace the RegisterCommands/attachRegisteredCommands side-channel with
WithCommands(), which wraps command registration functions as framework
services. Commands now participate in the Core lifecycle via OnStartup,
receiving the root cobra.Command through Core.App.

Main() accepts variadic framework.Option so binaries pass their commands
explicitly — no init(), no blank imports, no global state.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-02-21 22:06:40 +00:00
parent 8e7fb0e5a3
commit 2a90ae65b7
4 changed files with 45 additions and 58 deletions

View file

@ -13,9 +13,3 @@
//
// Sets MACOSX_DEPLOYMENT_TARGET to suppress linker warnings on macOS.
package gocmd
import "forge.lthn.ai/core/go/pkg/cli"
func init() {
cli.RegisterCommands(AddGoCommands)
}

View file

@ -48,9 +48,16 @@ func SemVer() string {
}
// Main initialises and runs the CLI application.
// This is the main entry point for the CLI.
// Pass command services via WithCommands to register CLI commands
// through the Core framework lifecycle.
//
// cli.Main(
// cli.WithCommands("config", config.AddConfigCommands),
// cli.WithCommands("doctor", doctor.AddDoctorCommands),
// )
//
// Exits with code 1 on error or panic.
func Main() {
func Main(commands ...framework.Option) {
// Recovery from panics
defer func() {
if r := recover(); r != nil {
@ -60,17 +67,21 @@ func Main() {
}
}()
// Core services load first, then command services
services := []framework.Option{
framework.WithName("i18n", NewI18nService(I18nOptions{})),
framework.WithName("log", NewLogService(log.Options{
Level: log.LevelInfo,
})),
framework.WithName("workspace", workspace.New),
}
services = append(services, commands...)
// Initialise CLI runtime with services
if err := Init(Options{
AppName: AppName,
Version: SemVer(),
Services: []framework.Option{
framework.WithName("i18n", NewI18nService(I18nOptions{})),
framework.WithName("log", NewLogService(log.Options{
Level: log.LevelInfo,
})),
framework.WithName("workspace", workspace.New),
},
AppName: AppName,
Version: SemVer(),
Services: services,
}); err != nil {
Error(err.Error())
os.Exit(1)

View file

@ -2,49 +2,34 @@
package cli
import (
"sync"
"context"
"forge.lthn.ai/core/go/pkg/framework"
"github.com/spf13/cobra"
)
// CommandRegistration is a function that adds commands to the root.
type CommandRegistration func(root *cobra.Command)
var (
registeredCommands []CommandRegistration
registeredCommandsMu sync.Mutex
commandsAttached bool
)
// RegisterCommands registers a function that adds commands to the CLI.
// Call this in your package's init() to register commands.
// WithCommands creates a framework Option that registers a command group.
// The register function receives the root command during service startup,
// allowing commands to participate in the Core lifecycle.
//
// func init() {
// cli.RegisterCommands(AddCommands)
// }
//
// func AddCommands(root *cobra.Command) {
// root.AddCommand(myCmd)
// }
func RegisterCommands(fn CommandRegistration) {
registeredCommandsMu.Lock()
defer registeredCommandsMu.Unlock()
registeredCommands = append(registeredCommands, fn)
// If commands already attached (CLI already running), attach immediately
if commandsAttached && instance != nil && instance.root != nil {
fn(instance.root)
}
// cli.Main(
// cli.WithCommands("config", config.AddConfigCommands),
// cli.WithCommands("doctor", doctor.AddDoctorCommands),
// )
func WithCommands(name string, register func(root *Command)) framework.Option {
return framework.WithName("cmd."+name, func(c *framework.Core) (any, error) {
return &commandService{core: c, register: register}, nil
})
}
// attachRegisteredCommands calls all registered command functions.
// Called by Init() after creating the root command.
func attachRegisteredCommands(root *cobra.Command) {
registeredCommandsMu.Lock()
defer registeredCommandsMu.Unlock()
for _, fn := range registeredCommands {
fn(root)
}
commandsAttached = true
type commandService struct {
core *framework.Core
register func(root *Command)
}
func (s *commandService) OnStartup(_ context.Context) error {
if root, ok := s.core.App.(*cobra.Command); ok {
s.register(root)
}
return nil
}

View file

@ -63,9 +63,6 @@ func Init(opts Options) error {
SilenceUsage: true,
}
// Attach all registered commands
attachRegisteredCommands(rootCmd)
// Build signal service options
var signalOpts []SignalOption
if opts.OnReload != nil {