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>
160 lines
4.2 KiB
Go
160 lines
4.2 KiB
Go
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"runtime/debug"
|
|
|
|
"forge.lthn.ai/core/go/pkg/framework"
|
|
"forge.lthn.ai/core/go/pkg/log"
|
|
"forge.lthn.ai/core/go/pkg/workspace"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
const (
|
|
// AppName is the CLI application name.
|
|
AppName = "core"
|
|
)
|
|
|
|
// Build-time variables set via ldflags (SemVer 2.0.0):
|
|
//
|
|
// go build -ldflags="-X forge.lthn.ai/core/go/pkg/cli.AppVersion=1.2.0 \
|
|
// -X forge.lthn.ai/core/go/pkg/cli.BuildCommit=df94c24 \
|
|
// -X forge.lthn.ai/core/go/pkg/cli.BuildDate=2026-02-06 \
|
|
// -X forge.lthn.ai/core/go/pkg/cli.BuildPreRelease=dev.8"
|
|
var (
|
|
AppVersion = "0.0.0"
|
|
BuildCommit = "unknown"
|
|
BuildDate = "unknown"
|
|
BuildPreRelease = ""
|
|
)
|
|
|
|
// SemVer returns the full SemVer 2.0.0 version string.
|
|
// - Release: 1.2.0
|
|
// - Pre-release: 1.2.0-dev.8
|
|
// - Full: 1.2.0-dev.8+df94c24.20260206
|
|
func SemVer() string {
|
|
v := AppVersion
|
|
if BuildPreRelease != "" {
|
|
v += "-" + BuildPreRelease
|
|
}
|
|
if BuildCommit != "unknown" {
|
|
v += "+" + BuildCommit
|
|
if BuildDate != "unknown" {
|
|
v += "." + BuildDate
|
|
}
|
|
}
|
|
return v
|
|
}
|
|
|
|
// 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.
|
|
func Main(commands ...framework.Option) {
|
|
// Recovery from panics
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
log.Error("recovered from panic", "error", r, "stack", string(debug.Stack()))
|
|
Shutdown()
|
|
Fatal(fmt.Errorf("panic: %v", r))
|
|
}
|
|
}()
|
|
|
|
// 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: services,
|
|
}); err != nil {
|
|
Error(err.Error())
|
|
os.Exit(1)
|
|
}
|
|
defer Shutdown()
|
|
|
|
// Add completion command to the CLI's root
|
|
RootCmd().AddCommand(completionCmd)
|
|
|
|
if err := Execute(); err != nil {
|
|
code := 1
|
|
var exitErr *ExitError
|
|
if As(err, &exitErr) {
|
|
code = exitErr.Code
|
|
}
|
|
Error(err.Error())
|
|
os.Exit(code)
|
|
}
|
|
}
|
|
|
|
// completionCmd generates shell completion scripts.
|
|
var completionCmd = &cobra.Command{
|
|
Use: "completion [bash|zsh|fish|powershell]",
|
|
Short: "Generate shell completion script",
|
|
Long: `Generate shell completion script for the specified shell.
|
|
|
|
To load completions:
|
|
|
|
Bash:
|
|
$ source <(core completion bash)
|
|
|
|
# To load completions for each session, execute once:
|
|
# Linux:
|
|
$ core completion bash > /etc/bash_completion.d/core
|
|
# macOS:
|
|
$ core completion bash > $(brew --prefix)/etc/bash_completion.d/core
|
|
|
|
Zsh:
|
|
# If shell completion is not already enabled in your environment,
|
|
# you will need to enable it. You can execute the following once:
|
|
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
|
|
|
|
# To load completions for each session, execute once:
|
|
$ core completion zsh > "${fpath[1]}/_core"
|
|
|
|
# You will need to start a new shell for this setup to take effect.
|
|
|
|
Fish:
|
|
$ core completion fish | source
|
|
|
|
# To load completions for each session, execute once:
|
|
$ core completion fish > ~/.config/fish/completions/core.fish
|
|
|
|
PowerShell:
|
|
PS> core completion powershell | Out-String | Invoke-Expression
|
|
|
|
# To load completions for every new session, run:
|
|
PS> core completion powershell > core.ps1
|
|
# and source this file from your PowerShell profile.
|
|
`,
|
|
DisableFlagsInUseLine: true,
|
|
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
|
|
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
switch args[0] {
|
|
case "bash":
|
|
_ = cmd.Root().GenBashCompletion(os.Stdout)
|
|
case "zsh":
|
|
_ = cmd.Root().GenZshCompletion(os.Stdout)
|
|
case "fish":
|
|
_ = cmd.Root().GenFishCompletion(os.Stdout, true)
|
|
case "powershell":
|
|
_ = cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
|
|
}
|
|
},
|
|
}
|