129 lines
5.4 KiB
Markdown
129 lines
5.4 KiB
Markdown
|
|
# CLI Meta-Package Restructure — Design
|
||
|
|
|
||
|
|
**Goal:** Transform `core/cli` from a 35K LOC monolith into a thin assembly repo that ships variant binaries. Domain repos own their commands. `go/pkg/cli` is the only import any domain package needs for CLI concerns.
|
||
|
|
|
||
|
|
**Architecture:** Commands register as framework services via `cli.WithCommands()`, passed to `cli.Main()`. Command code lives in the domain repos that own the business logic. The cli repo is a thin `main.go` that wires them together.
|
||
|
|
|
||
|
|
**Tech Stack:** go/pkg/cli (wraps cobra + charmbracelet), Core framework lifecycle, Taskfile
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 1. CLI SDK — The Single Import
|
||
|
|
|
||
|
|
`forge.lthn.ai/core/go/pkg/cli` is the **only** import domain packages use for CLI concerns. It wraps cobra, charmbracelet, and stdlib behind a stable API. If the underlying libraries change, only `go/pkg/cli` is touched — every domain repo is insulated.
|
||
|
|
|
||
|
|
### Already done
|
||
|
|
|
||
|
|
- **Cobra:** `Command` type alias, `NewCommand()`, `NewGroup()`, `NewRun()`, flag helpers (`StringFlag`, `BoolFlag`, `IntFlag`, `StringSliceFlag`), arg validators
|
||
|
|
- **Output:** `Success()`, `Error()`, `Warn()`, `Info()`, `Table`, `Section()`, `Label()`, `Task()`, `Hint()`
|
||
|
|
- **Prompts:** `Confirm()`, `Question()`, `Choose()`, `ChooseMulti()` with grammar-based action variants
|
||
|
|
- **Styles:** 17 pre-built styles, `AnsiStyle` builder, Tailwind colour constants (47 hex values)
|
||
|
|
- **Glyphs:** `:check:`, `:cross:`, `:warn:` etc. with Unicode/Emoji/ASCII themes
|
||
|
|
- **Layout:** HLCRF composite renderer (Header/Left/Content/Right/Footer)
|
||
|
|
- **Errors:** `Wrap()`, `WrapVerb()`, `ExitError`, `Is()`, `As()`
|
||
|
|
- **Logging:** `LogDebug()`, `LogInfo()`, `LogWarn()`, `LogError()`, `LogSecurity()`
|
||
|
|
- **TUI primitives:** `Spinner`, `ProgressBar`, `InteractiveList`, `TextInput`, `Viewport`, `RunTUI`
|
||
|
|
- **Command registration:** `WithCommands(name, fn)` — registers commands as framework services
|
||
|
|
|
||
|
|
### Stubbed for later (interface exists, returns simple fallback)
|
||
|
|
|
||
|
|
- `Form(fields []FormField) (map[string]string, error)` — multi-field form (backed by huh later)
|
||
|
|
- `FilePicker(opts ...FilePickerOption) (string, error)` — file browser
|
||
|
|
- `Tabs(items []TabItem) error` — tabbed content panes
|
||
|
|
|
||
|
|
### Rule
|
||
|
|
|
||
|
|
Domain packages import `forge.lthn.ai/core/go/pkg/cli` and **nothing else** for CLI concerns. No `cobra`, no `lipgloss`, no `bubbletea`.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 2. Command Registration — Framework Lifecycle
|
||
|
|
|
||
|
|
Commands register through the Core framework's service lifecycle, not through global state or `init()` functions.
|
||
|
|
|
||
|
|
### The contract
|
||
|
|
|
||
|
|
Each domain repo exports an `Add*Commands(root *cli.Command)` function. The CLI binary wires it in via `cli.WithCommands()`:
|
||
|
|
|
||
|
|
```go
|
||
|
|
// go-ai/cmd/daemon/cmd.go
|
||
|
|
package daemon
|
||
|
|
|
||
|
|
import "forge.lthn.ai/core/go/pkg/cli"
|
||
|
|
|
||
|
|
// AddDaemonCommand adds the 'daemon' command group to the root.
|
||
|
|
func AddDaemonCommand(root *cli.Command) {
|
||
|
|
daemonCmd := cli.NewGroup("daemon", "Manage the core daemon", "")
|
||
|
|
root.AddCommand(daemonCmd)
|
||
|
|
// subcommands...
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
No `init()`. No blank imports. No `cli.RegisterCommands()`.
|
||
|
|
|
||
|
|
### How it works
|
||
|
|
|
||
|
|
`cli.WithCommands(name, fn)` wraps the registration function as a framework service implementing `Startable`. During `Core.ServiceStartup()`, the service's `OnStartup()` casts `Core.App` to `*cobra.Command` and calls the registration function. Core services (i18n, log, workspace) start first since they're registered before command services.
|
||
|
|
|
||
|
|
```go
|
||
|
|
// cli/main.go
|
||
|
|
func main() {
|
||
|
|
cli.Main(
|
||
|
|
cli.WithCommands("config", config.AddConfigCommands),
|
||
|
|
cli.WithCommands("doctor", doctor.AddDoctorCommands),
|
||
|
|
// ...
|
||
|
|
)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Migration status (completed)
|
||
|
|
|
||
|
|
| Source | Destination | Status |
|
||
|
|
|--------|-------------|--------|
|
||
|
|
| `cmd/dev, setup, qa, docs, gitcmd, monitor` | `go-devops/cmd/` | Done |
|
||
|
|
| `cmd/lab` | `go-ai/cmd/` | Done |
|
||
|
|
| `cmd/workspace` | `go-agentic/cmd/` | Done |
|
||
|
|
| `cmd/go` | `core/go/cmd/gocmd` | Done |
|
||
|
|
| `cmd/vanity-import, community` | `go-devops/cmd/` | Done |
|
||
|
|
| `cmd/updater` | `go-update` | Done (own repo) |
|
||
|
|
| `cmd/daemon, mcpcmd, security` | `go-ai/cmd/` | Done |
|
||
|
|
| `cmd/crypt` | `go-crypt/cmd/` | Done |
|
||
|
|
| `cmd/rag` | `go-rag/cmd/` | Done |
|
||
|
|
| `cmd/unifi` | `go-netops/cmd/` | Done |
|
||
|
|
| `cmd/api` | `go-api/cmd/` | Done |
|
||
|
|
| `cmd/collect, forge, gitea` | `go-scm/cmd/` | Done |
|
||
|
|
| `cmd/deploy, prod, vm` | `go-devops/cmd/` | Done |
|
||
|
|
|
||
|
|
### Stays in cli/ (meta/framework commands)
|
||
|
|
|
||
|
|
`config`, `doctor`, `help`, `module`, `pkgcmd`, `plugin`, `session`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 3. Variant Binaries (future)
|
||
|
|
|
||
|
|
The cli/ repo can produce variant binaries by creating multiple `main.go` files that wire different sets of commands.
|
||
|
|
|
||
|
|
```
|
||
|
|
cli/
|
||
|
|
├── main.go # Current — meta commands only
|
||
|
|
├── cmd/core-full/main.go # Full CLI — all ecosystem commands
|
||
|
|
├── cmd/core-ci/main.go # CI agent dispatch + SCM
|
||
|
|
├── cmd/core-mlx/main.go # ML inference subprocess
|
||
|
|
└── cmd/core-ops/main.go # DevOps + infra management
|
||
|
|
```
|
||
|
|
|
||
|
|
Each variant calls `cli.Main()` with its specific `cli.WithCommands()` set. No blank imports needed.
|
||
|
|
|
||
|
|
### Why variants matter
|
||
|
|
|
||
|
|
- `core-mlx` ships to the homelab as a ~10MB binary, not 50MB with devops/forge/netops
|
||
|
|
- `core-ci` deploys to agent machines without ML or CGO dependencies
|
||
|
|
- Adding a new variant = one new `main.go` with the right `WithCommands` calls
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 4. Current State
|
||
|
|
|
||
|
|
cli/ has 7 meta packages, one `main.go`, and zero business logic. Everything else lives in the domain repos that own it. Total cli/ LOC is ~2K.
|