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