Files
Archduino/docs/superpowers/specs/2026-04-14-plan4a-subcommands-design.md

15 KiB
Raw Permalink Blame History

Plan 4a — User-Facing Subcommands Design

Status: Approved 2026-04-14 Scope: Ship three new subcommands — archduino theme, archduino new, archduino doctor — plus the shared theming package they all build on. This plan absorbs the functionality of lib/scaffold.sh (originally deferred from Plan 3b) and lib/themes/*.sh natively into Go, without shipping bash wrappers.

Plan 4b (reinstall/reset mode), 4c (bootstrap + goreleaser), and 4d (CI) are separate plans with independent specs.

Goal

Complete user-facing feature parity with the Bash installer by adding three subcommands: a theme switcher that updates starship/zellij/yazi/lazygit/NvChad configs in place; a project scaffolder for Arduino sketches; and a doctor command that reports module convergence and pinned-tool version drift.

Architecture

One new shared package and two subcommand packages:

  • internal/theming/ — reusable package that embeds per-theme config assets (5 themes × 4 tool configs + a manifest each) and exposes List() []Theme, Get(id) (Theme, bool), and Apply(cfg, themeID) error. Called from both the install command (applies the wizard's selected theme at the end of an install run) and the new theme subcommand.
  • internal/subcommands/newproject/ — owns archduino new <project> <board>. Board list lives in config.Config.Scaffolding.Boards; .ino starter templates embedded.
  • internal/subcommands/doctor/ — owns archduino doctor. Iterates the module registry, calls Detect + Plan on each, reports per-module status and pinned-tool version drift.

tui.BuiltinThemes and the tui.Theme.NvChadTheme field retire: the canonical theme list is now discovered from theming's embedded manifests. The wizard calls theming.List(). tui.Theme stays as a thin display value (ID, Name) used by the wizard's dropdown.

The existing install command gains a final step: after engine.Run succeeds, call theming.Apply(cfg, finalState.ThemeID) so the initial install leaves tools themed, not just installed. This is an improvement over the Bash behavior, where the theme was never applied during install.

Tech Stack

Go stdlib + existing deps. go:embed for all assets. gopkg.in/yaml.v3 for overlay round-trip updates. huh (already a dep) for the interactive theme picker. text/template used only where needed (neovim chadrc already uses it; theming's tool configs are static and don't).

Files

New — internal/theming/

  • theming.go
    • Exported: type Theme struct { ID, DisplayName, NvChadTheme string }
    • func List() []Theme — returns themes in manifest-discovery order (sort by ID for determinism).
    • func Get(id string) (Theme, bool)
    • func Apply(cfg config.Config, themeID string) error
    • Private: discoverThemes() run once at package init; reads assets/*/theme.yaml.
  • theming_test.go
  • assets/<theme>/theme.yaml — five files (gruvbox, catppuccin, tokyonight, nord, dracula), each containing:
    display_name: "Gruvbox Dark"
    nvchad: "gruvbox"
    
  • assets/<theme>/starship.toml, zellij.kdl, yazi.toml, lazygit.yml — 20 static config files lifted verbatim from the corresponding write_*_config heredocs in lib/themes/<theme>.sh. Tool config paths on disk are:
    • starship.toml → ~/.config/starship.toml
    • zellij.kdl → ~/.config/zellij/config.kdl
    • yazi.toml → ~/.config/yazi/theme.toml
    • lazygit.yml → ~/.config/lazygit/config.yml

New — internal/subcommands/newproject/

  • newproject.go
    • func Run(cfg config.Config, project, board string) error.
  • newproject_test.go
  • assets/<board>.ino — six starter sketches (uno, mega, nano, esp32, esp32s3, esp8266) plus default.ino. Content lifted from lib/scaffold.sh heredocs.

New — internal/subcommands/doctor/

  • doctor.go
    • func Run(ctx context.Context, cfg config.Config, reg *module.Registry, runner platform.Runner, w io.Writer) (exitCode int)
  • doctor_test.go

