From 6318362e9913af1b6f6a0170c9d21c55af9450e5 Mon Sep 17 00:00:00 2001 From: MDietermann Date: Tue, 14 Apr 2026 08:03:17 +0200 Subject: [PATCH] docs(specs): add Plan 3a install-modules design --- ...026-04-14-plan3a-install-modules-design.md | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-14-plan3a-install-modules-design.md diff --git a/docs/superpowers/specs/2026-04-14-plan3a-install-modules-design.md b/docs/superpowers/specs/2026-04-14-plan3a-install-modules-design.md new file mode 100644 index 0000000..49c2f64 --- /dev/null +++ b/docs/superpowers/specs/2026-04-14-plan3a-install-modules-design.md @@ -0,0 +1,138 @@ +# 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 `. + - 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//` with `.go` + `_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 ` 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.