2026-02-22 20:42:00 +00:00
|
|
|
// Package cli provides the CLI runtime and utilities.
|
|
|
|
|
package cli
|
|
|
|
|
|
|
|
|
|
import (
|
2026-03-17 01:35:04 +00:00
|
|
|
"io/fs"
|
2026-02-23 04:57:24 +00:00
|
|
|
"iter"
|
2026-02-22 20:42:00 +00:00
|
|
|
"sync"
|
|
|
|
|
|
2026-03-21 20:01:25 +00:00
|
|
|
"dappco.re/go/core"
|
2026-04-02 03:53:21 +00:00
|
|
|
"forge.lthn.ai/core/go-i18n"
|
2026-02-22 20:42:00 +00:00
|
|
|
"github.com/spf13/cobra"
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-21 20:01:25 +00:00
|
|
|
// WithCommands returns a CommandSetup that registers a command group.
|
|
|
|
|
// The register function receives the root cobra command during Main().
|
2026-02-22 23:01:31 +00:00
|
|
|
//
|
|
|
|
|
// cli.Main(
|
|
|
|
|
// cli.WithCommands("config", config.AddConfigCommands),
|
|
|
|
|
// cli.WithCommands("doctor", doctor.AddDoctorCommands),
|
|
|
|
|
// )
|
2026-03-21 20:01:25 +00:00
|
|
|
func WithCommands(name string, register func(root *Command), localeFS ...fs.FS) CommandSetup {
|
|
|
|
|
return func(c *core.Core) {
|
2026-04-02 03:53:21 +00:00
|
|
|
loadLocaleSources(localeSourcesFromFS(localeFS...)...)
|
2026-03-21 20:01:25 +00:00
|
|
|
if root, ok := c.App().Runtime.(*cobra.Command); ok {
|
|
|
|
|
register(root)
|
2026-03-17 05:22:28 +00:00
|
|
|
}
|
2026-03-21 22:56:10 +00:00
|
|
|
appendLocales(localeFS...)
|
2026-03-17 05:22:28 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 20:01:25 +00:00
|
|
|
// CommandRegistration is a function that adds commands to the CLI root.
|
2026-02-22 20:42:00 +00:00
|
|
|
type CommandRegistration func(root *cobra.Command)
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
registeredCommands []CommandRegistration
|
|
|
|
|
registeredCommandsMu sync.Mutex
|
|
|
|
|
commandsAttached bool
|
2026-03-17 01:35:04 +00:00
|
|
|
registeredLocales []fs.FS
|
2026-02-22 20:42:00 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// RegisterCommands registers a function that adds commands to the CLI.
|
2026-03-17 01:35:04 +00:00
|
|
|
// Optionally pass a locale fs.FS to provide translations for the commands.
|
2026-02-22 20:42:00 +00:00
|
|
|
//
|
|
|
|
|
// func init() {
|
2026-03-17 01:35:04 +00:00
|
|
|
// cli.RegisterCommands(AddCommands, locales.FS)
|
2026-02-22 20:42:00 +00:00
|
|
|
// }
|
2026-03-17 01:35:04 +00:00
|
|
|
func RegisterCommands(fn CommandRegistration, localeFS ...fs.FS) {
|
2026-02-22 20:42:00 +00:00
|
|
|
registeredCommandsMu.Lock()
|
|
|
|
|
registeredCommands = append(registeredCommands, fn)
|
2026-03-21 22:56:10 +00:00
|
|
|
attached := commandsAttached && instance != nil && instance.root != nil
|
|
|
|
|
root := instance
|
|
|
|
|
registeredCommandsMu.Unlock()
|
|
|
|
|
|
2026-04-02 03:53:21 +00:00
|
|
|
loadLocaleSources(localeSourcesFromFS(localeFS...)...)
|
2026-03-21 22:56:10 +00:00
|
|
|
appendLocales(localeFS...)
|
|
|
|
|
|
|
|
|
|
// If commands already attached (CLI already running), attach immediately
|
|
|
|
|
if attached {
|
|
|
|
|
fn(root.root)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// appendLocales appends non-nil locale filesystems to the registry.
|
|
|
|
|
func appendLocales(localeFS ...fs.FS) {
|
|
|
|
|
var nonempty []fs.FS
|
2026-03-17 01:35:04 +00:00
|
|
|
for _, lfs := range localeFS {
|
|
|
|
|
if lfs != nil {
|
2026-03-21 22:56:10 +00:00
|
|
|
nonempty = append(nonempty, lfs)
|
2026-03-17 01:35:04 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-21 22:56:10 +00:00
|
|
|
if len(nonempty) == 0 {
|
|
|
|
|
return
|
2026-02-22 20:42:00 +00:00
|
|
|
}
|
2026-03-21 22:56:10 +00:00
|
|
|
registeredCommandsMu.Lock()
|
|
|
|
|
registeredLocales = append(registeredLocales, nonempty...)
|
|
|
|
|
registeredCommandsMu.Unlock()
|
2026-02-22 20:42:00 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-02 03:53:21 +00:00
|
|
|
func localeSourcesFromFS(localeFS ...fs.FS) []LocaleSource {
|
|
|
|
|
sources := make([]LocaleSource, 0, len(localeFS))
|
|
|
|
|
for _, lfs := range localeFS {
|
|
|
|
|
if lfs != nil {
|
|
|
|
|
sources = append(sources, LocaleSource{FS: lfs, Dir: "."})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return sources
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func loadLocaleSources(sources ...LocaleSource) {
|
|
|
|
|
svc := i18n.Default()
|
|
|
|
|
if svc == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
for _, src := range sources {
|
|
|
|
|
if src.FS == nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if err := svc.AddLoader(i18n.NewFSLoader(src.FS, src.Dir)); err != nil {
|
|
|
|
|
LogDebug("failed to load locale source", "dir", src.Dir, "err", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 01:35:04 +00:00
|
|
|
// RegisteredLocales returns all locale filesystems registered by command packages.
|
|
|
|
|
func RegisteredLocales() []fs.FS {
|
|
|
|
|
registeredCommandsMu.Lock()
|
|
|
|
|
defer registeredCommandsMu.Unlock()
|
2026-04-02 06:45:31 +00:00
|
|
|
if len(registeredLocales) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
out := make([]fs.FS, len(registeredLocales))
|
|
|
|
|
copy(out, registeredLocales)
|
|
|
|
|
return out
|
2026-03-17 01:35:04 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-23 04:57:24 +00:00
|
|
|
// RegisteredCommands returns an iterator over the registered command functions.
|
|
|
|
|
func RegisteredCommands() iter.Seq[CommandRegistration] {
|
|
|
|
|
return func(yield func(CommandRegistration) bool) {
|
|
|
|
|
registeredCommandsMu.Lock()
|
2026-04-02 06:45:31 +00:00
|
|
|
snapshot := make([]CommandRegistration, len(registeredCommands))
|
|
|
|
|
copy(snapshot, registeredCommands)
|
|
|
|
|
registeredCommandsMu.Unlock()
|
|
|
|
|
|
|
|
|
|
for _, fn := range snapshot {
|
2026-02-23 04:57:24 +00:00
|
|
|
if !yield(fn) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 20:42:00 +00:00
|
|
|
// attachRegisteredCommands calls all registered command functions.
|
|
|
|
|
// Called by Init() after creating the root command.
|
|
|
|
|
func attachRegisteredCommands(root *cobra.Command) {
|
|
|
|
|
registeredCommandsMu.Lock()
|
2026-04-02 06:45:31 +00:00
|
|
|
snapshot := make([]CommandRegistration, len(registeredCommands))
|
|
|
|
|
copy(snapshot, registeredCommands)
|
|
|
|
|
commandsAttached = true
|
|
|
|
|
registeredCommandsMu.Unlock()
|
2026-02-22 20:42:00 +00:00
|
|
|
|
2026-04-02 06:45:31 +00:00
|
|
|
for _, fn := range snapshot {
|
2026-02-22 20:42:00 +00:00
|
|
|
fn(root)
|
|
|
|
|
}
|
|
|
|
|
}
|