Modified

  • internal/config/config.go + internal/config/defaults.yaml

    • Add:
      type Board struct { Name, FQBN, Template string `yaml:"name,fqbn,template"` }
      type Scaffolding struct { Boards []Board `yaml:"boards"` }
      
      (adjust tag syntax — yaml tags per field as with existing types).
    • Config.Scaffolding Scaffolding
    • merge clause: if len(over.Scaffolding.Boards) > 0 { out.Scaffolding = over.Scaffolding }.
    • Defaults: the 6 boards from lib/scaffold.sh FQBN_MAP with template set to the board name (so template: "uno" maps to internal/subcommands/newproject/assets/uno.ino).
  • internal/modules/neovim/neovim.go

    • Extract the chadrc render+write logic into an exported helper the theming package can call. Suggested signature: func RewriteChadrc(themeID string, themes []Theme) error (takes themes so the module doesn't depend on the theming package — avoid import cycle). Or simpler: func WriteChadrc(nvChadThemeName string) error — takes the already-resolved NvChad name, no theme-registry knowledge needed. The helper no-ops if ~/.config/nvim/lua/chadrc.lua doesn't exist.
  • internal/tui/state.go

    • Remove BuiltinThemes, remove NvChadTheme field from Theme, remove ThemeByID. Wizard takes a []Theme parameter or reads from theming.List() directly.
    • Option: delete tui.Theme entirely and have the wizard use theming.Theme directly. Weigh the import coupling: internal/tui already imports internal/config, and theming imports config too, so tui → theming is fine.
  • internal/tui/wizard/wizard.go

    • Populate the theme dropdown from theming.List().
  • cmd/archduino/main.go

    • Add three cobra.Command constructors: newThemeCmd(), newProjectCmd(), newDoctorCmd().
    • Register them on the root command alongside install.
    • In the install RunE, after engine.Run returns success, call theming.Apply(cfg, finalState.ThemeID) and surface any error.
    • buildRegistry no longer needs to receive themeID for neovim's chadrc — neovim's install path writes chadrc based on the ID threaded through neovim.New, and theming.Apply overwrites it afterward. Keeping the parameter is fine; it remains useful for the initial chadrc write.

Command surfaces

  • archduino theme → interactive huh-based picker listing themes; pre-selects cfg.ThemeOverrides["__active"] (or the first theme if unset). On confirm, calls theming.Apply.
  • archduino theme <id> → non-interactive; calls theming.Apply(cfg, id). Errors if unknown.
  • archduino theme list → prints <id> — <display_name> one per line to stdout.
  • archduino new <project> <board> → see data-flow below.
  • archduino doctor → see data-flow below.

Data Flow

theming.Apply

  1. Look up theme by ID; return error if unknown (message lists valid IDs).
  2. For each of the four tool configs under assets/<theme>/, read via embed.FS and write to its target path via platform.WriteUserFile with mode 0o644.
  3. If ~/.config/nvim/lua/chadrc.lua exists, call neovim.WriteChadrc(theme.NvChadTheme) to re-render it. If it doesn't exist, skip silently (user hasn't run install yet, or removed nvim config).
  4. Idempotently clear NvChad's theme cache: os.RemoveAll(~/.cache/nvim/base46) and os.RemoveAll(~/.local/share/nvim/lazy/base46/lua/base46/themes). Errors ignored — cache may not exist.
  5. Update the user's overlay file at ~/.config/archduino/overlay.yaml: load existing YAML (or empty), set theme_overrides.__active: <id>, write back via a YAML round-trip using gopkg.in/yaml.v3 decode-into-map-then-encode. First-run creates the file with the single key.

newproject.Run

  1. Validate project: non-empty, no / or \\, doesn't start with ..
  2. Look up board in cfg.Scaffolding.Boards. If absent, return an error whose message lists all valid board names, one per line.
  3. os.Stat(project); if it exists, return "project directory already exists". Do not overwrite.
  4. os.MkdirAll(project, 0o755).
  5. Determine .ino asset path: <board.Template>.ino, falling back to default.ino if the board-specific asset is missing. Write to <project>/<project>.ino via platform.WriteUserFile mode 0o644.
  6. Write <project>/sketch.yaml:
    default_fqbn: <FQBN>
    default_port: auto
    
  7. Write <project>/.clangd:
    CompileFlags:
      Add:
        - -xc++
        - -std=c++17
    
  8. Print a success line to stdout: Created <project>/ (fqbn: <FQBN>). Next: cd <project> && nvim <project>.ino.

doctor.Run

  1. For each module in reg.All(): call Detect(ctx), then Plan(ctx, state). Format output one row per module:
    • [✓] <id> — empty plan (converged).
    • [✗] <id> (N actions pending) — non-empty plan.
    • [!] <id> — detect failed: <err> — Detect/Plan returned error. Never propagate the error up; record and continue.
  2. Version drift checks (only if arduino-cli / wokwi-cli are found on PATH via the injected Runner):
    • arduino-cli version --format json → parse VersionString, compare to cfg.ArduinoCLI.Version. Print [✓] arduino-cli <ver> or [!] arduino-cli drift: got <X>, want <Y>.
    • wokwi-cli --version → parse first line's version-like token, compare to cfg.Renode.WokwiVersion. Same output format.
  3. Summary line: <converged>/<total> converged, <pending> need-install, <drift> drift, <errors> errors.
  4. Exit code: 0 if all modules converged AND no drift AND no errors; else 1. Return value from Run is the exit code; the cobra RunE wrapper calls os.Exit(code).

No network calls. No writes. Fast enough to run interactively (<5s on a typical install).

