docs: add human-friendly documentation
Some checks failed
Deploy / build (push) Failing after 4s
Security Scan / security (push) Failing after 13s

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-03-11 13:02:39 +00:00
parent 76cd3a5306
commit 7be4e243f2
9 changed files with 1303 additions and 0 deletions

View file

@ -1,3 +1,8 @@
---
title: Core CLI
description: Unified CLI for building, releasing, and deploying Go, Wails, PHP, and container workloads.
---
# Core CLI # Core CLI
Core is a unified CLI for the host-uk ecosystem - build, release, and deploy Go, Wails, PHP, and container workloads. Core is a unified CLI for the host-uk ecosystem - build, release, and deploy Go, Wails, PHP, and container workloads.

168
docs/pkg/cli/commands.md Normal file
View file

@ -0,0 +1,168 @@
---
title: Command Builders
description: Creating commands, flag helpers, args validation, and the config struct pattern.
---
# Command Builders
The framework provides three command constructors and a full set of flag helpers. All wrap cobra but remove the need to import it directly.
## Command Types
### `NewCommand` -- Standard command (returns error)
The most common form. The handler returns an error which `Main()` handles:
```go
cmd := cli.NewCommand("build", "Build the project", "", func(cmd *cli.Command, args []string) error {
if err := compile(); err != nil {
return cli.WrapVerb(err, "compile", "project")
}
cli.Success("Build complete")
return nil
})
```
The third parameter is the long description (shown in `--help`). Pass `""` to omit it.
### `NewGroup` -- Parent command (subcommands only)
Creates a command with no handler, used to group subcommands:
```go
scoreCmd := cli.NewGroup("score", "Scoring commands", "")
scoreCmd.AddCommand(grammarCmd, attentionCmd, tierCmd)
root.AddCommand(scoreCmd)
```
### `NewRun` -- Simple command (no error return)
For commands that cannot fail:
```go
cmd := cli.NewRun("version", "Show version", "", func(cmd *cli.Command, args []string) {
cli.Println("v1.0.0")
})
```
## Re-exports
The framework re-exports cobra types so you never need to import cobra directly:
```go
cli.Command // = cobra.Command
cli.PositionalArgs // = cobra.PositionalArgs
```
## Flag Helpers
All flag helpers follow the same signature: `(cmd, ptr, name, short, default, usage)`. Pass `""` for the short name to omit the short flag.
```go
var cfg struct {
Model string
Verbose bool
Count int
Score float64
Seed int64
Timeout time.Duration
Tags []string
}
cli.StringFlag(cmd, &cfg.Model, "model", "m", "", "Model path")
cli.BoolFlag(cmd, &cfg.Verbose, "verbose", "v", false, "Verbose output")
cli.IntFlag(cmd, &cfg.Count, "count", "n", 10, "Item count")
cli.Float64Flag(cmd, &cfg.Score, "score", "s", 0.0, "Min score")
cli.Int64Flag(cmd, &cfg.Seed, "seed", "", 0, "Random seed")
cli.DurationFlag(cmd, &cfg.Timeout, "timeout", "t", 30*time.Second, "Timeout")
cli.StringSliceFlag(cmd, &cfg.Tags, "tag", "", nil, "Tags")
```
### Persistent Flags
Persistent flags are inherited by all subcommands:
```go
cli.PersistentStringFlag(parentCmd, &dbPath, "db", "d", "", "Database path")
cli.PersistentBoolFlag(parentCmd, &debug, "debug", "", false, "Debug mode")
```
## Args Validation
Constrain the number of positional arguments:
```go
cmd := cli.NewCommand("deploy", "Deploy to env", "", deployFn)
cli.WithArgs(cmd, cli.ExactArgs(1)) // Exactly 1 arg
cli.WithArgs(cmd, cli.MinimumNArgs(1)) // At least 1
cli.WithArgs(cmd, cli.MaximumNArgs(3)) // At most 3
cli.WithArgs(cmd, cli.RangeArgs(1, 3)) // Between 1 and 3
cli.WithArgs(cmd, cli.NoArgs()) // No args allowed
cli.WithArgs(cmd, cli.ArbitraryArgs()) // Any number of args
```
## Command Configuration
Add examples to help text:
```go
cli.WithExample(cmd, ` core build --targets linux/amd64
core build --ci`)
```
## Pattern: Config Struct + Flags
The idiomatic pattern for commands with many flags is to define a config struct, bind flags to its fields, then pass the struct to the business logic:
```go
type DistillOpts struct {
Model string
Probes string
Runs int
DryRun bool
}
func addDistillCommand(parent *cli.Command) {
var cfg DistillOpts
cmd := cli.NewCommand("distill", "Run distillation", "", func(cmd *cli.Command, args []string) error {
return RunDistill(cfg)
})
cli.StringFlag(cmd, &cfg.Model, "model", "m", "", "Model config path")
cli.StringFlag(cmd, &cfg.Probes, "probes", "p", "", "Probe set name")
cli.IntFlag(cmd, &cfg.Runs, "runs", "r", 3, "Runs per probe")
cli.BoolFlag(cmd, &cfg.DryRun, "dry-run", "", false, "Preview without executing")
parent.AddCommand(cmd)
}
```
## Registration Function Pattern
Commands are organised in packages under `cmd/`. Each package exports an `Add*Commands` function:
```go
// cmd/score/commands.go
package score
import "forge.lthn.ai/core/cli/pkg/cli"
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)
}
```
Then in `main.go`:
```go
cli.Main(
cli.WithCommands("score", score.AddScoreCommands),
)
```

