139 lines
8.9 KiB
Markdown
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.
|