Error Handling

  • theming.Apply: first write error aborts. No rollback. User re-runs after fixing (partial state is a benign overlap of two themes' configs on disk until re-run). Overlay update happens last; if a config write fails, the overlay is not touched, so __active still reflects the previously-applied theme.
  • newproject.Run: refuses to overwrite an existing project directory. All other failures (mkdir, write) bubble up to the cobra layer and print via RunE's default error handling.
  • doctor.Run: never returns a Go error. Module failures are reported inline; only a programmer error (e.g., nil registry) would panic.

Testing

theming_test.go (all unit, tempdir-based $HOME)

  • TestListReadsManifests — asserts 5 themes discovered, each with non-empty DisplayName and NvChadTheme.
  • TestGetKnownAndUnknown — happy path + false return for bogus ID.
  • TestApplyWritesAllFourConfigs — t.Setenv("HOME", …); Apply(cfg, "dracula"); assert each of the 4 target files exists with non-empty content and the first line matches the asset's first line (robust to copy-paste drift in tests).
  • TestApplyUpdatesOverlayActive — after Apply, ~/.config/archduino/overlay.yaml contains theme_overrides.__active: dracula (round-trip through gopkg.in/yaml.v3).
  • TestApplyPreservesOtherOverlayKeys — seed overlay with extra_packages: [foo], run Apply, assert extra_packages still present after write.
  • TestApplyRewritesChadrcIfPresent — seed ~/.config/nvim/lua/chadrc.lua with gruvbox; Apply dracula; assert file now contains theme = "dracula".
  • TestApplyNoChadrcIsFine — no chadrc on disk; Apply succeeds.
  • TestApplyUnknownThemeErrors.

newproject_test.go

  • TestNewCreatesProject — config with uno board; project dir doesn't exist; Run(cfg, "blinky", "uno") in a tempdir (chdir); assert blinky/blinky.ino + sketch.yaml (contains arduino:avr:uno) + .clangd exist.
  • TestNewRefusesExistingDir — pre-create the target dir; Run returns error; dir untouched.
  • TestNewFallsBackToDefaultTemplate — config board entry whose .template points at an asset that doesn't exist (use a made-up board with template: "xyz"); Run succeeds and the generated .ino matches default.ino content.
  • TestNewUnknownBoardErrors — error message lists all valid board names.
  • TestNewRejectsSlashInProjectName.

doctor_test.go

  • TestDoctorAllConvergedExitsZero — single stub module returning empty Plan; runner that makes version commands not-found (exec: "arduino-cli": executable file not found in $PATH-style error — skip version checks cleanly). Exit code 0.
  • TestDoctorDivergedExitsOne — single stub module returning a Plan with actions; exit code 1.
  • TestDoctorDetectErrorIsReported — module whose Detect returns an error; doctor prints [!] and exits 1.
  • TestDoctorArduinoCLIDriftIsReported — injected Runner returns a different version for arduino-cli version --format json; exit code 1; output contains "drift".
  • TestDoctorMissingBinariesSkipVersionCheck — runner returns "not found"; no false drift report; exit reflects module status only.

install-command integration (smoke-level)

  • Existing install tests continue to pass; add a unit test that stubs theming.Apply and asserts install calls it after a successful engine.Run with the wizard's ThemeID.

No integration tests against real starship/zellij/arduino-cli/wokwi binaries — Plan 4d (CI) introduces those.

Non-Goals

  • Per-user theme creation / custom palettes — pinned to the five built-in themes.
  • Live reload: users must restart their shell, zellij session, etc. manually (bash also required this).
  • archduino doctor --fix mode — reserved for a future plan, possibly merged with Plan 4b's reinstall/reset.
  • Version-drift remediation — doctor only reports.
  • archduino new support for non-Arduino-CLI ecosystems (PlatformIO, etc.).

Open Risks / Watchpoints

  • NvChad chadrc extraction: pulling the render-and-write out of neovim.Module.Plan's closures into a free function requires care — currently that closure is built inside Plan. Acceptable fix: factor a writeChadrc(cfg string, nvChadTheme string) error free function in the neovim package, have both the Plan action and the new WriteChadrc export use it.
  • tui.Theme retirement: wizard test coverage is decent but BuiltinThemes is referenced in internal/tui/state_test.go; removing the symbol requires updating those tests. Grep before deleting.
  • Overlay YAML round-trip: gopkg.in/yaml.v3's node-based decoder preserves comments and order; using map[string]any decode/encode will lose both. Acceptable for v1 (the user's overlay is config, not hand-commented prose); document the behavior in the theming.Apply godoc.
  • Doctor version parsing: arduino-cli version --format json shape is stable, but wokwi-cli --version's output format is upstream-controlled. A tiny regex v?\d+\.\d+\.\d+ covers the common case; if it grows fragile, fall back to byte-for-byte compare with cfg.Renode.WokwiVersion.
  • Theme-applied-during-install is a behavior change vs. Bash. It means a fresh install writes tool configs. If the user had pre-existing starship/zellij configs they liked, they get overwritten. Mitigation: install preserves a .bak.<timestamp> copy before the theme apply step. Document clearly in the install command's help text.