From 138927baa5f377e025a56d9f7c0cf21b31bc2b18 Mon Sep 17 00:00:00 2001 From: Snider Date: Sat, 21 Feb 2026 22:13:22 +0000 Subject: [PATCH] docs: update plans to reflect WithCommands lifecycle pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rewrite cli-meta-package-design to document current state: WithCommands(), completed migrations, no init()/blank imports - Add completion status note to MCP integration plan - Update pkg-batch2-analysis RegisterCommands → WithCommands Co-Authored-By: Virgil --- docs/pkg-batch2-analysis.md | 4 +- docs/plans/2026-02-05-mcp-integration.md | 2 + .../2026-02-21-cli-meta-package-design.md | 302 ++++-------------- 3 files changed, 66 insertions(+), 242 deletions(-) diff --git a/docs/pkg-batch2-analysis.md b/docs/pkg-batch2-analysis.md index 2cbfc92..9557a55 100644 --- a/docs/pkg-batch2-analysis.md +++ b/docs/pkg-batch2-analysis.md @@ -30,7 +30,7 @@ The `cli` package is a comprehensive application runtime and UI framework design #### Command Building - `func NewCommand(use, short, long string, run func(*Command, []string) error) *Command`: Factory for standard commands. - `func NewGroup(use, short, long string) *Command`: Factory for parent commands (no run logic). -- `func RegisterCommands(fn CommandRegistration)`: Registers a callback to add commands to the root at runtime. +- `func WithCommands(name string, register func(root *Command)) framework.Option`: Registers a command group as a framework service. #### Output & Styling - `type AnsiStyle`: Fluent builder for text styling (Bold, Dim, Foreground, Background). @@ -67,7 +67,7 @@ The `cli` package is a comprehensive application runtime and UI framework design ### 5. Test Coverage Notes - **Interactive Prompts**: Tests must mock `stdin` to verify `Confirm`, `Prompt`, and `Select` behavior without hanging. -- **Command Registration**: Verify `RegisterCommands` works both before and after `Init` is called. +- **Command Registration**: Verify `WithCommands` services receive the root command during `OnStartup`. - **Daemon Lifecycle**: Tests needed for `PIDFile` locking and `HealthServer` endpoints (/health, /ready). - **Layout Rendering**: Snapshot testing is recommended for `Layout` and `Table` rendering to ensure ANSI codes and alignment are correct. diff --git a/docs/plans/2026-02-05-mcp-integration.md b/docs/plans/2026-02-05-mcp-integration.md index c63285a..9b3a109 100644 --- a/docs/plans/2026-02-05-mcp-integration.md +++ b/docs/plans/2026-02-05-mcp-integration.md @@ -1,5 +1,7 @@ # MCP Integration Implementation Plan +> **Status:** Completed. MCP command now lives in `go-ai/cmd/mcpcmd/`. Code examples below use the old `init()` + `RegisterCommands()` pattern — the current approach uses `cli.WithCommands()` (see cli-meta-package-design.md). + > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Add `core mcp serve` command with RAG and metrics tools, then configure the agentic-flows plugin to use it. diff --git a/docs/plans/2026-02-21-cli-meta-package-design.md b/docs/plans/2026-02-21-cli-meta-package-design.md index 2cf9bc6..eaf886f 100644 --- a/docs/plans/2026-02-21-cli-meta-package-design.md +++ b/docs/plans/2026-02-21-cli-meta-package-design.md @@ -2,9 +2,9 @@ **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:** Self-registration via `init()` + `cli.RegisterCommands()` (existing pattern, already works). Command code moves from `cli/cmd/*` into domain repos. The cli repo becomes a collection of `main.go` files — each variant blank-imports the domain `cmd` packages it needs. +**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), Go workspaces, Taskfile +**Tech Stack:** go/pkg/cli (wraps cobra + charmbracelet), Core framework lifecycle, Taskfile --- @@ -12,7 +12,7 @@ `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 (keep as-is) +### 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()` @@ -22,17 +22,8 @@ - **Layout:** HLCRF composite renderer (Header/Left/Content/Right/Footer) - **Errors:** `Wrap()`, `WrapVerb()`, `ExitError`, `Is()`, `As()` - **Logging:** `LogDebug()`, `LogInfo()`, `LogWarn()`, `LogError()`, `LogSecurity()` - -### New — TUI primitives (charmbracelet under the hood) - -Domain packages call these; the charm dependency stays inside `go/pkg/cli`. - -- `Spinner(message string) *SpinnerHandle` — async spinner with `.Update(msg)`, `.Done()`, `.Fail()` -- `ProgressBar(total int) *ProgressHandle` — progress bar with `.Increment()`, `.SetMessage(msg)`, `.Done()` -- `List(items []string, opts ...ListOption) (string, error)` — interactive scrollable list selection -- `TextInput(prompt string, opts ...InputOption) (string, error)` — styled single-line text input -- `Viewport(content string, opts ...ViewportOption) error` — scrollable content pane (for long output) -- `RunTUI(model Model) error` — escape hatch for complex interactive UIs (wraps `tea.Model`) +- **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) @@ -42,265 +33,96 @@ Domain packages call these; the charm dependency stays inside `go/pkg/cli`. ### Rule -Domain packages import `forge.lthn.ai/core/go/pkg/cli` and **nothing else** for CLI concerns. No `cobra`, no `lipgloss`, no `bubbletea`. The 34 files in cli/ that currently import cobra directly get rewritten to use `cli.*` helpers during migration. +Domain packages import `forge.lthn.ai/core/go/pkg/cli` and **nothing else** for CLI concerns. No `cobra`, no `lipgloss`, no `bubbletea`. --- -## 2. Domain-Owned Commands +## 2. Command Registration — Framework Lifecycle -Each domain repo exports its commands via the existing self-registration pattern. The command code moves out of `cli/cmd/*` into the domain repo that owns the business logic. - -### Package layout in domain repos - -``` -go-ml/ -├── cmd/ # CLI commands (self-registering) -│ ├── cmd.go # init() + AddMLCommands(root) -│ ├── cmd_score.go -│ ├── cmd_chat.go -│ └── ... -├── service.go # existing business logic -└── go.mod -``` +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-ml/cmd/cmd.go -package cmd +// go-ai/cmd/daemon/cmd.go +package daemon import "forge.lthn.ai/core/go/pkg/cli" -func init() { - cli.RegisterCommands(AddMLCommands) -} - -func AddMLCommands(root *cli.Command) { - mlCmd := cli.NewGroup("ml", "ML inference and training", "") - root.AddCommand(mlCmd) - addScoreCommand(mlCmd) - addChatCommand(mlCmd) - // ... +// 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... } ``` -### Migration mapping +No `init()`. No blank imports. No `cli.RegisterCommands()`. -| Current location | Destination | Files | -|-----------------|-------------|-------| -| `cmd/ml` | `go-ml/cmd/` | 40 | -| `cmd/ai` | `go-agent/cmd/` | 10 | -| `cmd/dev` | `go-devops/cmd/` | 20 | -| `cmd/forge` | `go-scm/cmd/` | 12 | -| `cmd/gitea` | `go-scm/cmd/` | 7 | -| `cmd/collect` | `go-scm/cmd/` | 8 | -| `cmd/security` | `go-devops/cmd/` | 7 | -| `cmd/deploy` | `go-devops/cmd/` | 3 | -| `cmd/prod` | `go-devops/cmd/` | 7 | -| `cmd/setup` | `go-devops/cmd/` | 14 | -| `cmd/go` | `go-devops/cmd/` | 8 | -| `cmd/qa` | `go-devops/cmd/` | 6 | -| `cmd/test` | `go-devops/cmd/` | 5 | -| `cmd/vm` | `go-devops/cmd/` | 4 | -| `cmd/monitor` | `go-devops/cmd/` | — | -| `cmd/crypt` | `go-crypt/cmd/` | 5 | -| `cmd/rag` | `go-rag/cmd/` | 5 | -| `cmd/unifi` | `go-netops/cmd/` | 7 | -| `cmd/api` | `go-api/cmd/` | 4 | -| `cmd/session` | `go-session/cmd/` | 1 | -| `cmd/gitcmd` | `go-git/cmd/` | 1 | -| `cmd/mcpcmd` | `go-ai/cmd/` | 1 | +### 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) -These are CLI-specific concerns, not domain logic: - -`config`, `workspace`, `doctor`, `help`, `updater`, `daemon`, `lab`, `module`, `pkgcmd`, `plugin`, `docs`, `vanity-import` +`config`, `doctor`, `help`, `module`, `pkgcmd`, `plugin`, `session` --- -## 3. Variant Binaries +## 3. Variant Binaries (future) -The cli/ repo becomes a build assembly point. Each variant is a `main.go` that blank-imports the command packages it needs. - -### Directory layout +The cli/ repo can produce variant binaries by creating multiple `main.go` files that wire different sets of commands. ``` cli/ -├── cmd/ -│ ├── core/main.go # Full CLI — everything -│ ├── core-ci/main.go # CI agent dispatch + SCM -│ ├── core-mlx/main.go # ML inference subprocess -│ ├── core-ops/main.go # DevOps + infra management -│ └── core-gui/main.go # Wails desktop app -├── cmd/ # Meta commands that stay in cli/ -│ ├── config/ -│ ├── doctor/ -│ ├── help/ -│ ├── updater/ -│ └── ... -├── go.mod -├── go.work -└── Taskfile.yaml +├── 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 ``` -### Variant definitions - -**core** (full kitchen sink): -```go -package main - -import ( - "forge.lthn.ai/core/go/pkg/cli" - - // Meta commands (local to cli/) - _ "forge.lthn.ai/core/cli/cmd/config" - _ "forge.lthn.ai/core/cli/cmd/doctor" - _ "forge.lthn.ai/core/cli/cmd/help" - _ "forge.lthn.ai/core/cli/cmd/updater" - _ "forge.lthn.ai/core/cli/cmd/workspace" - - // Domain commands (self-register from domain repos) - _ "forge.lthn.ai/core/go-ml/cmd" - _ "forge.lthn.ai/core/go-agent/cmd" - _ "forge.lthn.ai/core/go-ai/cmd" - _ "forge.lthn.ai/core/go-devops/cmd" - _ "forge.lthn.ai/core/go-scm/cmd" - _ "forge.lthn.ai/core/go-crypt/cmd" - _ "forge.lthn.ai/core/go-rag/cmd" - _ "forge.lthn.ai/core/go-netops/cmd" - _ "forge.lthn.ai/core/go-api/cmd" - _ "forge.lthn.ai/core/go-git/cmd" - _ "forge.lthn.ai/core/go-session/cmd" -) - -func main() { cli.Main() } -``` - -**core-ci** (lightweight CI agent): -```go -package main - -import ( - "forge.lthn.ai/core/go/pkg/cli" - _ "forge.lthn.ai/core/cli/cmd/config" - _ "forge.lthn.ai/core/go-agent/cmd" - _ "forge.lthn.ai/core/go-scm/cmd" - _ "forge.lthn.ai/core/go-devops/cmd" -) - -func main() { cli.Main() } -``` - -**core-mlx** (ML inference as external process): -```go -package main - -import ( - "forge.lthn.ai/core/go/pkg/cli" - _ "forge.lthn.ai/core/cli/cmd/config" - _ "forge.lthn.ai/core/go-ml/cmd" -) - -func main() { cli.Main() } -``` - -**core-ops** (infra management): -```go -package main - -import ( - "forge.lthn.ai/core/go/pkg/cli" - _ "forge.lthn.ai/core/cli/cmd/config" - _ "forge.lthn.ai/core/go-devops/cmd" - _ "forge.lthn.ai/core/go-scm/cmd" - _ "forge.lthn.ai/core/go-netops/cmd" -) - -func main() { cli.Main() } -``` - -### Taskfile - -```yaml -tasks: - build:all: - cmds: - - go build -o bin/core ./cmd/core - - go build -o bin/core-ci ./cmd/core-ci - - go build -o bin/core-mlx ./cmd/core-mlx - - go build -o bin/core-ops ./cmd/core-ops - - build:core: - cmds: [go build -o bin/core ./cmd/core] - - build:ci: - cmds: [go build -o bin/core-ci ./cmd/core-ci] - - build:mlx: - cmds: [go build -o bin/core-mlx ./cmd/core-mlx] - - build:ops: - cmds: [go build -o bin/core-ops ./cmd/core-ops] -``` +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 -- Other packages use `exec.Command("core-mlx", "serve")` to consume heavy subsystems as external processes rather than linking them in -- Adding a new variant = one new `main.go` with the right blank imports +- Adding a new variant = one new `main.go` with the right `WithCommands` calls --- -## 4. Migration Order +## 4. Current State -Gradual migration, largest packages first, cli/ works at every step. Each phase is one session's worth of work. - -### Phase 0: CLI SDK expansion (prerequisite) - -Extend `go/pkg/cli` with charmbracelet TUI wrappers (Spinner, ProgressBar, List, TextInput, Viewport, RunTUI). Stub Form, FilePicker, Tabs. Ensure all `cli.*` helpers cover what the 34 direct-cobra files need. This unblocks all subsequent phases. - -### Phase 1: cmd/ml → go-ml/cmd/ (40 files) - -The ML pipeline is the largest command package and the primary candidate for the `core-mlx` variant. Moving it out proves the pattern and shrinks cli/ by a third. - -### Phase 2: cmd/ai → go-agent/cmd/ (10 files) - -AgentCI dispatch, task management. Natural fit — go-agent already has the orchestration logic. Unblocks `core-ci` variant. - -### Phase 3: cmd/forge + cmd/gitea + cmd/collect → go-scm/cmd/ (27 files) - -All three use go-scm packages directly. Bundle into one move since they share the same domain repo. - -### Phase 4: cmd/dev + cmd/deploy + cmd/prod + cmd/setup + cmd/security + cmd/go + cmd/qa + cmd/test + cmd/vm + cmd/monitor → go-devops/cmd/ (74 files) - -All ops/infra/dev tooling belongs in go-devops. Can split across multiple sessions if needed. Unblocks `core-ops` variant. - -### Phase 5: Small moves (one session, batch them) - -- `cmd/crypt` → `go-crypt/cmd/` (5 files) -- `cmd/rag` → `go-rag/cmd/` (5 files) -- `cmd/unifi` → `go-netops/cmd/` (7 files) -- `cmd/api` → `go-api/cmd/` (4 files, mostly done) -- `cmd/session` → `go-session/cmd/` (1 file) -- `cmd/gitcmd` → `go-git/cmd/` (1 file) -- `cmd/mcpcmd` → `go-ai/cmd/` (1 file) - -### Phase 6: Variant assembly - -Create `cmd/core/main.go`, `cmd/core-ci/main.go`, `cmd/core-mlx/main.go`, `cmd/core-ops/main.go`. Update Taskfile. The current root `main.go` becomes `cmd/core/main.go`. Old `cli/cmd/*` directories that moved out get deleted. - -### Per-phase checklist - -1. Copy files to domain repo's `cmd/` -2. Rewrite any direct cobra imports → `cli.*` helpers -3. `go test ./...` in domain repo -4. Update cli's `main.go` blank import from `cli/cmd/X` → `go-X/cmd` -5. Delete old `cli/cmd/X` -6. `go test ./...` in cli -7. Commit and push both repos - -### End state - -cli/ has ~12 meta packages, ~5 variant `main.go` files, and zero business logic. Everything else lives in the domain repos that own it. Total cli/ LOC drops from ~35K to ~2K. +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.