docs: initial wiki — pkg/cli API reference and usage guides
commit
197f05cc69
9 changed files with 630 additions and 0 deletions
102
Command-Builders.md
Normal file
102
Command-Builders.md
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
# Command Builders & Flag Helpers
|
||||||
|
|
||||||
|
## Command Types
|
||||||
|
|
||||||
|
### `NewCommand` — Standard command (returns error)
|
||||||
|
|
||||||
|
```go
|
||||||
|
cmd := cli.NewCommand("build", "Build the project", "", func(cmd *cli.Command, args []string) error {
|
||||||
|
// Business logic here
|
||||||
|
return nil // or return fmt.Errorf("failed: %w", err)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### `NewGroup` — Parent command (no handler, only subcommands)
|
||||||
|
|
||||||
|
```go
|
||||||
|
scoreCmd := cli.NewGroup("score", "Scoring commands", "")
|
||||||
|
scoreCmd.AddCommand(grammarCmd, attentionCmd, tierCmd)
|
||||||
|
root.AddCommand(scoreCmd)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `NewRun` — Simple command (no error return)
|
||||||
|
|
||||||
|
```go
|
||||||
|
cmd := cli.NewRun("version", "Show version", "", func(cmd *cli.Command, args []string) {
|
||||||
|
cli.Println("v1.0.0")
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Flag Helpers
|
||||||
|
|
||||||
|
All flag helpers follow the same signature: `(cmd, ptr, name, short, default, usage)`.
|
||||||
|
|
||||||
|
```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 (inherited by subcommands)
|
||||||
|
|
||||||
|
```go
|
||||||
|
cli.PersistentStringFlag(parentCmd, &dbPath, "db", "d", "", "Database path")
|
||||||
|
cli.PersistentBoolFlag(parentCmd, &debug, "debug", "", false, "Debug mode")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Args Validation
|
||||||
|
|
||||||
|
```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.NoArgs()) // No args allowed
|
||||||
|
cli.WithArgs(cmd, cli.ArbitraryArgs()) // Any args
|
||||||
|
```
|
||||||
|
|
||||||
|
## Re-exports
|
||||||
|
|
||||||
|
- `cli.Command` = `cobra.Command` (no need to import cobra)
|
||||||
|
- `cli.PositionalArgs` = `cobra.PositionalArgs`
|
||||||
|
|
||||||
|
## Pattern: Config Struct + Flags
|
||||||
|
|
||||||
|
The idiomatic pattern for commands with many flags:
|
||||||
|
|
||||||
|
```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)
|
||||||
|
}
|
||||||
|
```
|
||||||
78
Daemon-Mode.md
Normal file
78
Daemon-Mode.md
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
# Daemon Mode
|
||||||
|
|
||||||
|
## Execution Modes
|
||||||
|
|
||||||
|
```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 terminal?
|
||||||
|
cli.IsStdinTTY() // stdin is terminal?
|
||||||
|
```
|
||||||
|
|
||||||
|
## Simple Daemon
|
||||||
|
|
||||||
|
```go
|
||||||
|
func runDaemon(cmd *cli.Command, args []string) error {
|
||||||
|
ctx := cli.Context() // Cancelled on SIGINT/SIGTERM
|
||||||
|
// ... start work ...
|
||||||
|
return cli.Run(ctx) // Blocks until signal
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Full Daemon with Health Checks
|
||||||
|
|
||||||
|
```go
|
||||||
|
daemon := cli.NewDaemon(cli.DaemonOptions{
|
||||||
|
PIDFile: "/var/run/myapp.pid",
|
||||||
|
ShutdownTimeout: 30 * time.Second,
|
||||||
|
HealthAddr: ":9090",
|
||||||
|
HealthChecks: []cli.HealthCheck{checkDB},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := daemon.Start(); err != nil {
|
||||||
|
return err // "another instance is running (PID 1234)"
|
||||||
|
}
|
||||||
|
|
||||||
|
daemon.SetReady(true)
|
||||||
|
return daemon.Run(cli.Context()) // Blocks, handles shutdown
|
||||||
|
```
|
||||||
|
|
||||||
|
### Health Endpoints
|
||||||
|
|
||||||
|
- `GET /health` — Liveness (200 if server up, 503 if checks fail)
|
||||||
|
- `GET /ready` — Readiness (200 if ready, 503 if not)
|
||||||
|
|
||||||
|
## PID File
|
||||||
|
|
||||||
|
```go
|
||||||
|
pid := cli.NewPIDFile("/tmp/myapp.pid")
|
||||||
|
if err := pid.Acquire(); err != nil {
|
||||||
|
// "another instance is running (PID 1234)"
|
||||||
|
}
|
||||||
|
defer pid.Release()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shutdown with Timeout
|
||||||
|
|
||||||
|
```go
|
||||||
|
cli.Init(cli.Options{AppName: "worker"})
|
||||||
|
shutdown := cli.RunWithTimeout(30 * time.Second)
|
||||||
|
defer shutdown() // Replaces cli.Shutdown()
|
||||||
|
cli.Run(cli.Context())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Signal Handling
|
||||||
|
|
||||||
|
Built into the runtime — SIGINT/SIGTERM cancel `cli.Context()`. Optional SIGHUP for config reload:
|
||||||
|
|
||||||
|
```go
|
||||||
|
cli.Init(cli.Options{
|
||||||
|
AppName: "daemon",
|
||||||
|
OnReload: func() error {
|
||||||
|
return reloadConfig()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
72
Error-Handling.md
Normal file
72
Error-Handling.md
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
# Error Handling
|
||||||
|
|
||||||
|
## Error Creation
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Simple error (replaces fmt.Errorf)
|
||||||
|
return cli.Err("invalid model: %s", name)
|
||||||
|
|
||||||
|
// Wrap with context (nil-safe)
|
||||||
|
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>"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Inspection
|
||||||
|
|
||||||
|
Re-exports of `errors` package for convenience:
|
||||||
|
|
||||||
|
```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
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Return specific exit code from a command
|
||||||
|
return cli.Exit(2, fmt.Errorf("validation failed"))
|
||||||
|
```
|
||||||
|
|
||||||
|
The `ExitError` type is checked in `Main()` — commands that return `*ExitError` cause the process to exit with that code.
|
||||||
|
|
||||||
|
## Fatal Functions (Deprecated)
|
||||||
|
|
||||||
|
These exist for legacy code but should not be used in new commands. Return errors from `RunE` instead.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// DON'T use these in new code:
|
||||||
|
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)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pattern: Commands Return Errors
|
||||||
|
|
||||||
|
```go
|
||||||
|
// GOOD: 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// BAD: calling os.Exit from library code
|
||||||
|
func runBuild(cmd *cli.Command, args []string) error {
|
||||||
|
if err := compile(); err != nil {
|
||||||
|
cli.Fatal(err) // DON'T DO THIS
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
120
Framework-Integration.md
Normal file
120
Framework-Integration.md
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
# Framework Integration
|
||||||
|
|
||||||
|
## How Commands Register
|
||||||
|
|
||||||
|
Commands register through the Core framework lifecycle using `WithCommands`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// In main.go
|
||||||
|
cli.Main(
|
||||||
|
cli.WithCommands("score", score.AddScoreCommands),
|
||||||
|
cli.WithCommands("gen", gen.AddGenCommands),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Internally, `WithCommands` wraps the registration function in a `commandService` that implements `Startable.OnStartup()`. During startup, it casts `Core.App` to `*cobra.Command` and calls the registration function.
|
||||||
|
|
||||||
|
**Startup order:**
|
||||||
|
1. Core services start (i18n, log, crypt, workspace)
|
||||||
|
2. Command services start (your `WithCommands` functions)
|
||||||
|
3. `Execute()` runs the matched command
|
||||||
|
|
||||||
|
## Registration Function Pattern
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Accessing Core 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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Built-in Services
|
||||||
|
|
||||||
|
| Service | Name | Purpose |
|
||||||
|
|---------|------|---------|
|
||||||
|
| i18n | `i18n` | Internationalisation, grammar |
|
||||||
|
| log | `log` | Structured logging (slog) |
|
||||||
|
| crypt | `crypt` | OpenPGP encryption |
|
||||||
|
| workspace | `workspace` | Project root detection |
|
||||||
|
| signal | `signal` | SIGINT/SIGTERM/SIGHUP handling |
|
||||||
|
|
||||||
|
## `Main()` Lifecycle
|
||||||
|
|
||||||
|
1. Recovery defer (catches panics)
|
||||||
|
2. Register core services (i18n, log, crypt, workspace)
|
||||||
|
3. Append user command services
|
||||||
|
4. `Init()` — creates cobra root, signals, starts all services
|
||||||
|
5. Add completion command
|
||||||
|
6. `Execute()` — runs matched command
|
||||||
|
7. `Shutdown()` — stops all services in reverse order
|
||||||
|
|
||||||
|
## Legacy: `RegisterCommands`
|
||||||
|
|
||||||
|
For packages that need `init()`-time registration (not recommended):
|
||||||
|
|
||||||
|
```go
|
||||||
|
func init() {
|
||||||
|
cli.RegisterCommands(func(root *cobra.Command) {
|
||||||
|
root.AddCommand(myCmd)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Prefer `WithCommands` — it's explicit and doesn't rely on import side effects.
|
||||||
|
|
||||||
|
## Building a CLI Binary
|
||||||
|
|
||||||
|
```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 `[]framework.Option`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func Commands() []framework.Option {
|
||||||
|
return []framework.Option{
|
||||||
|
cli.WithCommands("score", addScoreCommands),
|
||||||
|
cli.WithCommands("gen", addGenCommands),
|
||||||
|
cli.WithCommands("data", addDataCommands),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
41
Home.md
Normal file
41
Home.md
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Core CLI Framework (`pkg/cli`)
|
||||||
|
|
||||||
|
Go 1.26 CLI framework built on cobra + bubbletea + lipgloss. Provides command registration, flag helpers, styled output, streaming, daemon mode, 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)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pages
|
||||||
|
|
||||||
|
- [[Command-Builders]] — NewCommand, NewGroup, NewRun, flag helpers
|
||||||
|
- [[Output-Functions]] — Success, Error, Warn, Info, Progress, Section, Label
|
||||||
|
- [[Error-Handling]] — Err, Wrap, WrapVerb, ExitError, Is/As/Join
|
||||||
|
- [[Streaming]] — Stream for token-by-token rendering
|
||||||
|
- [[Daemon-Mode]] — PIDFile, HealthServer, Daemon runner
|
||||||
|
- [[Prompt-Input]] — Prompt, Select, MultiSelect
|
||||||
|
- [[Runtime]] — Init, Main, Shutdown, Context, signal handling
|
||||||
|
- [[Framework-Integration]] — WithCommands, Core lifecycle, services
|
||||||
|
- [[Styles-and-Glyphs]] — ANSI styles, glyph shortcodes, colours
|
||||||
|
- [[Frame-TUI]] — Bubbletea AppShell with focus, navigation, keymaps
|
||||||
63
Output-Functions.md
Normal file
63
Output-Functions.md
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
# Output Functions
|
||||||
|
|
||||||
|
All output functions support glyph shortcodes (`:check:`, `:cross:`, `:warn:`, `:info:`) which are auto-converted to Unicode symbols.
|
||||||
|
|
||||||
|
## Styled Output
|
||||||
|
|
||||||
|
```go
|
||||||
|
cli.Success("All tests passed") // ✓ All tests passed (green)
|
||||||
|
cli.Successf("Built %d files", n) // ✓ Built 5 files (green)
|
||||||
|
cli.Error("Connection refused") // ✗ Connection refused (red, stderr)
|
||||||
|
cli.Errorf("Port %d in use", port) // ✗ Port 8080 in use (red, stderr)
|
||||||
|
cli.Warn("Deprecated flag used") // ⚠ Deprecated flag used (amber, stderr)
|
||||||
|
cli.Warnf("Skipping %s", name) // ⚠ Skipping foo (amber, 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 (gray)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Progress
|
||||||
|
|
||||||
|
```go
|
||||||
|
for i, item := range items {
|
||||||
|
cli.Progress("check", i+1, len(items), item.Name) // Overwrites line
|
||||||
|
}
|
||||||
|
cli.ProgressDone() // Clears progress line
|
||||||
|
```
|
||||||
|
|
||||||
|
## Severity
|
||||||
|
|
||||||
|
```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] gray
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Wrapping for Output
|
||||||
|
|
||||||
|
```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>
|
||||||
|
```
|
||||||
42
Prompt-Input.md
Normal file
42
Prompt-Input.md
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
# Prompt & Input
|
||||||
|
|
||||||
|
## Text Prompt
|
||||||
|
|
||||||
|
```go
|
||||||
|
name, err := cli.Prompt("Project name", "my-app")
|
||||||
|
// Project name [my-app]: _
|
||||||
|
// Returns default if user presses Enter
|
||||||
|
```
|
||||||
|
|
||||||
|
## Single Select
|
||||||
|
|
||||||
|
```go
|
||||||
|
choice, err := cli.Select("Choose backend:", []string{"metal", "rocm", "cpu"})
|
||||||
|
// Choose backend:
|
||||||
|
// 1. metal
|
||||||
|
// 2. rocm
|
||||||
|
// 3. cpu
|
||||||
|
// Choose [1-3]: _
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multi Select
|
||||||
|
|
||||||
|
```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]: _
|
||||||
|
// Returns []string of selected items
|
||||||
|
```
|
||||||
|
|
||||||
|
## Confirm (via Prompt)
|
||||||
|
|
||||||
|
```go
|
||||||
|
answer, _ := cli.Prompt("Proceed? (y/n)", "y")
|
||||||
|
if answer == "y" || answer == "Y" {
|
||||||
|
// proceed
|
||||||
|
}
|
||||||
|
```
|
||||||
60
Runtime.md
Normal file
60
Runtime.md
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
# Runtime
|
||||||
|
|
||||||
|
## Initialisation
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Simple (recommended)
|
||||||
|
cli.WithAppName("myapp")
|
||||||
|
cli.Main(
|
||||||
|
cli.WithCommands("cmd", addCommands),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Manual (advanced)
|
||||||
|
cli.Init(cli.Options{
|
||||||
|
AppName: "myapp",
|
||||||
|
Version: "1.0.0",
|
||||||
|
Services: []framework.Option{...},
|
||||||
|
OnReload: func() error { return reloadConfig() },
|
||||||
|
})
|
||||||
|
defer cli.Shutdown()
|
||||||
|
cli.Execute()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Global Accessors
|
||||||
|
|
||||||
|
| Function | Returns | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `cli.Core()` | `*framework.Core` | Framework container |
|
||||||
|
| `cli.RootCmd()` | `*cobra.Command` | Root cobra command |
|
||||||
|
| `cli.Context()` | `context.Context` | Cancelled on SIGINT/SIGTERM |
|
||||||
|
| `cli.Execute()` | `error` | Run matched command |
|
||||||
|
| `cli.Shutdown()` | — | Stop all services |
|
||||||
|
|
||||||
|
## Version Info
|
||||||
|
|
||||||
|
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)"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Signal Handling
|
||||||
|
|
||||||
|
Automatic via the `signalService` registered during `Init()`:
|
||||||
|
|
||||||
|
- **SIGINT/SIGTERM** → cancels `cli.Context()`
|
||||||
|
- **SIGHUP** → calls `OnReload` if configured
|
||||||
|
|
||||||
|
No manual signal handling needed in commands — use `cli.Context()` for cancellation.
|
||||||
52
Streaming.md
Normal file
52
Streaming.md
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Streaming Text Output
|
||||||
|
|
||||||
|
The `Stream` type renders growing text as tokens arrive, with optional word-wrap. 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()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Word Wrap
|
||||||
|
|
||||||
|
```go
|
||||||
|
stream := cli.NewStream(cli.WithWordWrap(80))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Output
|
||||||
|
|
||||||
|
```go
|
||||||
|
var buf strings.Builder
|
||||||
|
stream := cli.NewStream(cli.WithStreamOutput(&buf))
|
||||||
|
// ... write tokens ...
|
||||||
|
stream.Done()
|
||||||
|
result := stream.Captured() // or buf.String()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reading from io.Reader
|
||||||
|
|
||||||
|
```go
|
||||||
|
stream := cli.NewStream(cli.WithWordWrap(120))
|
||||||
|
err := stream.WriteFrom(resp.Body)
|
||||||
|
stream.Done()
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `NewStream(opts...)` | Create 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 (when using Builder) |
|
||||||
Loading…
Add table
Reference in a new issue