131
docs/pkg/cli/daemon.md Normal file
View file

@ -0,0 +1,131 @@
---
title: Daemon Mode
description: Daemon process management, PID files, health checks, and execution modes.
---
# Daemon Mode
The framework provides both low-level daemon primitives and a high-level command group that adds `start`, `stop`, `status`, and `run` subcommands to your CLI.
## Execution Modes
The framework auto-detects the execution environment:
```go
mode := cli.DetectMode()
```
| Mode | Condition | Behaviour |
|------|-----------|-----------|
| `ModeInteractive` | TTY attached | Colours enabled, spinners active |
| `ModePipe` | stdout piped | Colours disabled, plain output |
| `ModeDaemon` | `CORE_DAEMON=1` env var | Log-only output |
Helper functions:
```go
cli.IsTTY() // stdout is a terminal?
cli.IsStdinTTY() // stdin is a terminal?
cli.IsStderrTTY() // stderr is a terminal?
```
## Adding Daemon Commands
`AddDaemonCommand` registers a command group with four subcommands:
```go
func AddMyCommands(root *cli.Command) {
cli.AddDaemonCommand(root, cli.DaemonCommandConfig{
Name: "daemon", // Command group name (default: "daemon")
Description: "Manage the worker", // Short description
PIDFile: "/var/run/myapp.pid",
HealthAddr: ":9090",
RunForeground: func(ctx context.Context, daemon *process.Daemon) error {
// Your long-running service logic here.
// ctx is cancelled on SIGINT/SIGTERM.
return runWorker(ctx)
},
})
}
```
This creates:
- `myapp daemon start` -- Re-executes the binary as a background process with `CORE_DAEMON=1`
- `myapp daemon stop` -- Sends SIGTERM to the daemon, waits for shutdown (30s timeout, then SIGKILL)
- `myapp daemon status` -- Reports whether the daemon is running and queries health endpoints
- `myapp daemon run` -- Runs in the foreground (for development or process managers like systemd)
### Custom Persistent Flags
Add flags that apply to all daemon subcommands:
```go
cli.AddDaemonCommand(root, cli.DaemonCommandConfig{
// ...
Flags: func(cmd *cli.Command) {
cli.PersistentStringFlag(cmd, &configPath, "config", "c", "", "Config file")
},
ExtraStartArgs: func() []string {
return []string{"--config", configPath}
},
})
```
`ExtraStartArgs` passes additional flags when re-executing the binary as a daemon.
### Health Endpoints
When `HealthAddr` is set, the daemon serves:
- `GET /health` -- Liveness check (200 if server is up, 503 if health checks fail)
- `GET /ready` -- Readiness check (200 if `daemon.SetReady(true)` has been called)
The `start` command waits up to 5 seconds for the health endpoint to become available before reporting success.
## Simple Daemon (Manual)
For cases where you do not need the full command group:
```go
func runDaemon(cmd *cli.Command, args []string) error {
ctx := cli.Context() // Cancelled on SIGINT/SIGTERM
// ... start your work ...
<-ctx.Done()
return nil
}
```
## Shutdown with Timeout
The daemon stop logic sends SIGTERM and waits up to 30 seconds. If the process has not exited by then, it sends SIGKILL and removes the PID file.
## Signal Handling
Signal handling is built into the CLI runtime:
- **SIGINT/SIGTERM** cancel `cli.Context()`
- **SIGHUP** calls the `OnReload` handler if configured:
```go
cli.Init(cli.Options{
AppName: "daemon",
OnReload: func() error {
return reloadConfig()
},
})
```
No manual signal handling is needed in commands. Use `cli.Context()` for cancellation-aware operations.
## DaemonCommandConfig Reference
| Field | Type | Description |
|-------|------|-------------|
| `Name` | `string` | Command group name (default: `"daemon"`) |
| `Description` | `string` | Short description for help text |
| `PIDFile` | `string` | PID file path (default flag value) |
| `HealthAddr` | `string` | Health check listen address (default flag value) |
| `RunForeground` | `func(ctx, daemon) error` | Service logic for foreground/daemon mode |
| `Flags` | `func(cmd)` | Registers custom persistent flags |
| `ExtraStartArgs` | `func() []string` | Additional args for background re-exec |

