refactor: migrate to dappco.re/go/core + Options{} API
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
542698c579
commit
92da6e8a73
7 changed files with 79 additions and 144 deletions
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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=
|
||||
|
|
|
|||
2
go.mod
2
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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
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())
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
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
|
||||
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)
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import (
|
|||
"sync"
|
||||
"syscall"
|
||||
|
||||
"forge.lthn.ai/core/go/pkg/core"
|
||||
"dappco.re/go/core"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
@ -41,7 +41,8 @@ type runtime struct {
|
|||
type Options struct {
|
||||
AppName string
|
||||
Version string
|
||||
Services []core.Option // Additional services to register
|
||||
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...)),
|
||||
// Register additional services
|
||||
for _, svc := range opts.Services {
|
||||
if svc.Name != "" {
|
||||
c.Service(svc.Name, svc)
|
||||
}
|
||||
coreOpts = append(coreOpts, opts.Services...)
|
||||
coreOpts = append(coreOpts, core.WithServiceLock())
|
||||
|
||||
c, err := core.New(coreOpts...)
|
||||
if err != nil {
|
||||
initErr = err
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
|
||||
instance = &runtime{
|
||||
|
|
@ -91,8 +102,11 @@ func Init(opts Options) error {
|
|||
cancel: cancel,
|
||||
}
|
||||
|
||||
if err := c.ServiceStartup(ctx, nil); err != nil {
|
||||
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}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue