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

139 lines
8.9 KiB
Markdown

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