110
docs/pkg/cli/errors.md Normal file
View file

@ -0,0 +1,110 @@
---
title: Error Handling
description: Error creation, wrapping with i18n grammar, exit codes, and inspection helpers.
---
# Error Handling
The framework provides error creation and wrapping functions that integrate with i18n grammar composition, plus re-exports of the standard `errors` package for convenience.
## Error Creation
```go
// Simple error (replaces fmt.Errorf)
return cli.Err("invalid model: %s", name)
// Wrap with context (nil-safe -- returns nil if err is nil)
return cli.Wrap(err, "load config") // "load config: <original>"
// Wrap with i18n grammar
return cli.WrapVerb(err, "load", "config") // "Failed to load config: <original>"
return cli.WrapAction(err, "connect") // "Failed to connect: <original>"
```
`WrapVerb` and `WrapAction` use the i18n `ActionFailed` function, which produces grammatically correct error messages across languages.
All wrapping functions are nil-safe: they return `nil` if the input error is `nil`.
## Error Inspection
Re-exports of the `errors` package for convenience, so you do not need a separate import:
```go
if cli.Is(err, os.ErrNotExist) { ... }
var exitErr *cli.ExitError
if cli.As(err, &exitErr) {
os.Exit(exitErr.Code)
}
combined := cli.Join(err1, err2, err3)
```
## Exit Codes
Return a specific exit code from a command:
```go
return cli.Exit(2, fmt.Errorf("validation failed"))
```
The `ExitError` type wraps an error with an exit code. `Main()` checks for `*ExitError` and uses its code when exiting the process. All other errors exit with code 1.
```go
type ExitError struct {
Code int
Err error
}
```
`ExitError` implements `error` and `Unwrap()`, so it works with `errors.Is` and `errors.As`.
## The Pattern: Commands Return Errors
Commands should return errors rather than calling `os.Exit` directly. `Main()` handles the exit:
```go
// Correct: return error, let Main() handle exit
func runBuild(cmd *cli.Command, args []string) error {
if err := compile(); err != nil {
return cli.WrapVerb(err, "compile", "project")
}
cli.Success("Build complete")
return nil
}
// Wrong: calling os.Exit from command code
func runBuild(cmd *cli.Command, args []string) error {
if err := compile(); err != nil {
cli.Fatal(err) // Do not do this
}
return nil
}
```
## Fatal Functions (Deprecated)
These exist for legacy code but should not be used in new commands:
```go
cli.Fatal(err) // prints + os.Exit(1)
cli.Fatalf("bad: %v", err) // prints + os.Exit(1)
cli.FatalWrap(err, "load config") // prints + os.Exit(1)
cli.FatalWrapVerb(err, "load", "x") // prints + os.Exit(1)
```
All `Fatal*` functions log the error, print it to stderr with the error style, and call `os.Exit(1)`. They bypass the normal shutdown sequence.
## Error Output Functions
For displaying errors without returning them (see [Output](output.md) for full details):
```go
cli.Error("message") // ✗ message (stderr)
cli.Errorf("port %d in use", port) // ✗ port 8080 in use (stderr)
cli.ErrorWrap(err, "context") // ✗ context: <error> (stderr)
cli.ErrorWrapVerb(err, "load", "config") // ✗ Failed to load config: <error> (stderr)
cli.ErrorWrapAction(err, "connect") // ✗ Failed to connect: <error> (stderr)
```
These print styled error messages but do not terminate the process or return an error value. Use them when you want to report a problem and continue.

