English | 简体中文
cmd is a modern command-line library for Go. It keeps a small API surface, but adds the capabilities usually needed by production-grade CLIs:
- Recursive command trees
- Global flags and command-local flags
- Positional argument schema
- Multi-source binding with
env / config / CLI / default - Shell completion
- REPL and cursor-aware line completion APIs
- Machine-readable
spec - Markdown and man page generation
- Hooks, middleware, and observers
- Unified CLI errors and exit codes
This document is organized from quick start to platform-style integration.
- Installation
- Quick Start
- CLI + REPL Tutorial
- Architecture Overview
- Two Usage Modes
- Command Model
- Flag Model
- Parsing Rules
- Help and Built-in Commands
- Config, Environment Variables, and Precedence
- Positional Arguments
- Completion
- REPL and Line Execution
- Machine-readable Spec
- Docs Generation
- Lifecycle Hooks
- Middleware
- Observers and Telemetry
- Unified Errors and Exit Codes
- Custom Extension Metadata
- Common Patterns
- API Quick Reference
Import the package using your actual module path. In this repository, the package path is:
import "pkg.gostartkit.com/cmd"This minimal example includes:
- A global flag
--verbose - A
versioncommand - A
hellocommand - Command-local flags, positional arguments, and env binding
package main
import (
"context"
"fmt"
"pkg.gostartkit.com/cmd"
)
var (
verbose bool
name string
version = "v1.0.0"
)
func main() {
cmd.SetFlags(func(f *cmd.FlagSet) {
f.BoolVar(&verbose, "verbose", false, "enable verbose output", "v")
f.SetCategory("verbose", "Global")
})
cmd.AddCommands(
&cmd.Command{
Name: "version",
UsageLine: "app version",
Short: "print version",
Run: func(ctx context.Context, c *cmd.Command, args []string) error {
fmt.Println(version)
return nil
},
},
&cmd.Command{
Name: "hello",
UsageLine: "app hello [flags] <target>",
Short: "print greeting",
Examples: []string{
"app hello team --name sam",
"APP_NAME=sam app hello user",
},
Positionals: []cmd.PositionalArg{
{Name: "target", Usage: "greeting target", Required: true, Enum: []string{"team", "user"}},
},
SetFlags: func(f *cmd.FlagSet) {
f.StringVar(&name, "name", "", "name to greet", "n")
f.BindEnv("name", "APP_NAME")
f.MarkRequired("name")
f.SetEnum("name", "sam", "sara", "tom")
},
Run: func(ctx context.Context, c *cmd.Command, args []string) error {
if verbose {
fmt.Printf("[verbose] target=%s name=%s\n", args[0], name)
}
fmt.Printf("hello %s (%s)\n", name, args[0])
return nil
},
},
)
cmd.Execute()
}Example invocations:
app version
app --verbose hello team --name sam
APP_NAME=sara app hello user
app hello team -n tomIf your goal is "define one command tree and support both a regular CLI and a REPL with smart hints and completion", the workflow below is the recommended setup.
Using an explicit App instance keeps CLI, REPL, tests, and embedded runtimes on the same model:
package main
import (
"context"
"fmt"
"pkg.gostartkit.com/cmd"
)
func buildApp() *cmd.App {
app := cmd.NewApp("ops")
app.Short = "Operations console"
var verbose bool
app.ConfigureFlags(func(f *cmd.FlagSet) {
f.BoolVar(&verbose, "verbose", false, "verbose output", "v")
})
app.AddCommands(
&cmd.Command{
Name: "deploy",
UsageLine: "ops deploy [flags] <env>",
Short: "deploy service",
Positionals: []cmd.PositionalArg{
{
Name: "env",
Required: true,
Enum: []string{"dev", "staging", "prod"},
Completion: func(ctx cmd.CompletionContext) []string { return []string{"dev", "staging", "prod"} },
},
},
SetFlags: func(f *cmd.FlagSet) {
var region string
f.StringVar(®ion, "region", "", "target region", "r")
f.SetCompletion("region", func(ctx cmd.CompletionContext) []string {
return []string{"cn", "us", "eu"}
})
},
Run: func(ctx context.Context, c *cmd.Command, args []string) error {
if verbose {
fmt.Printf("[verbose] deploy to %s\n", args[0])
}
fmt.Printf("deploy %s\n", args[0])
return nil
},
},
)
return app
}The important part is:
- commands, flags, and positionals are defined once
- completion metadata such as
Enum,SetCompletion(...), andPositionalArg.Completionlives on the same command tree - CLI and REPL both reuse the same completion engine
If you want users to enter interactive mode through app repl, enable the built-in REPL command:
app.EnableREPL()You can also configure the prompt and welcome text:
app.ConfigureREPL(func(cfg *cmd.REPLConfig) {
cfg.Prompt = "ops> "
cfg.Welcome = "type .help or press Tab"
})The lowest-boilerplate main function looks like this:
func main() {
app := buildApp()
app.EnableREPL()
cmd.Main(app)
}With that setup:
ops deploy prodruns as a normal CLIops replenters REPL mode
If you want to choose the runtime explicitly in code, you still can:
err := app.RunWith(ctx, cmd.CLIRuntime{Args: []string{"deploy", "prod"}})
err = app.RunWith(ctx, cmd.REPLRuntime{In: os.Stdin, Out: os.Stdout})The regular CLI usage stays unchanged:
ops deploy prod
ops deploy prod --region us
ops --verbose deploy devEnter REPL mode with:
ops replIn a TTY terminal, the default REPL driver automatically supports:
Tab: complete commands, flags, positionals, and values- repeated
Tab: page through longer candidate lists - real-time hints while typing
- inline ghost text for the current best completion
Up / Down: command historyLeft / Right: cursor movementBackspace / Delete: character editing
For example:
ops> dep
hint: deploy - deploy service
ops> deploy --r
hint: --region - target region
ops> deploy p
hint: prod
Anything defined on the command tree is automatically reused by REPL, including:
- command names and aliases
- global flags and command-local flags
- flag usage text
- positional argument schemas
EnumSetCompletion(...)PositionalArg.Completion- command
Shortdescriptions
That is why the recommended pattern is to keep completion rules on Command, FlagSet, and PositionalArg, instead of building a separate REPL-only layer.
For most apps, this is the minimal recommended stack:
- Keep one shared
buildApp() - Put value completion rules on the command model with
EnumandSetCompletion(...) - Call
app.EnableREPL() - Start with
cmd.Main(app)
That usually gives you all of the following at once:
- regular CLI execution
- shell completion
- REPL mode
- REPL smart hints
- REPL command and argument completion
- REPL history and inline editing
The public API stays intentionally small, but internally the library follows a clear pipeline. Understanding that pipeline helps when you need to embed the package, override behavior, or generate tooling from the same command tree.
App.Rootis optional. If you do not set it, the library synthesizes a root command fromApp.Name,App.Short,App.Long, andApp.Commands.- If both
App.Root.SubCommandsandApp.Commandsare present, they are merged into one effective root. Names already present onApp.Root.SubCommandswin; duplicate names fromApp.Commandsare skipped. - Root-visible flags are also merged into one layer. The effective global flag set is built from the config flag (when enabled),
App.SetFlags, andApp.Root.SetFlags.
This is why CLI, help, completion, REPL, spec, and docs all see the same command tree instead of parallel models.
Execution is split into three focused stages:
Registry: indexes command paths, aliases, and visible built-ins from the effective root.Resolver: parses argv or REPL tokens into anInvocation, selects the target command, applies config/env/global flags, parses command-local flags, and validates positional arguments.Dispatcher: handles the resolved invocation kind (usage,help, built-in, or command), then runs hooks, middleware, observers, and error normalization.
The built-ins help, completion, spec, and docs are registered only when the command tree does not already define those names. repl is added only after app.EnableREPL(), and it can also be shadowed by a user command.
The runtime layer is explicit:
CLIRuntimealways runs CLI parsing and dispatch.REPLRuntimealways starts REPL with the provided streams, prompt overrides, driver, and history hooks.AutoRuntimeprefers CLI whenever args are present. With no args, it enters REPL only if REPL is enabled and stdin/stdout are TTYs; otherwise it falls back to CLI usage/root execution.
That behavior is what makes cmd.Main(app) safe for both interactive terminals and non-interactive environments such as tests, pipes, or process supervisors.
For performance, flag definitions are cached and reused as immutable templates. Each invocation still gets fresh runtime state:
- repeated CLI runs do not leak previous flag values
- REPL lines do not inherit mutable state from earlier commands
- metadata returned by
Lookup,Spec(), docs generation, and completion stays isolated from accidental mutation at runtime
In practice, one command model drives human-facing help, shell completion, REPL hints, and machine-facing exports without requiring separate synchronization code.
The library supports two styles.
This is the simplest approach for a single binary:
cmd.SetFlags(...)
cmd.AddCommands(...)
cmd.Execute()Global entry points:
SetFlagsAddCommandsSetUsageTemplateExecute
These helpers are only thin wrappers around the shared DefaultApp instance. The real execution model is still App + Root Command.
Use this when you need:
- Multiple CLI instances
- Tests
- Embedded execution
- Framework or platform wrappers
app := cmd.NewApp("myapp")
app.SetFlags = func(f *cmd.FlagSet) { ... }
app.Commands = []*cmd.Command{...}
err := app.Run(context.Background(), []string{"hello", "team"})
if err != nil {
// handle
}If you want to stay on instance-style APIs without mutating fields directly, the app also provides thin helpers:
app := cmd.NewApp("myapp")
app.ConfigureFlags(func(f *cmd.FlagSet) { ... })
app.SetRootCommand(&cmd.Command{ ... })
app.AddCommands(...)
err := app.Execute([]string{"hello", "team"})The core types are App and Command.
App represents the entire CLI application. Common fields:
Name: application nameShort: short descriptionLong: long descriptionRoot: optional root commandCommands: top-level commandsSetFlags: register global flagsBeforeRun / AfterRun / OnError: lifecycle hooksMiddlewares: middleware chainObservers: event observersExtensions: custom metadata
App can also own a real root command through App.Root.
App.SetFlagsremains the app-level global flag entry point.App.Commandsremains compatible and is treated as root subcommands.App.Root.SetFlagsis merged into the root/global flag set and is also visible to subcommands.- If the root command has
Run, invoking only the binary runs the root command. - If the root command has no
Runbut has subcommands, invoking only the binary shows usage.
app := cmd.NewApp("myapp")
var (
verbose bool
profile string
)
app.SetFlags = func(f *cmd.FlagSet) {
f.BoolVar(&verbose, "verbose", false, "enable verbose output", "v")
}
app.Root = &cmd.Command{
UsageLine: "myapp [flags] [target]",
Short: "root entrypoint",
Examples: []string{"myapp team", "myapp version"},
Positionals: []cmd.PositionalArg{
{Name: "target", Usage: "target name"},
},
SetFlags: func(f *cmd.FlagSet) {
f.StringVar(&profile, "profile", "", "profile name", "p")
},
Run: func(ctx context.Context, c *cmd.Command, args []string) error {
fmt.Printf("root args=%v verbose=%v profile=%s\n", args, verbose, profile)
return nil
},
SubCommands: []*cmd.Command{
{
Name: "version",
UsageLine: "myapp version",
Short: "print version",
Run: func(ctx context.Context, c *cmd.Command, args []string) error {
fmt.Println("v1.0.0")
return nil
},
},
},
}Command represents a node in the command tree. Common fields:
NameAliasesUsageLineShortLongExamplesPositionalsSetFlagsRunSubCommandsDeprecatedHiddenBeforeRun / AfterRun / OnErrorMiddlewaresObserversExtensions
cmdAdmin := &cmd.Command{
Name: "admin",
UsageLine: "app admin",
Short: "admin operations",
SubCommands: []*cmd.Command{
{
Name: "users",
UsageLine: "app admin users",
Short: "manage users",
Run: func(ctx context.Context, c *cmd.Command, args []string) error {
return nil
},
},
},
}Flags are managed through FlagSet. The library supports:
- Global flags
- Command-local flags
- Positional arguments
var (
force bool
count int
format string
)
f.BoolVar(&force, "force", false, "force operation", "f")
f.IntVar(&count, "count", 1, "retry count", "c")
f.StringVar(&format, "format", "text", "output format", "")Supported value types include:
BoolVarIntVarInt64VarUintVarUint64VarStringVarFloat64VarDurationVarTextVarFuncBoolFunc
There are also global versions of the same helpers operating on the default CommandLine.
After defining a flag, you can attach metadata:
f.StringVar(&format, "format", "text", "output format", "")
f.BindEnv("format", "APP_FORMAT")
f.BindConfig("format", "output.format")
f.SetEnum("format", "json", "yaml", "text")
f.MarkRequired("format")
f.MarkHidden("format")
f.MarkDeprecated("format", "use --output instead")
f.SetCategory("format", "Output")
f.SetExample("format", "json")These metadata fields affect:
- Parsing and validation
- Help output
- Completion
specdocs
The parsing behavior is one of the main features of the library.
app --verbose version
app --config app.json helloapp hello team -n sam
app hello team --name sam
app hello team extra --name samIn other words, command flags do not need to appear before all positional arguments.
app hello -- --name-not-a-flagapp help hello
app --verbose help hello
app hello --helpThe library suggests close matches for unknown commands and flags:
statu->status--verboes->--verbose
Built-ins:
helpcompletionspecdocs
If you define a user command with the same name, the user command wins and the built-in is skipped.
If you call app.EnableREPL(), the registry also exposes a built-in repl entry unless your own command tree already defines repl.
If you use the default global instance, you can replace the default usage template:
cmd.SetUsageTemplate(`
{{.Name}} - {{.Short}}
Usage:
{{.Name}} [flags] <command>
`)For performance, usage rendering uses a small built-in replacement engine instead of text/template. Custom templates support literal text plus simple fields such as {{.Name}}, {{.Short}}, {{.Long}}, and {{.UsageLine}}.
app := cmd.NewApp("app")
app.EnableConfigSupport()Once enabled, the library injects a built-in global flag:
app --config app.json helloThe default loader expects JSON:
{
"name": "from-config",
"output": {
"format": "json"
}
}f.StringVar(&name, "name", "", "target name", "n")
f.BindEnv("name", "APP_NAME", "LEGACY_NAME")f.StringVar(&format, "format", "", "output format", "")
f.BindConfig("format", "output.format")The precedence is fixed:
CLI flag > env > config > default
Examples:
app --config app.json hello
APP_NAME=sam app --config app.json hello
app --config app.json hello --name cliYou can also override:
ConfigLoaderConfigFlag
For example, to plug in your own config loader.
Positional arguments are described through Command.Positionals.
cmdDeploy := &cmd.Command{
Name: "deploy",
UsageLine: "app deploy <env> [service]",
Positionals: []cmd.PositionalArg{
{Name: "env", Usage: "target environment", Required: true, Enum: []string{"dev", "staging", "prod"}},
{Name: "service", Usage: "service name"},
},
Run: func(ctx context.Context, c *cmd.Command, args []string) error {
env := args[0]
service := ""
if len(args) > 1 {
service = args[1]
}
_ = env
_ = service
return nil
},
}Positionals: []cmd.PositionalArg{
{Name: "files", Variadic: true, Usage: "input files"},
}Positional arguments also support:
EnumCompletionExtensions
Positionals: []cmd.PositionalArg{
{
Name: "service",
Completion: func(ctx cmd.CompletionContext) []string {
return []string{"api", "worker", "web"}
},
},
}The library automatically handles:
- Missing required positional arguments
- Extra arguments for non-variadic commands
- Enum validation
app completion bash > /etc/bash_completion.d/app
app completion zsh > "${fpath[1]}/_app"
app completion fish > ~/.config/fish/completions/app.fish
app completion powershell > app.ps1- Command names
- Command aliases
- Global flags
- Command-local flags
- Flag enum values
- Flag dynamic completion
- Positional enum values
- Positional dynamic completion
- Built-in commands and their arguments
Example:
f.StringVar(&format, "format", "", "output format", "f")
f.SetEnum("format", "json", "yaml", "text")
f.StringVar(&name, "name", "", "target name", "n")
f.SetCompletion("name", func(ctx cmd.CompletionContext) []string {
return []string{"sam", "sara", "tom"}
})Built-ins also have completion:
app completion <shell>app spec jsonapp docs markdownapp docs man
For readline, TUI, editor, or agent integrations, use the line completion APIs. They reuse the same command tree and completion engine as shell completion.
plain := app.CompleteLine("deploy --e", len("deploy --e"))
detailed := app.CompleteLineDetailed("deploy --e", len("deploy --e"))CompleteLine returns plain suggestion strings and stays compatible with existing integrations. CompleteLineDetailed returns metadata for richer UIs:
type CompletionResult struct {
Value string
Description string
Kind string
}Current Kind values are:
commandflagvaluepositionalbuiltin
Shell completion remains plain text through __complete; it does not emit structured metadata.
The REPL APIs let embedded programs reuse the existing App, command tree, flags, positionals, and completion logic without rebuilding dispatch.
err := app.RunLine(ctx, `deploy "hello world" --env prod`)RunLine trims empty lines, splits shell-like input, then calls App.Run(ctx, args). The splitter supports whitespace, single quotes, double quotes, and backslash escaping.
For an interactive loop:
err := app.RunREPL(ctx, os.Stdin, os.Stdout)If you want to choose the runtime explicitly, use the shared runtime interface:
err := app.RunWith(ctx, cmd.CLIRuntime{Args: os.Args[1:]})
err = app.RunWith(ctx, cmd.REPLRuntime{In: os.Stdin, Out: os.Stdout})
err = app.RunDefault(ctx, os.Args[1:])For application entrypoints, you can also use the main-style helpers:
app.RunAuto(ctx, os.Args[1:])
app.MustRunDefault(ctx, os.Args[1:])
cmd.Main(app)If you want the same binary to expose REPL mode without adding your own command, enable the built-in REPL entry:
app.EnableREPL()Then users can enter REPL mode with:
app replOr configure the runtime directly:
repl := &cmd.REPL{
App: app,
Prompt: "app> ",
In: in,
Out: out,
Err: errOut,
}
err := repl.Run(ctx)If you need a dynamic prompt, provide PromptFunc. It is evaluated before each render:
app.ConfigureREPL(func(cfg *cmd.REPLConfig) {
cfg.Prompt = "app> "
cfg.PromptFunc = func(ctx context.Context, repl *cmd.REPL) string {
if repl.App == nil {
return ""
}
return repl.App.Name + "> "
}
})If PromptFunc returns an empty string, REPL falls back to Prompt, then to the default prompt "> ".
You can also load and persist history through hooks:
app.ConfigureREPL(func(cfg *cmd.REPLConfig) {
cfg.History = &cmd.REPLHistoryHooks{
Load: func(ctx context.Context) ([]string, error) {
return []string{"deploy prod", "status"}, nil
},
Append: func(ctx context.Context, line string) error {
fmt.Println("persist history:", line)
return nil
},
}
})Load runs when REPL starts. Append runs when a non-empty line is accepted for execution. In-memory history is still used for the current session, while hooks let you inject persistence.
Built-in REPL commands are:
exitquit.exit.quit.help
When stdin/stdout is a TTY, the default REPL driver also enables inline editing, history navigation, real-time context-aware hints, inline ghost text for the best completion, and Tab completion powered by the same command tree and value completion hooks used by CLI completion. Candidate lists are labeled by kind, so commands, flags, values, and positional arguments stay easy to distinguish, and repeated Tab presses page through longer candidate lists.
During line editing, terminal REPL keeps stdin in raw mode. After you press Enter to submit a command, the driver temporarily restores normal terminal mode before executing the command, then re-enters raw mode when REPL resumes. This allows command handlers to read from stdin, ask for confirmation, prompt for passwords, or perform their own terminal interaction without fighting the REPL line editor.
Command errors are printed and the REPL keeps running. context.Canceled or input EOF exits the loop.
app spec
app spec jsonspec is a versioned contract for the current command tree. It is suitable for:
- Static site generation
- IDE integration
- Console UIs
- Agent and AI pipelines
- Automated tests
Current output includes:
schema_versionsurfaceavailable_surfacesbuiltinscapabilitiesconfig- App and command hooks
- Middleware and observer markers
- Global flags
- The command tree
- Stable command IDs and handler IDs
- Positionals
- Flags
extensions
Spec() keeps the default/base contract. If you need a REPL/runtime-facing contract from the same command tree, export a specific surface:
cliSpec := app.Spec()
replSpec := app.SpecFor(cmd.SurfaceREPL)This is useful when CLI and REPL differ in usage lines or positional requirements, but still share the same base command definition.
idhandler_idpathkindenumrequiredrepeatabledeprecatedcompletion_keysupports_completionsource_orderextensions
app spec json > spec.jsonExample fields:
{
"schema_version": "v2",
"name": "app",
"surface": "repl",
"builtins": ["help", "completion", "spec", "docs"],
"capabilities": {
"completion_keys": true,
"docs_export": true,
"middleware": true,
"observers": true,
"surface_overrides": true,
"stable_ids": true
}
}docs is generated from Spec(), so it shares the default command contract used by spec, completion, and help output. If you need a REPL/runtime-specific schema, export it separately with SpecFor(surface).
app docs markdown
app docs manapp docs markdown ./docs
app docs man ./manpagesREADME.mdcommands/<command>.mdcommands/<command>/<subcommand>.md
<app>.1<app>-<command>.1<app>-<command>-<subcommand>.1
Markdown docs include frontmatter automatically, which is useful for:
- Hugo, Docusaurus, Astro, MkDocs, and similar site generators
- Search indexers
- Content pipelines
Frontmatter includes:
kindtitlesummarycommand_namecommand_pathextensions
Hooks are best for execution lifecycle behavior, not wrapping-style cross-cutting control.
app.BeforeRun = func(ctx cmd.HookContext) error {
return nil
}
app.AfterRun = func(ctx cmd.HookContext) {
if ctx.Err != nil {
log.Printf("command failed: %v", ctx.Err)
}
}
app.OnError = func(ctx cmd.HookContext) {
var cliErr *cmd.CLIError
if errors.As(ctx.Err, &cliErr) {
log.Printf("kind=%s exit=%d command=%s", cliErr.Kind, cliErr.ExitCode, cliErr.Command)
}
}cmdDeploy := &cmd.Command{
Name: "deploy",
BeforeRun: func(ctx cmd.HookContext) error {
return nil
},
AfterRun: func(ctx cmd.HookContext) {},
OnError: func(ctx cmd.HookContext) {},
Run: runDeploy,
}On success:
- App
BeforeRun - Command
BeforeRun Run- Command
AfterRun - App
AfterRun
On failure:
- App
BeforeRun - Command
BeforeRun Runor a hook returns an error- Command
OnError - App
OnError - Command
AfterRun - App
AfterRun
Middleware is for cross-cutting behavior such as:
- Authentication
- Tracing
- Rate limiting
- Auditing
- Unified logging
app.Use(func(ctx cmd.MiddlewareContext, next cmd.NextFunc) error {
start := time.Now()
err := next(ctx.Context)
log.Printf("command=%s duration=%s err=%v", ctx.Command.Name, time.Since(start), err)
return err
})cmdDeploy := &cmd.Command{
Name: "deploy",
Middlewares: []cmd.Middleware{
func(ctx cmd.MiddlewareContext, next cmd.NextFunc) error {
if len(ctx.Args) == 0 {
return errors.New("missing target")
}
return next(ctx.Context)
},
},
Run: runDeploy,
}Execution order is:
app middleware -> command middleware -> Command.Run
Observers provide a stable event stream for:
- Metrics
- Tracing adapters
- Event logs
- Analytics
app.AddObserver(cmd.ObserverFunc(func(event cmd.Event) {
log.Printf(
"type=%s command=%s exit=%d duration=%s",
event.Type,
event.Command.Name,
event.ExitCode,
event.Duration,
)
}))Commands can also register their own Observers.
command_startedcommand_finishedcommand_failed
Event includes:
TypeAppCommandArgsErrStartTimeEndTimeDurationExitCode
Library-generated normalized errors are returned as *CLIError.
Current Kind values:
invalid_argumentsnot_foundcanceledinternalruntime
- Invalid arguments:
2 - Unknown command:
2 context.Canceled:130context.DeadlineExceeded:124- Runtime failure:
1
err := app.Run(ctx, os.Args[1:])
if err != nil {
var cliErr *cmd.CLIError
if errors.As(err, &cliErr) {
fmt.Println(cliErr.Kind, cliErr.ExitCode, cliErr.Command)
}
}Execute() automatically exits using ExitStatus() on the default instance.
If you need to attach custom metadata to the command tree, for example:
- Site categorization
- Console UI hints
- Internal ownership
- Feature flags
- OpenAPI or agent-specific extension fields
use extensions.
app.SetExtension("x-site-section", "cli")cmdDeploy.SetExtension("x-owner", "platform")cmdDeploy.Positionals[0].SetExtension("x-label", "Environment")cmdDeploy.SetFlags = func(f *cmd.FlagSet) {
f.StringVar(&format, "format", "", "output format", "")
_ = f.SetExtension("format", "x-ui-control", "select")
}If one command definition needs different exported shapes for CLI and REPL/runtime schema, keep one base command and attach per-surface overrides:
requiredFalse := false
cmdCreateUser := &cmd.Command{
Name: "user",
UsageLine: "app create user <name> [flags]",
Positionals: []cmd.PositionalArg{{
Name: "name",
Usage: "user name",
Required: true,
Kind: "user",
CompletionKey: "user",
Surfaces: map[cmd.Surface]cmd.PositionalSurface{
cmd.SurfaceREPL: {Required: &requiredFalse},
},
}},
Surfaces: map[cmd.Surface]cmd.CommandSurface{
cmd.SurfaceREPL: {UsageLine: "app create user [name] [flags]"},
},
}These metadata fields are exported into:
spec- Markdown frontmatter
Extension maps are cloned when metadata is copied into specs, docs, and runtime flag views. Map and slice-shaped values are cloned recursively, but opaque pointer or custom object payloads are shared by reference. If you need full isolation, store immutable values or clone the payload before attaching it.
app := cmd.NewApp("app")
app.EnableConfigSupport()
app.SetFlags = func(f *cmd.FlagSet) {
f.BoolVar(&verbose, "verbose", false, "verbose output", "v")
}
app.Commands = []*cmd.Command{
{
Name: "sync",
SetFlags: func(f *cmd.FlagSet) {
f.StringVar(&endpoint, "endpoint", "", "api endpoint", "")
f.BindEnv("endpoint", "APP_ENDPOINT")
f.BindConfig("endpoint", "api.endpoint")
f.MarkRequired("endpoint")
},
Run: runSync,
},
}f.StringVar(&env, "env", "", "target environment", "")
f.SetEnum("env", "dev", "staging", "prod")app.AddObserver(cmd.ObserverFunc(func(event cmd.Event) {
switch event.Type {
case cmd.EventCommandFinished:
metrics.RecordSuccess(event.Command.Name, event.Duration)
case cmd.EventCommandFailed:
metrics.RecordFailure(event.Command.Name, event.Duration)
}
}))app spec json > site/spec.json
app docs markdown ./site/docsNewApp(name string) *App(*App).Run(ctx, args)(*App).RunWith(ctx, runtime)(*App).RunAuto(ctx, args)(*App).RunDefault(ctx, args)(*App).RunLine(ctx, line)(*App).RunREPL(ctx, in, out)(*App).Main(ctx, runtime)(*App).MainAuto(ctx, args)(*App).MainDefault(ctx, args)(*App).MustRun(ctx, runtime)(*App).MustRunAuto(ctx, args)(*App).MustRunDefault(ctx, args)(*App).DefaultRuntime(args)(*App).CompleteLine(line, cursor)(*App).CompleteLineDetailed(line, cursor)(*App).EnableREPL()(*App).ConfigureREPL(fn)(*App).EnableConfigSupport()(*App).Use(...)(*App).AddObserver(...)(*App).SetExtension(key, value)(*App).Spec()(*App).SpecFor(surface)(*App).AvailableSurfaces()
SetFlags(...)AddCommands(...)SetUsageTemplate(...)Execute()Main(app)MainWithContext(ctx, app)
BindEnvBindConfigSetIDSetKindSetEnumSetCompletionKeySetCompletionMarkRepeatableMarkRequiredMarkHiddenMarkDeprecatedSetCategorySetExampleSetExtensionSetSurface
SuggestCommandsUnknownCommandErrorUnknownSubcommandErrorUsageError
AppCommandREPLREPLConfigRuntimeDefaultRuntimeCLIRuntimeREPLRuntimeAutoRuntimeSurfaceCommandSurfacePositionalSurfaceFlagSurfaceFlagSetFlagPositionalArgCompletionContextCompletionResultLineCompleterDetailedLineCompleterREPLHookContextMiddlewareContextEventCLIErrorAppSpec
If you only need a small CLI, these are enough:
SetFlagsAddCommandsExecute
If you want a CLI that can grow into a platform, organize around this model:
Command / Flag / Positionalas the single command modelenv / config / CLIas the single source-of-truth for configuration resolutionhooks / middleware / observeras the runtime extension layerspec / docsas the external contractsurface overrides + rich spec metadataas the bridge to REPL, parser, schema, and agent consumers
That is the direction this library is best suited for today.