diff --git a/cmd/core/doctor/cmd_install.go b/cmd/core/doctor/cmd_install.go index 07cfd6a..4ffb59c 100644 --- a/cmd/core/doctor/cmd_install.go +++ b/cmd/core/doctor/cmd_install.go @@ -7,7 +7,7 @@ import ( "forge.lthn.ai/core/go-i18n" ) -// printInstallInstructions prints OS-specific installation instructions +// printInstallInstructions prints OperatingSystem-specific installation instructions func printInstallInstructions() { switch runtime.GOOS { case "darwin": diff --git a/cmd/core/go.mod b/cmd/core/go.mod index 6bd66cc..0d351d8 100644 --- a/cmd/core/go.mod +++ b/cmd/core/go.mod @@ -74,9 +74,10 @@ require ( github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect - github.com/oasdiff/oasdiff v1.12.1 // indirect - github.com/oasdiff/yaml v0.0.0-20260313112342-a3ea61cb4d4c // indirect - github.com/oasdiff/yaml3 v0.0.0-20260224194419-61cd415a242b // indirect + github.com/oasdiff/kin-openapi v0.136.1 // indirect + github.com/oasdiff/oasdiff v1.12.3 // indirect + github.com/oasdiff/yaml v0.0.1 // indirect + github.com/oasdiff/yaml3 v0.0.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect diff --git a/cmd/core/go.sum b/cmd/core/go.sum index e7f28c9..141f923 100644 --- a/cmd/core/go.sum +++ b/cmd/core/go.sum @@ -159,12 +159,10 @@ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/oasdiff/oasdiff v1.12.1 h1:wnvBQS/WSqGqH23u1Jo3XVaF5y5X67TC5znSiy5nIug= -github.com/oasdiff/oasdiff v1.12.1/go.mod h1:4l8lF8SkdyiBVpa7AH3xc+oyDDXS1QTegX25mBS11/E= -github.com/oasdiff/yaml v0.0.0-20260313112342-a3ea61cb4d4c h1:7ACFcSaQsrWtrH4WHHfUqE1C+f8r2uv8KGaW0jTNjus= -github.com/oasdiff/yaml v0.0.0-20260313112342-a3ea61cb4d4c/go.mod h1:JKox4Gszkxt57kj27u7rvi7IFoIULvCZHUsBTUmQM/s= -github.com/oasdiff/yaml3 v0.0.0-20260224194419-61cd415a242b h1:vivRhVUAa9t1q0Db4ZmezBP8pWQWnXHFokZj0AOea2g= -github.com/oasdiff/yaml3 v0.0.0-20260224194419-61cd415a242b/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/oasdiff/kin-openapi v0.136.1 h1:x1G9doDyPcagCNXDcMK5dt5yAmIgsSCiK7F5gPUiQdM= +github.com/oasdiff/oasdiff v1.12.3 h1:eUzJ/AiyyCY1KwUZPv7fosgDyETacIZbFesJrRz+QdY= +github.com/oasdiff/yaml v0.0.1 h1:dPrn0F2PJ7HdzHPndJkArvB2Fw0cwgFdVUKCEkoFuds= +github.com/oasdiff/yaml3 v0.0.1 h1:kReOSraQLTxuuGNX9aNeJ7tcsvUB2MS+iupdUrWe4Z0= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= diff --git a/go.mod b/go.mod index 56f4fd6..dd0a981 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module forge.lthn.ai/core/cli go 1.26.0 -require forge.lthn.ai/core/go v0.3.3 +require dappco.re/go/core v0.4.7 require ( forge.lthn.ai/core/go-i18n v0.1.7 diff --git a/pkg/cli/app.go b/pkg/cli/app.go index f8f339f..fbc96c6 100644 --- a/pkg/cli/app.go +++ b/pkg/cli/app.go @@ -9,7 +9,7 @@ import ( "forge.lthn.ai/core/go-i18n" "forge.lthn.ai/core/go-log" - "forge.lthn.ai/core/go/pkg/core" + "dappco.re/go/core" "github.com/spf13/cobra" ) @@ -60,16 +60,6 @@ func WithAppName(name string) { AppName = name } -// Main initialises and runs the CLI application. -// 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. // LocaleSource pairs a filesystem with a directory for loading translations. type LocaleSource = i18n.FSSource @@ -78,13 +68,16 @@ func WithLocales(fsys fs.FS, dir string) LocaleSource { return LocaleSource{FS: fsys, Dir: dir} } +// CommandSetup is a function that registers commands on the CLI after init. +type CommandSetup func(c *core.Core) + // Main initialises and runs the CLI with the framework's built-in translations. -func Main(commands ...core.Option) { +func Main(commands ...CommandSetup) { MainWithLocales(nil, commands...) } // MainWithLocales initialises and runs the CLI with additional translation sources. -func MainWithLocales(locales []LocaleSource, commands ...core.Option) { +func MainWithLocales(locales []LocaleSource, commands ...CommandSetup) { // Recovery from panics defer func() { if r := recover(); r != nil { @@ -103,25 +96,22 @@ func MainWithLocales(locales []LocaleSource, commands ...core.Option) { extraFS = append(extraFS, i18n.FSSource{FS: lfs, Dir: "."}) } - // Core services load first, then command services - services := []core.Option{ - core.WithName("i18n", i18n.NewCoreService(i18n.ServiceOptions{ - ExtraFS: extraFS, - })), - } - services = append(services, commands...) - - // Initialise CLI runtime with services + // Initialise CLI runtime if err := Init(Options{ - AppName: AppName, - Version: SemVer(), - Services: services, + AppName: AppName, + Version: SemVer(), + I18nSources: extraFS, }); err != nil { Error(err.Error()) os.Exit(1) } defer Shutdown() + // Run command setup functions + for _, setup := range commands { + setup(Core()) + } + // Add completion command to the CLI's root RootCmd().AddCommand(newCompletionCmd()) diff --git a/pkg/cli/commands.go b/pkg/cli/commands.go index b538843..f00ea3c 100644 --- a/pkg/cli/commands.go +++ b/pkg/cli/commands.go @@ -2,80 +2,36 @@ package cli import ( - "context" "io/fs" "iter" "sync" - "forge.lthn.ai/core/go/pkg/core" + "dappco.re/go/core" "github.com/spf13/cobra" ) -// 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. +// WithCommands returns a CommandSetup that registers a command group. +// The register function receives the root cobra command during Main(). // // cli.Main( // cli.WithCommands("config", config.AddConfigCommands), // cli.WithCommands("doctor", doctor.AddDoctorCommands), // ) -// WithCommands creates a framework Option that registers a command group. -// Optionally pass a locale fs.FS as the third argument to provide translations. -// -// cli.WithCommands("dev", dev.AddDevCommands, locales.FS) -func WithCommands(name string, register func(root *Command), localeFS ...fs.FS) core.Option { - return core.WithName("cmd."+name, func(c *core.Core) (any, error) { - svc := &commandService{core: c, name: name, register: register} - if len(localeFS) > 0 { - svc.localeFS = localeFS[0] +func WithCommands(name string, register func(root *Command), localeFS ...fs.FS) CommandSetup { + return func(c *core.Core) { + if root, ok := c.App().Runtime.(*cobra.Command); ok { + register(root) } - return svc, nil - }) -} - -type commandService struct { - core *core.Core - name string - register func(root *Command) - localeFS fs.FS -} - -func (s *commandService) OnStartup(_ context.Context) error { - if root, ok := s.core.App().Runtime.(*cobra.Command); ok { - s.register(root) - // Auto-set Short/Long from i18n keys derived from command name. - // The Conclave's i18n service has already loaded all translations - // from sibling services' LocaleProvider before commands attach. - s.applyI18n(root) - } - return nil -} - -// applyI18n walks commands added by this service and sets Short/Long -// from derived i18n keys if they're empty or still raw keys. -func (s *commandService) applyI18n(root *cobra.Command) { - for _, cmd := range root.Commands() { - key := "cmd." + cmd.Name() - // Only set if Short is empty or looks like a raw key (contains dots) - if cmd.Short == "" || cmd.Short == key+".short" { - if translated := T(key + ".short"); translated != key+".short" { - cmd.Short = translated - } - } - if cmd.Long == "" || cmd.Long == key+".long" { - if translated := T(key + ".long"); translated != key+".long" { - cmd.Long = translated - } + // Register locale FS if provided + if len(localeFS) > 0 && localeFS[0] != nil { + registeredCommandsMu.Lock() + registeredLocales = append(registeredLocales, localeFS[0]) + registeredCommandsMu.Unlock() } } } -// Locales implements core.LocaleProvider. -func (s *commandService) Locales() fs.FS { - return s.localeFS -} - -// CommandRegistration is a function that adds commands to the root. +// CommandRegistration is a function that adds commands to the CLI root. type CommandRegistration func(root *cobra.Command) var ( @@ -138,4 +94,3 @@ func attachRegisteredCommands(root *cobra.Command) { } commandsAttached = true } - diff --git a/pkg/cli/runtime.go b/pkg/cli/runtime.go index 6c8cf7d..17cb6f0 100644 --- a/pkg/cli/runtime.go +++ b/pkg/cli/runtime.go @@ -20,7 +20,7 @@ import ( "sync" "syscall" - "forge.lthn.ai/core/go/pkg/core" + "dappco.re/go/core" "github.com/spf13/cobra" ) @@ -39,9 +39,10 @@ type runtime struct { // Options configures the CLI runtime. type Options struct { - AppName string - Version string - Services []core.Option // Additional services to register + AppName string + Version string + Services []core.Service // Additional services to register + I18nSources []LocaleSource // Additional i18n translation sources // OnReload is called when SIGHUP is received (daemon mode). // Use for configuration reloading. Leave nil to ignore SIGHUP. @@ -63,25 +64,35 @@ func Init(opts Options) error { SilenceUsage: true, } - // Build signal service options - var signalOpts []SignalOption + // Create Core with app identity + c := core.New(core.Options{ + {Key: "name", Value: opts.AppName}, + }) + c.App().Version = opts.Version + c.App().Runtime = rootCmd + + // Register signal service + signalSvc := &signalService{ + cancel: cancel, + sigChan: make(chan os.Signal, 1), + } if opts.OnReload != nil { - signalOpts = append(signalOpts, WithReloadHandler(opts.OnReload)) + signalSvc.onReload = opts.OnReload } + c.Service("signal", core.Service{ + OnStart: func() core.Result { + return signalSvc.start(ctx) + }, + OnStop: func() core.Result { + return signalSvc.stop() + }, + }) - // Build options: app, signal service + any additional services - coreOpts := []core.Option{ - core.WithApp(rootCmd), - core.WithName("signal", newSignalService(cancel, signalOpts...)), - } - coreOpts = append(coreOpts, opts.Services...) - coreOpts = append(coreOpts, core.WithServiceLock()) - - c, err := core.New(coreOpts...) - if err != nil { - initErr = err - cancel() - return + // Register additional services + for _, svc := range opts.Services { + if svc.Name != "" { + c.Service(svc.Name, svc) + } } instance = &runtime{ @@ -91,8 +102,11 @@ func Init(opts Options) error { cancel: cancel, } - if err := c.ServiceStartup(ctx, nil); err != nil { - initErr = err + r := c.ServiceStartup(ctx, nil) + if !r.OK { + if err, ok := r.Value.(error); ok { + initErr = err + } return } @@ -145,7 +159,7 @@ func Shutdown() { _ = instance.core.ServiceShutdown(instance.ctx) } -// --- Signal Service (internal) --- +// --- Signal Srv (internal) --- type signalService struct { cancel context.CancelFunc @@ -154,30 +168,7 @@ type signalService struct { shutdownOnce sync.Once } -// SignalOption configures signal handling. -type SignalOption func(*signalService) - -// WithReloadHandler sets a callback for SIGHUP. -func WithReloadHandler(fn func() error) SignalOption { - return func(s *signalService) { - s.onReload = fn - } -} - -func newSignalService(cancel context.CancelFunc, opts ...SignalOption) func(*core.Core) (any, error) { - return func(c *core.Core) (any, error) { - svc := &signalService{ - cancel: cancel, - sigChan: make(chan os.Signal, 1), - } - for _, opt := range opts { - opt(svc) - } - return svc, nil - } -} - -func (s *signalService) OnStartup(ctx context.Context) error { +func (s *signalService) start(ctx context.Context) core.Result { signals := []os.Signal{syscall.SIGINT, syscall.SIGTERM} if s.onReload != nil { signals = append(signals, syscall.SIGHUP) @@ -207,13 +198,13 @@ func (s *signalService) OnStartup(ctx context.Context) error { } }() - return nil + return core.Result{OK: true} } -func (s *signalService) OnShutdown(ctx context.Context) error { +func (s *signalService) stop() core.Result { s.shutdownOnce.Do(func() { signal.Stop(s.sigChan) close(s.sigChan) }) - return nil + return core.Result{OK: true} }