View file

@ -0,0 +1,219 @@
---
title: Getting Started
description: 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:
```go
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:
1. Registers core services (i18n, log, crypt, workspace)
2. Appends your command services
3. Creates the cobra root command and signal handler
4. Starts all services via the Core DI framework
5. Adds the `completion` command
6. Executes the matched command
7. Shuts down all services in reverse order
8. Exits with the appropriate code
```go
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:
```go
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:
```go
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:**
1. Core services start (i18n, log, crypt, workspace, signal)
2. Command services start (your `WithCommands` functions run)
3. `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:
```go
// 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:
```go
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()`:
```go
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:
```go
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:
```go
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:
```bash
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:
```go
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:
```go
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:
```go
cli.Init(cli.Options{
AppName: "daemon",
OnReload: func() error {
return reloadConfig()
},
})
```

103
docs/pkg/cli/index.md Normal file
View file

@ -0,0 +1,103 @@
---
title: CLI Framework Overview
description: Go CLI framework built on cobra, with styled output, streaming, daemon mode, and TUI components.
---
# CLI Framework (`pkg/cli`)
`pkg/cli` is the CLI framework that powers the `core` binary and all derivative binaries in the ecosystem. It wraps cobra with the Core DI framework, adding styled output, streaming, interactive prompts, daemon management, and TUI components.
**Import:** `forge.lthn.ai/core/cli/pkg/cli`
## Quick Start
```go
package main
import "forge.lthn.ai/core/cli/pkg/cli"
func main() {
cli.WithAppName("myapp")
cli.Main(
cli.WithCommands("greet", addGreetCommands),
)
}
func addGreetCommands(root *cli.Command) {
cmd := cli.NewCommand("greet", "Say hello", "", func(cmd *cli.Command, args []string) error {
cli.Success("Hello, world!")
return nil
})
root.AddCommand(cmd)
}
```
## Architecture
The framework has three layers:
1. **Runtime** (`runtime.go`, `app.go`) -- Initialises the Core DI container, cobra root command, and signal handling. Provides global accessors (`cli.Core()`, `cli.Context()`, `cli.RootCmd()`).
2. **Command registration** (`commands.go`, `command.go`) -- Two mechanisms for adding commands: `WithCommands` (lifecycle-aware, preferred) and `RegisterCommands` (init-time, for ecosystem packages).
3. **Output & interaction** (`output.go`, `stream.go`, `prompt.go`, `utils.go`) -- Styled output functions, streaming text renderer, interactive prompts, tables, trees, and task trackers.
## Key Types
| Type | Description |
|------|-------------|
| `Command` | Re-export of `cobra.Command` |
| `Stream` | Token-by-token text renderer with optional word-wrap |
| `Table` | Aligned tabular output with optional box-drawing borders |
| `TreeNode` | Tree structure with box-drawing connectors |
| `TaskTracker` | Concurrent task display with live spinners |
| `CheckBuilder` | Fluent API for pass/fail/skip result lines |
| `AnsiStyle` | Terminal text styling (bold, dim, colour) |
## Built-in Services
When you call `cli.Main()`, these services are registered automatically:
| Service | Name | Purpose |
|---------|------|---------|
| I18nService | `i18n` | Internationalisation and grammar composition |
| LogService | `log` | Structured logging with CLI-styled output |
| openpgp | `crypt` | OpenPGP encryption |
| workspace | `workspace` | Project root detection |
| signalService | `signal` | SIGINT/SIGTERM/SIGHUP handling |
## Documentation
- [Getting Started](getting-started.md) -- `Main()`, `WithCommands()`, building binaries
- [Commands](commands.md) -- Command builders, flag helpers, args validation
- [Output](output.md) -- Styled output, tables, trees, task trackers
- [Prompts](prompts.md) -- Interactive prompts, confirmations, selections
- [Streaming](streaming.md) -- Real-time token streaming with word-wrap
- [Daemon](daemon.md) -- Daemon mode, PID files, health checks
- [Errors](errors.md) -- Error creation, wrapping, exit codes
## Colour & Theme Control
Colours are enabled by default and respect the `NO_COLOR` environment variable and `TERM=dumb`. You can also control them programmatically:
```go
cli.SetColorEnabled(false) // Disable ANSI colours
cli.UseASCII() // ASCII glyphs + disable colours
cli.UseEmoji() // Emoji glyph theme
cli.UseUnicode() // Default Unicode glyph theme
```
## Execution Modes
The framework auto-detects the execution environment:
```go
mode := cli.DetectMode()
// cli.ModeInteractive -- TTY attached, colours enabled
// cli.ModePipe -- stdout piped, colours disabled
// cli.ModeDaemon -- CORE_DAEMON=1, log-only output
cli.IsTTY() // stdout is a terminal?
cli.IsStdinTTY() // stdin is a terminal?
cli.IsStderrTTY() // stderr is a terminal?
```

