Files
Archduino/docs/superpowers/specs/2026-04-14-plan3a-install-modules-design.md
2026-04-14 08:03:17 +02:00

8.9 KiB

Plan 3a — Install Modules (Simple/Medium Tier) Design

Status: Approved 2026-04-14 Scope: Port 10 of the 13 Bash install modules to Go. Plan 3b (neovim, renode, scaffold) is a separate brainstorm.

Goal

Port the 10 simple/medium lib/*.sh installer modules into Go packages under internal/modules/, each implementing the module.Module contract (Detect / Plan / Apply / Verify). Every module is unit-tested against an injected fake platform.Runner. Along the way, add the platform helpers these modules need (AUR, download, extract, git-clone, text-block writer) and fix the wizard renderPlan UI-blocking watchpoint flagged at the end of Plan 2.

Architecture

Foundation tasks land first, then modules in simplest-first order:

  1. Wizard cache fix — cache Detect+Plan results per module; invalidate on selection toggle. One small change, but it unblocks every downstream module from stalling the UI.
  2. Platform helpers — AURHelper (yay), Download, Extract, gitclient, textblock. Each gets its own file, each gets TDD against a fake Runner (except extract which uses real tempdirs with fixture archives).
  3. Ten modules in this order: packages → gittools(lazygit) → starship → yazi → zsh → fonts → zellij → themes → arduinocli → shellconfig.

All module inputs (package lists, versions, URLs, plugin repos, shell-config blocks) are typed fields on config.Config populated from the embedded defaults.yaml. Overlay override plumbing already exists from Plan 1. Modules take their inputs as struct fields at construction time — no globals.

Tech Stack

Go, existing internal/platform + internal/module + internal/engine + internal/tui packages. No new external dependencies expected beyond what Plan 1/2 pulled in.

Files

New — platform helpers

  • internal/platform/aur.go + aur_test.go

    • AURHelper interface, Yay implementation.
    • EnsureInstalled(ctx) error — bootstraps yay from AUR via git clone https://aur.archlinux.org/yay.git /tmp/... && makepkg -si --noconfirm when missing.
    • Install(ctx, pkgs []string) error — yay -S --needed --noconfirm <pkgs...>.
    • Takes a Runner and a PackageManager (to check if yay itself is installed).
  • internal/platform/download.go + download_test.go

    • Download(ctx, url, destPath string, mode os.FileMode) error.
    • Atomic-ish: write to destPath+".tmp" then rename. Clean up on error.
    • Tested with httptest.Server.
  • internal/platform/extract.go + extract_test.go

    • ExtractTarGz(ctx, archivePath, destDir string) error.
    • ExtractZip(ctx, archivePath, destDir string) error.
    • Tested against real fixture archives in testdata/.
  • internal/platform/git.go + git_test.go

    • GitClient struct taking a Runner.
    • Clone(ctx, url, dest string) error.
    • EnsureRepo(ctx, url, dest string) error — clone if dest/.git missing, otherwise noop.
  • internal/platform/textblock.go + textblock_test.go

    • EnsureBlock(path, marker, content string) error — writes or replaces a block delimited by # >>> marker >>> / # <<< marker <<< markers. Creates the file if missing.
    • RemoveBlock(path, marker string) error.
    • HasBlock(path, marker string) (bool, error).
    • Table-driven tests: insert, replace, remove, preserve surrounding content, missing-file, empty-file.

New — modules

Each lives in its own package under internal/modules/<name>/ with <name>.go + <name>_test.go.

  • packages — installs base pacman package list from cfg.Packages.Base.
  • gittools — installs lazygit (pacman). Named gittools to avoid collision with Go stdlib / pacman git.
  • starship — installs starship (pacman).
  • yazi — installs yazi (pacman).
  • zsh — installs zsh (pacman) + clones oh-my-zsh + configured plugins via gitclient.
  • fonts — installs AUR fonts (e.g. ttf-jetbrains-mono-nerd) via AURHelper. First action is EnsureYayInstalled if needed.
  • zellij — installs zellij (pacman) + downloads config.
  • themes — copies theme files from source directory (bundled under lib/themes/ or overridable via cfg.Themes.Source).
  • arduinocli — downloads arduino-cli tarball, extracts to ~/.local/bin, runs arduino-cli core install <cores...> from cfg.ArduinoCLI.Cores.
  • shellconfig — writes marker-delimited blocks into ~/.zshrc, ~/.bashrc, etc., using textblock.EnsureBlock. Blocks defined in cfg.ShellConfig.Blocks.

Modified

  • internal/config/config.go + internal/config/defaults.yaml — add typed fields:
    • Packages.Base []string
    • Fonts []string
    • ArduinoCLI{Version string, URL string, Cores []string}
    • Starship{Version string} (reserved; pacman install doesn't need it but overlay may)
    • Zsh{OhMyZshRepo string, Plugins []struct{Name, Repo string}}
    • Zellij{ConfigURL string}
    • Themes{Source string} (defaults to embedded lib/themes/)
    • ShellConfig{Blocks []struct{Name, File, Content string}}
  • internal/tui/wizard/wizard.go — cache Detect+Plan per module; invalidate on selection toggle.
  • Module registry (wherever cmd/archduino/install.go currently registers hello) — register the 10 new modules instead of / alongside hello.

Module Contract Details

All modules follow the same shape:

  • Detect — pure read. PackageManager.IsInstalled per pkg, os.Stat per file, textblock.HasBlock per marker. Returns a module-private state struct. Must be fast (wizard caches results, but Detect still shouldn't shell out more than necessary).
  • Plan — given current state, returns Plan{Actions: []Action}. Empty actions = already converged (idempotent no-op). No side effects.
  • Apply — executes each action sequentially, reporting progress via Reporter.Progress(id, i, total, description).
  • Verify — re-runs Detect and asserts convergence. Returns a descriptive error on mismatch.

Error semantics from the engine (--halt-on-error flag, sibling-on-failure continuation) apply unchanged.

Module archetypes

  1. Pacman-only (packages, gittools, starship, yazi, zellij base, themes deps if any): Detect = IsInstalled per pkg; Plan = install missing; Apply = Pacman.Install; Verify = all installed.
  2. AUR (fonts): same as pacman-only but via AURHelper. Plan prepends an EnsureYay action if yay is not installed.
  3. Download + extract (arduinocli): Detect = probe binary + --version; Plan = download + extract + symlink + arduino-cli core install; Verify = version string matches.
  4. Git-clone configs (zsh oh-my-zsh + plugins, zellij config fetch if URL-based, themes if sourced from repo): Detect = check .git or file presence; Plan = gitclient.EnsureRepo or Download; Verify = path exists.
  5. Text-block writers (shellconfig): Detect = textblock.HasBlock per block; Plan = textblock.EnsureBlock; Verify = re-scan.

Testing Strategy

Per-module tests (all unit, all fake-Runner-based):

  1. Detect with nothing installed → absent state.
  2. Detect with everything installed → present state.
  3. Plan(absent) → non-empty actions matching config inputs.
  4. Plan(present) → empty actions (idempotent no-op).
  5. Apply → records correct shell commands against fake Runner.
  6. Verify after Apply → passes when fake reports success.

Platform helper tests:

  • aur_test.go — yay bootstrap path (fake Runner sees git clone + makepkg -si), install path, already-installed short-circuit.
  • download_test.go — httptest.Server for happy path + 404 + mid-download error cleanup.
  • extract_test.go — real tar/zip fixtures in testdata/ extracted into t.TempDir().
  • git_test.go — fake Runner for clone-if-missing, skip-if-present.
  • textblock_test.go — table-driven: insert new, replace existing, remove, preserve surrounding content, missing-file, empty-file.

Wizard caching test: assert Detect is called once per module regardless of redraw count.

No integration tests that touch real pacman, real AUR, or the real network. That's Plan 4 (CI with Arch container). Manual smoke after plan completes.

Non-Goals

  • Plan 3b modules (neovim, renode, scaffold) — separate brainstorm.
  • Subcommands beyond install (theme/new/doctor) — Plan 4.
  • bootstrap.sh, goreleaser, CI — Plan 4.
  • Debian/Fedora PackageManager implementations — Plan 4+.
  • Integration tests against real package managers — Plan 4.

Open Risks / Watchpoints

  • AUR bootstrap fragility — if makepkg -si --noconfirm prompts despite --noconfirm (sudo password mid-build), the fake-Runner tests won't catch it. Manual smoke on fonts module is essential.
  • arduino-cli URL drift — hardcoded version in defaults.yaml will go stale. Acceptable for v1; doctor subcommand in Plan 4 can warn.
  • Theme file source — if themes ship bundled via Go embed, we need to decide that at module time. Default assumption: copy from lib/themes/ on disk (same behavior as bash); switch to embed only if it's a clear win.