5.3 KiB
| title | description |
|---|---|
| Getting Started | How to use cli.Main(), WithCommands(), and build CLI binaries with the framework. |
Getting Started
The core Binary
The core binary is built from main.go at the repo root. It composes commands from both local packages and ecosystem modules:
package main
import (
"forge.lthn.ai/core/cli/cmd/config"
"forge.lthn.ai/core/cli/cmd/gocmd"
"forge.lthn.ai/core/cli/pkg/cli"
// Ecosystem packages self-register via init()
_ "forge.lthn.ai/core/go-devops/cmd/dev"
_ "forge.lthn.ai/core/go-build/cmd/build"
)
func main() {
cli.Main(
cli.WithCommands("config", config.AddConfigCommands),
cli.WithCommands("go", gocmd.AddGoCommands),
)
}
cli.Main()
Main() is the primary entry point. It:
- Registers core services (i18n, log, crypt, workspace)
- Appends your command services
- Creates the cobra root command and signal handler
- Starts all services via the Core DI framework
- Adds the
completioncommand - Executes the matched command
- Shuts down all services in reverse order
- Exits with the appropriate code
cli.Main(
cli.WithCommands("score", score.AddScoreCommands),
cli.WithCommands("gen", gen.AddGenCommands),
)
If a command returns an *ExitError, the process exits with that code. All other errors exit with code 1.
cli.WithCommands()
This is the preferred way to register commands. It wraps your registration function in a Core service that participates in the lifecycle:
func WithCommands(name string, register func(root *Command)) core.Option
During startup, the Core framework calls your function with the root cobra command. Your function adds subcommands to it:
func AddScoreCommands(root *cli.Command) {
scoreCmd := cli.NewGroup("score", "Scoring commands", "")
grammarCmd := cli.NewCommand("grammar", "Grammar analysis", "", runGrammar)
cli.StringFlag(grammarCmd, &inputPath, "input", "i", "", "Input file")
scoreCmd.AddCommand(grammarCmd)
root.AddCommand(scoreCmd)
}
Startup order:
- Core services start (i18n, log, crypt, workspace, signal)
- Command services start (your
WithCommandsfunctions run) Execute()runs the matched command
Building a Variant Binary
To create a standalone binary (not the core binary), set the app name and compose your commands:
// cmd/lem/main.go
package main
import (
"forge.lthn.ai/core/cli/pkg/cli"
"forge.lthn.ai/lthn/lem/cmd/lemcmd"
)
func main() {
cli.WithAppName("lem")
cli.Main(lemcmd.Commands()...)
}
Where Commands() returns a slice of framework options:
package lemcmd
import (
"forge.lthn.ai/core/go/pkg/core"
"forge.lthn.ai/core/cli/pkg/cli"
)
func Commands() []core.Option {
return []core.Option{
cli.WithCommands("score", addScoreCommands),
cli.WithCommands("gen", addGenCommands),
cli.WithCommands("data", addDataCommands),
}
}
cli.RegisterCommands() (Legacy)
For ecosystem packages that need to self-register via init():
func init() {
cli.RegisterCommands(func(root *cobra.Command) {
root.AddCommand(myCmd)
})
}
The core binary imports these packages with blank imports (_ "forge.lthn.ai/core/go-build/cmd/build"), triggering their init() functions.
Prefer WithCommands -- it is explicit and does not rely on import side effects.
Manual Initialisation (Advanced)
If you need more control over the lifecycle:
cli.Init(cli.Options{
AppName: "myapp",
Version: "1.0.0",
Services: []core.Option{...},
OnReload: func() error { return reloadConfig() },
})
defer cli.Shutdown()
// Add commands manually
cli.RootCmd().AddCommand(myCmd)
if err := cli.Execute(); err != nil {
os.Exit(1)
}
Version Info
Version fields are set via ldflags at build time:
cli.AppVersion // "1.2.0"
cli.BuildCommit // "df94c24"
cli.BuildDate // "2026-02-06"
cli.BuildPreRelease // "dev.8"
cli.SemVer() // "1.2.0-dev.8+df94c24.20260206"
Build command:
go build -ldflags="-X forge.lthn.ai/core/cli/pkg/cli.AppVersion=1.2.0 \
-X forge.lthn.ai/core/cli/pkg/cli.BuildCommit=$(git rev-parse --short HEAD) \
-X forge.lthn.ai/core/cli/pkg/cli.BuildDate=$(date +%Y-%m-%d)"
Accessing Core Services
Inside a command handler, you can access the Core DI container and retrieve services:
func runMyCommand(cmd *cli.Command, args []string) error {
ctx := cli.Context() // Root context (cancelled on signal)
core := cli.Core() // Framework Core instance
root := cli.RootCmd() // Root cobra command
// Type-safe service retrieval
ws, err := framework.ServiceFor[*workspace.Service](core)
if err != nil {
return cli.WrapVerb(err, "get", "workspace service")
}
return nil
}
Signal Handling
Signal handling is automatic. SIGINT and SIGTERM cancel cli.Context(). Use this context in your commands for graceful cancellation:
func runServer(cmd *cli.Command, args []string) error {
ctx := cli.Context()
// ctx is cancelled when the user presses Ctrl+C
<-ctx.Done()
return nil
}
Optional SIGHUP handling for configuration reload:
cli.Init(cli.Options{
AppName: "daemon",
OnReload: func() error {
return reloadConfig()
},
})