283
docs/pkg/cli/output.md Normal file
View file

@ -0,0 +1,283 @@
---
title: Output Functions
description: Styled output, tables, trees, task trackers, glyphs, and formatting utilities.
---
# Output Functions
All output functions support glyph shortcodes (`:check:`, `:cross:`, `:warn:`, `:info:`) which are auto-converted to Unicode symbols.
## Styled Messages
```go
cli.Success("All tests passed") // ✓ All tests passed (green, bold)
cli.Successf("Built %d files", n) // ✓ Built 5 files (green, bold)
cli.Error("Connection refused") // ✗ Connection refused (red, bold, stderr)
cli.Errorf("Port %d in use", port) // ✗ Port 8080 in use (red, bold, stderr)
cli.Warn("Deprecated flag used") // ⚠ Deprecated flag used (amber, bold, stderr)
cli.Warnf("Skipping %s", name) // ⚠ Skipping foo (amber, bold, stderr)
cli.Info("Using default config") // Using default config (blue)
cli.Infof("Found %d items", n) // Found 42 items (blue)
cli.Dim("Optional detail") // Optional detail (grey, dimmed)
```
`Error` and `Warn` write to stderr and also log the message. `Success` and `Info` write to stdout.
## Plain Output
```go
cli.Println("Hello %s", name) // fmt.Sprintf + glyph conversion + newline
cli.Print("Loading...") // No newline
cli.Text("raw", "text") // Like fmt.Println but with glyphs
cli.Blank() // Empty line
cli.Echo("key.label", args...) // i18n.T translation + newline
```
## Structured Output
```go
cli.Label("version", "1.2.0") // Version: 1.2.0 (styled key)
cli.Task("php", "Running tests") // [php] Running tests
cli.Section("audit") // ── AUDIT ──
cli.Hint("fix", "go mod tidy") // fix: go mod tidy
cli.Result(passed, "Tests passed") // ✓ or ✗ based on bool
```
## Error Wrapping for Output
These display errors without returning them -- useful when you need to show an error but continue:
```go
cli.ErrorWrap(err, "load config") // ✗ load config: <error>
cli.ErrorWrapVerb(err, "load", "config") // ✗ Failed to load config: <error>
cli.ErrorWrapAction(err, "connect") // ✗ Failed to connect: <error>
```
All three are nil-safe -- they do nothing if `err` is nil.
## Progress Indicator
Overwrites the current terminal line to show progress:
```go
for i, item := range items {
cli.Progress("check", i+1, len(items), item.Name) // Overwrites line
}
cli.ProgressDone() // Clears progress line
```
The verb is passed through `i18n.Progress()` for gerund form ("Checking...").
## Severity Levels
```go
cli.Severity("critical", "SQL injection found") // [critical] red, bold
cli.Severity("high", "XSS vulnerability") // [high] orange, bold
cli.Severity("medium", "Missing CSRF token") // [medium] amber
cli.Severity("low", "Debug mode enabled") // [low] grey
```
## Check Results
Fluent API for building pass/fail/skip/warn result lines:
```go
cli.Check("audit").Pass().Print() // ✓ audit passed
cli.Check("fmt").Fail().Duration("2.3s").Print() // ✗ fmt failed 2.3s
cli.Check("test").Skip().Print() // - test skipped
cli.Check("lint").Warn().Message("3 warnings").Print()
```
## Tables
Aligned tabular output with optional box-drawing borders:
```go
t := cli.NewTable("REPO", "STATUS", "BRANCH")
t.AddRow("core-php", "clean", "main")
t.AddRow("core-tenant", "dirty", "feature/x")
t.Render()
```
Output:
```
REPO STATUS BRANCH
core-php clean main
core-tenant dirty feature/x
```
### Bordered Tables
```go
t := cli.NewTable("REPO", "STATUS").
WithBorders(cli.BorderRounded)
t.AddRow("core-php", "clean")
t.Render()
```
Output:
```
╭──────────┬────────╮
│ REPO │ STATUS │
├──────────┼────────┤
│ core-php │ clean │
╰──────────┴────────╯
```
Border styles: `BorderNone` (default), `BorderNormal`, `BorderRounded`, `BorderHeavy`, `BorderDouble`.
### Per-Column Cell Styling
```go
t := cli.NewTable("REPO", "STATUS", "BRANCH").
WithCellStyle(1, func(val string) *cli.AnsiStyle {
if val == "clean" {
return cli.SuccessStyle
}
return cli.WarningStyle
}).
WithMaxWidth(80)
```
## Trees
Hierarchical output with box-drawing connectors:
```go
tree := cli.NewTree("core-php")
tree.Add("core-tenant").Add("core-bio")
tree.Add("core-admin")
tree.Add("core-api")
tree.Render()
```
Output:
```
core-php
├── core-tenant
│ └── core-bio
├── core-admin
└── core-api
```
Styled nodes:
```go
tree.AddStyled("core-tenant", cli.RepoStyle)
```
## Task Tracker
Displays multiple concurrent tasks with live spinners. Uses ANSI cursor manipulation when connected to a TTY; falls back to line-by-line output otherwise.
```go
tracker := cli.NewTaskTracker()
for _, repo := range repos {
t := tracker.Add(repo.Name)
go func(t *cli.TrackedTask) {
t.Update("pulling...")
if err := pull(repo); err != nil {
t.Fail(err.Error())
return
}
t.Done("up to date")
}(t)
}
tracker.Wait()
cli.Println(tracker.Summary()) // "5/5 passed"
```
`TrackedTask` methods (`Update`, `Done`, `Fail`) are safe for concurrent use.
## String Builders (No Print)
These return styled strings without printing, for use in composing output:
```go
cli.SuccessStr("done") // Returns "✓ done" styled green
cli.ErrorStr("failed") // Returns "✗ failed" styled red
cli.WarnStr("warning") // Returns "⚠ warning" styled amber
cli.InfoStr("note") // Returns " note" styled blue
cli.DimStr("detail") // Returns dimmed text
cli.Styled(style, "text") // Apply any AnsiStyle
cli.Styledf(style, "formatted %s", arg)
```
## Glyph Shortcodes
All output functions auto-convert shortcodes to symbols:
| Shortcode | Unicode | Emoji | ASCII |
|-----------|---------|-------|-------|
| `:check:` | ✓ | ✅ | [OK] |
| `:cross:` | ✗ | ❌ | [FAIL] |
| `:warn:` | ⚠ | ⚠️ | [WARN] |
| `:info:` | | | [INFO] |
| `:arrow_right:` | → | ➡️ | -> |
| `:bullet:` | • | • | * |
| `:dash:` | ─ | ─ | - |
| `:pipe:` | │ | │ | \| |
| `:corner:` | └ | └ | \` |
| `:tee:` | ├ | ├ | + |
| `:pending:` | … | ⏳ | ... |
Switch themes:
```go
cli.UseUnicode() // Default
cli.UseEmoji() // Emoji symbols
cli.UseASCII() // ASCII fallback (also disables colours)
```
## ANSI Styles
Build custom styles with the fluent `AnsiStyle` API:
```go
style := cli.NewStyle().Bold().Foreground(cli.ColourBlue500)
fmt.Println(style.Render("Important text"))
```
Available methods: `Bold()`, `Dim()`, `Italic()`, `Underline()`, `Foreground(hex)`, `Background(hex)`.
The framework provides pre-defined styles using the Tailwind colour palette:
| Style | Description |
|-------|-------------|
| `SuccessStyle` | Green, bold |
| `ErrorStyle` | Red, bold |
| `WarningStyle` | Amber, bold |
| `InfoStyle` | Blue |
| `SecurityStyle` | Purple, bold |
| `DimStyle` | Grey, dimmed |
| `HeaderStyle` | Grey-200, bold |
| `AccentStyle` | Cyan |
| `LinkStyle` | Blue, underlined |
| `CodeStyle` | Grey-300 |
| `NumberStyle` | Blue-300 |
| `RepoStyle` | Blue, bold |
Colours respect `NO_COLOR` and `TERM=dumb`. Use `cli.ColorEnabled()` to check and `cli.SetColorEnabled(false)` to disable programmatically.
## Formatting Utilities
```go
cli.Truncate("long string", 10) // "long st..."
cli.Pad("short", 20) // "short "
cli.FormatAge(time.Now().Add(-2*time.Hour)) // "2h ago"
```
## Logging
The framework provides package-level log functions that delegate to the Core log service when available:
```go
cli.LogDebug("cache miss", "key", "foo")
cli.LogInfo("server started", "port", 8080)
cli.LogWarn("slow query", "duration", "3.2s")
cli.LogError("connection failed", "err", err)
cli.LogSecurity("login attempt", "user", "admin")
```

187
docs/pkg/cli/prompts.md Normal file
View file

@ -0,0 +1,187 @@
---
title: Interactive Prompts
description: Text prompts, confirmations, single and multi-select menus, and type-safe generic selectors.
---
# Interactive Prompts
The framework provides several prompt functions, ranging from simple text input to type-safe generic selectors.
## Text Prompt
Basic text input with an optional default:
```go
name, err := cli.Prompt("Project name", "my-app")
// Project name [my-app]: _
// Returns default if user presses Enter
```
## Question (Enhanced Prompt)
`Question` extends `Prompt` with validation and required-input support:
```go
name := cli.Question("Enter your name:")
name := cli.Question("Enter your name:", cli.WithDefault("Anonymous"))
name := cli.Question("Enter your name:", cli.RequiredInput())
```
With validation:
```go
port := cli.Question("Port:", cli.WithValidator(func(s string) error {
n, err := strconv.Atoi(s)
if err != nil || n < 1 || n > 65535 {
return fmt.Errorf("must be 1-65535")
}
return nil
}))
```
Grammar-composed question:
```go
name := cli.QuestionAction("rename", "old.txt")
// Rename old.txt? _
```
## Confirmation
Yes/no confirmation with sensible defaults:
```go
if cli.Confirm("Delete file?") { ... } // Default: no [y/N]
if cli.Confirm("Save?", cli.DefaultYes()) { ... } // Default: yes [Y/n]
if cli.Confirm("Destroy?", cli.Required()) { ... } // Must type y or n [y/n]
```
With auto-timeout:
```go
if cli.Confirm("Continue?", cli.Timeout(30*time.Second)) { ... }
// Continue? [y/N] (auto in 30s)
// Auto-selects default after timeout
```
Combine options:
```go
if cli.Confirm("Deploy?", cli.DefaultYes(), cli.Timeout(10*time.Second)) { ... }
// Deploy? [Y/n] (auto in 10s)
```
### Grammar-Composed Confirmation
```go
if cli.ConfirmAction("delete", "config.yaml") { ... }
// Delete config.yaml? [y/N]
if cli.ConfirmAction("save", "changes", cli.DefaultYes()) { ... }
// Save changes? [Y/n]
```
### Dangerous Action (Double Confirmation)
```go
if cli.ConfirmDangerousAction("delete", "production database") { ... }
// Delete production database? [y/n] (must type y/n)
// Really delete production database? [y/n]
```
## Single Select
Numbered menu, returns the selected string:
```go
choice, err := cli.Select("Choose backend:", []string{"metal", "rocm", "cpu"})
// Choose backend:
// 1. metal
// 2. rocm
// 3. cpu
// Choose [1-3]: _
```
## Multi Select
Space-separated number input, returns selected strings:
```go
tags, err := cli.MultiSelect("Enable features:", []string{"auth", "api", "admin", "mcp"})
// Enable features:
// 1. auth
// 2. api
// 3. admin
// 4. mcp
// Choose (space-separated) [1-4]: _
```
## Type-Safe Generic Select (`Choose`)
For selecting from typed slices with custom display:
```go
type File struct {
Name string
Size int64
}
files := []File{{Name: "a.go", Size: 1024}, {Name: "b.go", Size: 2048}}
choice := cli.Choose("Select a file:", files,
cli.Display(func(f File) string {
return fmt.Sprintf("%s (%d bytes)", f.Name, f.Size)
}),
)
```
With a default selection:
```go
choice := cli.Choose("Select:", items, cli.WithDefaultIndex[Item](0))
// Items marked with * are the default when Enter is pressed
```
Grammar-composed:
```go
file := cli.ChooseAction("select", "file", files)
// Select file:
// 1. ...
```
## Type-Safe Generic Multi-Select (`ChooseMulti`)
Select multiple items with ranges:
```go
selected := cli.ChooseMulti("Select files:", files,
cli.Display(func(f File) string { return f.Name }),
)
// Select files:
// 1. a.go
// 2. b.go
// 3. c.go
// Enter numbers (e.g., 1 3 5 or 1-3) or empty for none: _
```
Input formats:
- `1 3 5` -- select items 1, 3, and 5
- `1-3` -- select items 1, 2, and 3
- `1 3-5` -- select items 1, 3, 4, and 5
- (empty) -- select none
Grammar-composed:
```go
files := cli.ChooseMultiAction("select", "files", allFiles)
```
## Testing Prompts
Override stdin for testing:
```go
cli.SetStdin(strings.NewReader("test input\n"))
defer cli.SetStdin(os.Stdin)
```

97
docs/pkg/cli/streaming.md Normal file
View file

@ -0,0 +1,97 @@
---
title: Streaming Output
description: Real-time token-by-token text rendering with optional word-wrap.
---
# Streaming Output
The `Stream` type renders growing text as tokens arrive, with optional word-wrap. It is designed for displaying LLM output, log tails, or any content that arrives incrementally. Thread-safe for a single producer goroutine.
## Basic Usage
```go
stream := cli.NewStream()
go func() {
for token := range tokens {
stream.Write(token)
}
stream.Done()
}()
stream.Wait()
```
`Done()` ensures a trailing newline if the stream did not end with one. `Wait()` blocks until `Done()` is called.
## Word Wrap
Wrap text at a column boundary:
```go
stream := cli.NewStream(cli.WithWordWrap(80))
```
When word-wrap is enabled, the stream tracks the current column position and inserts line breaks when the column limit is reached.
## Custom Output Writer
By default, streams write to `os.Stdout`. Redirect to any `io.Writer`:
```go
var buf strings.Builder
stream := cli.NewStream(cli.WithStreamOutput(&buf))
// ... write tokens ...
stream.Done()
result := stream.Captured() // or buf.String()
```
`Captured()` returns the output as a string when using a `*strings.Builder` or any `fmt.Stringer`.
## Reading from `io.Reader`
Stream content from an HTTP response body, file, or any `io.Reader`:
```go
stream := cli.NewStream(cli.WithWordWrap(120))
err := stream.WriteFrom(resp.Body)
stream.Done()
```
`WriteFrom` reads in 256-byte chunks until EOF, calling `Write` for each chunk.
## API Reference
| Method | Description |
|--------|-------------|
| `NewStream(opts...)` | Create a stream with options |
| `Write(text)` | Append text (thread-safe) |
| `WriteFrom(r)` | Stream from `io.Reader` until EOF |
| `Done()` | Signal completion (adds trailing newline if needed) |
| `Wait()` | Block until `Done` is called |
| `Column()` | Current column position |
| `Captured()` | Get output as string (requires `*strings.Builder` or `fmt.Stringer` writer) |
## Options
| Option | Description |
|--------|-------------|
| `WithWordWrap(cols)` | Set the word-wrap column width |
| `WithStreamOutput(w)` | Set the output writer (default: `os.Stdout`) |
## Example: LLM Token Streaming
```go
func streamResponse(ctx context.Context, model *Model, prompt string) error {
stream := cli.NewStream(cli.WithWordWrap(100))
go func() {
ch := model.Generate(ctx, prompt)
for token := range ch {
stream.Write(token)
}
stream.Done()
}()
stream.Wait()
return nil
}
```