From 2a90ae65b7c7e26ec7d8f09fa6c870702948fbf5 Mon Sep 17 00:00:00 2001 From: Snider Date: Sat, 21 Feb 2026 22:06:40 +0000 Subject: [PATCH] refactor(cli): register commands through Core framework lifecycle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- cmd/gocmd/cmd_commands.go | 6 ---- pkg/cli/app.go | 33 ++++++++++++++------- pkg/cli/commands.go | 61 +++++++++++++++------------------------ pkg/cli/runtime.go | 3 -- 4 files changed, 45 insertions(+), 58 deletions(-) diff --git a/cmd/gocmd/cmd_commands.go b/cmd/gocmd/cmd_commands.go index 44b6fb3..5b2943a 100644 --- a/cmd/gocmd/cmd_commands.go +++ b/cmd/gocmd/cmd_commands.go @@ -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) -} diff --git a/pkg/cli/app.go b/pkg/cli/app.go index 7d2d4e2..c02cc16 100644 --- a/pkg/cli/app.go +++ b/pkg/cli/app.go @@ -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) diff --git a/pkg/cli/commands.go b/pkg/cli/commands.go index 20ea2da..f481974 100644 --- a/pkg/cli/commands.go +++ b/pkg/cli/commands.go @@ -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 } diff --git a/pkg/cli/runtime.go b/pkg/cli/runtime.go index 08636f1..c0dd383 100644 --- a/pkg/cli/runtime.go +++ b/pkg/cli/runtime.go @@ -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 {