go/docs/plans/completed/2026-02-21-cli-meta-package-design-original.md
Snider 12ff432d6b docs: archive completed plans with summaries
Move completed plan documents to docs/plans/completed/ with
concise completion summaries alongside the originals.

Archived: MCP integration, Go API design/plan, CLI meta-package design.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 23:46:13 +00:00

5.4 KiB

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-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.

// 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.