diff --git a/docs/superpowers/plans/2026-04-14-force-reapply.md b/docs/superpowers/plans/2026-04-14-force-reapply.md new file mode 100644 index 0000000..9ffc09c --- /dev/null +++ b/docs/superpowers/plans/2026-04-14-force-reapply.md @@ -0,0 +1,726 @@ +# Force Reapply Implementation Plan (Plan 4b) + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add `archduino install --force` that reapplies every selected module unconditionally — new `PlanForce` method on `module.Module`, new `Reinstall` helpers on `platform.Pacman`/`platform.Yay`, engine + wizard + CLI wiring. + +**Architecture:** Interface-level `PlanForce(ctx) (Plan, error)` gives each module an explicit "redo everything" path. Engine branches on `RunOptions.Force` to skip `Detect` and call `PlanForce`. Wizard mirrors that branch in its preview cache. Package-only modules reinstall without `--needed`; config-writing modules rewrite managed files. + +**Tech Stack:** Go, existing cobra + bubbletea stack. No new dependencies. + +**Spec:** `docs/superpowers/specs/2026-04-14-force-reapply-design.md` + +--- + +## File Structure + +**Modified (no new files):** +- `internal/platform/pacman.go` + `_test.go` — add `Reinstall` +- `internal/platform/aur.go` + `_test.go` — add `Reinstall` +- `internal/module/module.go` — add `PlanForce` to interface +- `internal/engine/engine.go` + `_test.go` — add `RunOptions.Force` +- `internal/tui/state.go` — add `WizardState.Force` +- `internal/tui/wizard/wizard.go` + `_test.go` — force-aware preview +- `cmd/archduino/main.go` — add `--force` flag +- 13 module files + their tests — implement `PlanForce` + +--- + +## Task 1: Platform layer — `Reinstall` on Pacman and Yay + +**Files:** +- Modify: `internal/platform/pacman.go` +- Modify: `internal/platform/pacman_test.go` +- Modify: `internal/platform/aur.go` +- Modify: `internal/platform/aur_test.go` + +- [ ] **Step 1: Inspect existing Install methods** + +Read the existing `Install` methods in `pacman.go` and `aur.go` to know their exact command construction. The new `Reinstall` must be identical except for the `--needed` flag being absent. + +```bash +grep -n -A 15 "func.*Install" internal/platform/pacman.go internal/platform/aur.go +``` + +Read the test files to understand how commands are asserted (likely via `FakeRunner` recording executed commands). + +- [ ] **Step 2: Write failing test for `Pacman.Reinstall`** + +Add to `internal/platform/pacman_test.go`: + +```go +func TestPacmanReinstall(t *testing.T) { + fr := &FakeRunner{} + p := NewPacman(fr) + if err := p.Reinstall(context.Background(), []string{"git", "curl"}); err != nil { + t.Fatalf("Reinstall: %v", err) + } + if len(fr.Calls) != 1 { + t.Fatalf("expected 1 call, got %d", len(fr.Calls)) + } + got := fr.Calls[0] + // Must include -S and --noconfirm but NOT --needed. + if !containsAll(got.Args, "-S", "--noconfirm") { + t.Errorf("expected -S and --noconfirm in args; got %v", got.Args) + } + for _, a := range got.Args { + if a == "--needed" { + t.Errorf("Reinstall must not pass --needed; got args %v", got.Args) + } + } + if !containsAll(got.Args, "git", "curl") { + t.Errorf("expected packages git, curl in args; got %v", got.Args) + } +} +``` + +Adjust `containsAll` / `FakeRunner` / `Calls` field names to match what the existing tests already use. Read `pacman_test.go` first to confirm the exact helper names — do NOT invent names. + +- [ ] **Step 3: Run test, verify it fails** + +```bash +cd /home/marvin/git-clones/ArduinoArchSetup +go test ./internal/platform/ -run TestPacmanReinstall -v +``` + +Expected: FAIL with `undefined: Pacman.Reinstall` or similar. + +- [ ] **Step 4: Implement `Pacman.Reinstall`** + +Add to `internal/platform/pacman.go`, mirroring the existing `Install`: + +```go +// Reinstall runs `pacman -S --noconfirm pkg...` without --needed so pacman +// unconditionally reinstalls each package. Used by force-reapply. +func (p Pacman) Reinstall(ctx context.Context, pkgs []string) error { + if len(pkgs) == 0 { + return nil + } + args := append([]string{"pacman", "-S", "--noconfirm"}, pkgs...) + return p.runner.RunSudo(ctx, args[0], args[1:]...) +} +``` + +Adjust the runner method (`RunSudo` / `Run`) to whatever the existing `Install` uses. Read `Install` first. + +- [ ] **Step 5: Run test, verify it passes** + +```bash +go test ./internal/platform/ -run TestPacmanReinstall -v +``` + +Expected: PASS. + +- [ ] **Step 6: Repeat Steps 2–5 for `Yay.Reinstall`** + +Add `TestYayReinstall` to `internal/platform/aur_test.go` (parallel structure; `yay` command instead of `pacman`). Then implement `Yay.Reinstall` in `aur.go`. Run `go test ./internal/platform/ -run TestYayReinstall -v`, verify it passes. + +- [ ] **Step 7: Commit** + +```bash +git add internal/platform/pacman.go internal/platform/pacman_test.go \ + internal/platform/aur.go internal/platform/aur_test.go +git commit -m "feat(platform): add Reinstall helpers to Pacman and Yay" +``` + +No Co-Authored-By trailer. + +--- + +## Task 2: Extend Module interface + stub all 13 modules + +Adding `PlanForce` to the interface breaks compilation until every module implements it. This task adds the interface method and a *stub* implementation on every module that returns `module.Plan{}, errors.New("PlanForce not implemented")`. Compilation stays green, engine is not wired yet, and each module's real implementation lands in a later task with its own test. + +**Files:** +- Modify: `internal/module/module.go` +- Modify: 13 module files listed below + +- [ ] **Step 1: Add `PlanForce` to the interface** + +Edit `internal/module/module.go`. In the `Module` interface block: + +```go +type Module interface { + Meta() Meta + Detect(ctx context.Context) (State, error) + Plan(ctx context.Context, current State) (Plan, error) + PlanForce(ctx context.Context) (Plan, error) + Apply(ctx context.Context, plan Plan, r Reporter) error + Verify(ctx context.Context) error +} +``` + +Add a doc comment above `PlanForce`: + +```go + // PlanForce returns a plan that reapplies the module unconditionally, + // ignoring current detected state. Used by `archduino install --force`. + // Implementations should produce a plan equivalent to a from-scratch + // install — reinstall packages without pacman's --needed guard and + // rewrite managed config files. +``` + +- [ ] **Step 2: Verify compile fails** + +```bash +cd /home/marvin/git-clones/ArduinoArchSetup +go build ./... 2>&1 | head -20 +``` + +Expected: compile errors like `*starship.Module does not implement module.Module (missing method PlanForce)` for every module. + +- [ ] **Step 3: Add stub `PlanForce` to every module** + +For each of the 13 module files below, append this stub at the bottom of the file (replace `Module` receiver type if the file uses something different — read each file first to confirm): + +```go +func (m Module) PlanForce(ctx context.Context) (module.Plan, error) { + return module.Plan{}, errors.New("PlanForce not implemented") +} +``` + +Ensure `errors` is imported. The module files: + +1. `internal/modules/packages/packages.go` +2. `internal/modules/gittools/gittools.go` +3. `internal/modules/fonts/fonts.go` +4. `internal/modules/renode/renode.go` +5. `internal/modules/hello/hello.go` +6. `internal/modules/starship/starship.go` +7. `internal/modules/yazi/yazi.go` +8. `internal/modules/zsh/zsh.go` +9. `internal/modules/zellij/zellij.go` +10. `internal/modules/shellconfig/shellconfig.go` +11. `internal/modules/themes/themes.go` +12. `internal/modules/neovim/neovim.go` +13. `internal/modules/arduinocli/arduinocli.go` + +- [ ] **Step 4: Verify compile + existing tests pass** + +```bash +go build ./... +go test ./... +``` + +Expected: compile clean, all existing tests green. The new stubs aren't exercised yet. + +- [ ] **Step 5: Commit** + +```bash +git add internal/module/module.go internal/modules/ +git commit -m "feat(module): add PlanForce to Module interface with stubs" +``` + +--- + +## Task 3: Engine — `RunOptions.Force` + branch + +**Files:** +- Modify: `internal/engine/engine.go` +- Modify: `internal/engine/engine_test.go` + +- [ ] **Step 1: Write failing test** + +In `internal/engine/engine_test.go`, add: + +```go +func TestRunForceSkipsDetectAndCallsPlanForce(t *testing.T) { + fake := &fakeModule{ + id: "fake", + planForce: module.Plan{Actions: []module.Action{{ + Description: "forced action", + Exec: func(ctx context.Context) error { return nil }, + }}}, + } + reg := module.NewRegistry() + if err := reg.Register(fake); err != nil { + t.Fatal(err) + } + var buf bytes.Buffer + sum, err := engine.Run(context.Background(), reg, engine.RunOptions{ + Selected: []string{"fake"}, + Force: true, + Reporter: engine.NewPlainReporter(&buf), + }) + if err != nil { + t.Fatalf("Run: %v", err) + } + if fake.detectCalls != 0 { + t.Errorf("Detect should not be called under Force; got %d calls", fake.detectCalls) + } + if fake.planForceCalls != 1 { + t.Errorf("PlanForce should be called once; got %d", fake.planForceCalls) + } + if fake.planCalls != 0 { + t.Errorf("Plan should not be called under Force; got %d", fake.planCalls) + } + if sum.Applied != 1 { + t.Errorf("expected Applied=1, got %d", sum.Applied) + } +} +``` + +`fakeModule` already exists in `engine_test.go`; extend it with: +- `planForce module.Plan` +- `planForceCalls int` +- Implement `PlanForce(ctx) (module.Plan, error)` that increments the counter and returns `planForce`. +- If `detectCalls`/`planCalls` don't already exist on the fake, add them too (they may already be there — read the file first). + +Read `engine_test.go` before editing to confirm the fake's field layout. + +- [ ] **Step 2: Run test, verify it fails** + +```bash +go test ./internal/engine/ -run TestRunForceSkipsDetectAndCallsPlanForce -v +``` + +Expected: FAIL — likely `unknown field Force in RunOptions` at compile time. + +- [ ] **Step 3: Add `Force` to `RunOptions`** + +In `internal/engine/engine.go`, find the `RunOptions` struct and add: + +```go +// Force, when true, skips Detect and calls PlanForce on every module +// so each one produces a from-scratch plan regardless of current state. +Force bool +``` + +- [ ] **Step 4: Branch `Run` on `Force`** + +Find the per-module loop inside `Run`. The current shape is roughly: + +```go +cur, err := mod.Detect(ctx) +if err != nil { /* handle */ } +plan, err := mod.Plan(ctx, cur) +``` + +Replace with: + +```go +var ( + plan module.Plan + err error +) +if opts.Force { + plan, err = mod.PlanForce(ctx) +} else { + cur, derr := mod.Detect(ctx) + if derr != nil { + // preserve existing Detect error handling verbatim + err = derr + } else { + plan, err = mod.Plan(ctx, cur) + } +} +``` + +Read the current block first and preserve its error-handling shape (reporter calls, halt-on-error, summary bookkeeping). Do not inline a different handling style. + +- [ ] **Step 5: Run test, verify it passes** + +```bash +go test ./internal/engine/ -run TestRunForceSkipsDetectAndCallsPlanForce -v +go test ./internal/engine/ -v +``` + +Expected: both green. + +- [ ] **Step 6: Commit** + +```bash +git add internal/engine/engine.go internal/engine/engine_test.go +git commit -m "feat(engine): add RunOptions.Force to skip Detect and call PlanForce" +``` + +--- + +## Task 4: Package-only modules — real `PlanForce` + +Package-only modules' `PlanForce` produces a plan that calls `Reinstall` on the full package list. + +**Modules covered:** `packages`, `gittools`, `fonts`, `renode`, `hello`. + +**Files:** for each module, both `.go` and `_test.go`. + +For the four real modules (packages, gittools, fonts, renode), follow the same pattern. Do this **one module at a time** (5 cycles of write-test → verify-fail → implement → verify-pass → next). For `hello`, since it's a placeholder, make `PlanForce` return a fixed non-empty plan (see below). + +### Pattern (use for each of packages / gittools / fonts / renode) + +- [ ] **Step 1: Write failing `TestPlanForce` in the module's `_test.go`** + +Template (adapt field names to the module under test — read the existing `TestPlan` test for each module to see how the fake pacman/yay is wired): + +```go +func TestPlanForceReinstallsAllPackages(t *testing.T) { + fp := &FakePacman{installed: map[string]bool{"git": true, "curl": true}} + m := New([]string{"git", "curl"}, fp) + plan, err := m.PlanForce(context.Background()) + if err != nil { + t.Fatalf("PlanForce: %v", err) + } + if plan.IsEmpty() { + t.Fatal("force plan must not be empty even when packages are installed") + } + // Execute to confirm it calls Reinstall, not Install. + for _, a := range plan.Actions { + if err := a.Exec(context.Background()); err != nil { + t.Fatalf("Exec: %v", err) + } + } + if fp.ReinstallCalls != 1 { + t.Errorf("expected 1 Reinstall call, got %d", fp.ReinstallCalls) + } +} +``` + +If `FakePacman` doesn't already have a `ReinstallCalls` counter and a `Reinstall` method, add them to the fake first (read the fake in the same test file — most modules share a local fake or use a common one in `internal/platform`). + +- [ ] **Step 2: Run test, verify it fails** + +```bash +go test ./internal/modules// -run TestPlanForce -v +``` + +Expected: FAIL — the stub returns an error. + +- [ ] **Step 3: Replace stub with real `PlanForce`** + +In `.go`, replace the stub with (adjust receiver/field names): + +```go +func (m Module) PlanForce(ctx context.Context) (module.Plan, error) { + if len(m.pkgs) == 0 { + return module.Plan{}, nil + } + pkgs := append([]string{}, m.pkgs...) + return module.Plan{Actions: []module.Action{{ + Description: "pacman -S (force) " + strings.Join(pkgs, ", "), + Kind: module.ActionPackageInstall, + Exec: func(ctx context.Context) error { return m.pm.Reinstall(ctx, pkgs) }, + }}}, nil +} +``` + +Ensure the module's stored `pm` field (or equivalent) has the `Reinstall` method available — it will, because Task 1 added it to both `Pacman` and `Yay`, and the fake was extended in Step 1. + +- [ ] **Step 4: Run test, verify it passes** + +```bash +go test ./internal/modules// -v +``` + +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add internal/modules// +git commit -m "feat(): implement PlanForce to reinstall all packages" +``` + +### `hello` module + +The `hello` module is a placeholder. Implement `PlanForce` as: + +```go +func (m Module) PlanForce(ctx context.Context) (module.Plan, error) { + return module.Plan{Actions: []module.Action{{ + Description: "hello force reapply", + Exec: func(ctx context.Context) error { return nil }, + }}}, nil +} +``` + +Add a test that just asserts the plan is non-empty. Same commit cadence as above. + +- [ ] **Step 6: Complete all 5 modules before moving on** + +After each of packages / gittools / fonts / renode / hello has its own commit, move to Task 5. Do NOT batch them into one commit. + +--- + +## Task 5: Hybrid modules — `starship`, `yazi` + +These modules install packages AND (optionally) write configs. Their existing `Plan` already produces a plan that may include both; `PlanForce` should produce both unconditionally. + +For each of `starship` and `yazi`: + +- [ ] **Step 1: Read the existing `Plan` to see what it does** + +```bash +grep -n -A 30 "func.*Module.*Plan\b" internal/modules/starship/starship.go +grep -n -A 30 "func.*Module.*Plan\b" internal/modules/yazi/yazi.go +``` + +Identify the branches: package install vs. config write (if any). + +- [ ] **Step 2: Write failing `TestPlanForce`** + +The test must arrange a "fully satisfied" state (packages installed, any config files already present) and assert the force plan is non-empty AND its actions reinstall the packages (via Reinstall call counter) AND rewrite the config files (via a writefile call counter or file presence assertion). + +Use the same pattern as Task 4 for the package side. For the config side, use whatever the module's existing tests use to assert config writes (likely `WriteFile` mock or a tempdir). + +- [ ] **Step 3: Run test, verify it fails** + +```bash +go test ./internal/modules// -run TestPlanForce -v +``` + +- [ ] **Step 4: Replace stub with real `PlanForce`** + +```go +func (m Module) PlanForce(ctx context.Context) (module.Plan, error) { + var actions []module.Action + if len(m.pkgs) > 0 { + pkgs := append([]string{}, m.pkgs...) + actions = append(actions, module.Action{ + Description: "pacman -S (force) " + strings.Join(pkgs, ", "), + Kind: module.ActionPackageInstall, + Exec: func(ctx context.Context) error { return m.pm.Reinstall(ctx, pkgs) }, + }) + } + // Any config-write actions the module normally conditions on Detect: + actions = append(actions, /* unconditional config-write actions */ ...) + return module.Plan{Actions: actions}, nil +} +``` + +Fill in the config-write actions by lifting them from `Plan` but stripping the "already exists" short-circuit. Reuse the same helper functions (e.g. `writeConfig`) that `Plan` uses. + +- [ ] **Step 5: Run test, verify it passes, commit** + +```bash +go test ./internal/modules// -v +git add internal/modules// +git commit -m "feat(): implement PlanForce (reinstall + rewrite config)" +``` + +Repeat for both modules, one commit each. + +--- + +## Task 6: Config-writing simple modules — `zsh`, `zellij`, `shellconfig`, `themes` + +Same pattern as Task 5 — read existing `Plan`, lift its actions, strip conditionals. Do one module per commit. + +For each of `zsh`, `zellij`, `shellconfig`, `themes`: + +- [ ] **Step 1: Write failing `TestPlanForce`** — satisfied-system fixture → assert non-empty plan with all the module's write/install actions. +- [ ] **Step 2: Run test, verify it fails.** +- [ ] **Step 3: Replace stub with real implementation.** +- [ ] **Step 4: Run test, verify it passes.** +- [ ] **Step 5: Commit** with message `feat(): implement PlanForce`. + +Module-specific notes: + +- **`shellconfig`** has no packages; `PlanForce` only produces file-write actions. +- **`themes`** writes under `~/.config/archduino/overlay.yaml` — force rewrites the overlay's theme fields unconditionally. +- **`zsh`** clones plugins (oh-my-zsh, etc.) via `git`; `PlanForce` should clone even if the target already exists — add a pre-step that removes the directory, or use the `git` helper's `ForceClone` if one exists (grep `internal/platform/git.go`; if missing, note it as a concern and fall back to rm-then-clone via an action that uses `os.RemoveAll` before the clone). + +--- + +## Task 7: Config-writing complex modules — `neovim`, `arduinocli` + +Same pattern as Task 6, but these modules have multiple sub-steps (neovim: clone NvChad, write chadrc, run post-install; arduinocli: install binary, update index, install cores, install libraries). + +For each module: + +- [ ] **Step 1: Enumerate the existing `Plan`'s sub-actions.** Every conditional "already done" branch in `Plan` becomes an unconditional branch in `PlanForce`. +- [ ] **Step 2: Write failing `TestPlanForce`** — satisfied-system fixture → assert ALL sub-actions are present (count-based is fine if exact equality is fragile). +- [ ] **Step 3: Run test, verify it fails.** +- [ ] **Step 4: Replace stub with real implementation.** +- [ ] **Step 5: Run test, verify it passes.** +- [ ] **Step 6: Commit** with message `feat(): implement PlanForce`. + +**Neovim specifics:** +- If `NvChad` is already cloned, `PlanForce` should either `git pull` + rewrite `chadrc.lua` OR `rm -rf && git clone`. Pick the simpler: rewrite `chadrc.lua` unconditionally, do NOT delete the existing clone. Users' local NvChad modifications shouldn't be wiped by `--force`. Document this in a code comment. + +**arduinocli specifics:** +- `PlanForce` should re-download the binary tarball, re-run `arduino-cli core update-index`, and re-install every configured core/library regardless of current installation. Cores install is idempotent at the arduino-cli layer; no issue. + +--- + +## Task 8: TUI wizard — force-aware preview + +**Files:** +- Modify: `internal/tui/state.go` +- Modify: `internal/tui/wizard/wizard.go` +- Modify: `internal/tui/wizard/wizard_test.go` + +- [ ] **Step 1: Add `Force bool` to `WizardState`** + +In `internal/tui/state.go`, find the `WizardState` struct (or wherever the wizard's global state lives) and add a `Force bool` field with a doc comment: + +```go +// Force mirrors the --force CLI flag: when true, the wizard's per-module +// plan preview uses PlanForce instead of Detect + Plan, and the header +// shows a "Force mode" banner. +Force bool +``` + +If `WizardState` is constructed in a factory (e.g., `NewWizardState(cfg, reg)`), leave the constructor signature alone — the flag is set by the CLI after construction. + +- [ ] **Step 2: Write failing test for wizard preview under force** + +In `internal/tui/wizard/wizard_test.go`, add a test that dispatches the wizard's plan-preview code path with `Force: true` on a fake module whose `Detect` panics (proving it's not called) and whose `PlanForce` returns a non-empty plan. + +The exact test shape depends on how the wizard currently tests previews — read the existing preview tests before writing. Bare minimum: + +```go +func TestWizardPreviewUsesPlanForceWhenForceSet(t *testing.T) { + mod := &fakeModule{ + planForce: module.Plan{Actions: []module.Action{{Description: "forced"}}}, + detect: func() (module.State, error) { t.Fatal("Detect should not be called"); return nil, nil }, + } + // ... drive the preview path and assert the displayed plan is the PlanForce output +} +``` + +- [ ] **Step 3: Run test, verify it fails** + +```bash +go test ./internal/tui/wizard/ -run TestWizardPreviewUsesPlanForceWhenForceSet -v +``` + +- [ ] **Step 4: Branch the preview cache on `Force`** + +In `internal/tui/wizard/wizard.go`, find the `planCache` / preview logic (Plan 3a Task 1 added this — search for `planCache` or `Detect` call sites inside the wizard). Add a branch: + +```go +if state.Force { + plan, err = mod.PlanForce(ctx) +} else { + cur, derr := mod.Detect(ctx) + if derr != nil { /* existing error path */ } + plan, err = mod.Plan(ctx, cur) +} +``` + +Also: the cache key must include `force` so a force run doesn't reuse a non-force cached plan. If the cache is keyed by module ID only today, extend it to `(moduleID, force)`. + +- [ ] **Step 5: Add a header banner** + +Find the wizard's view/render code (likely a `View()` method). When `state.Force`, prepend a banner line to the rendered output: + +```go +if state.Force { + sb.WriteString(styles.Warning.Render("⚠ Force mode: all selected modules will be reapplied")) + sb.WriteString("\n\n") +} +``` + +Use whatever Lipgloss style the codebase already has for warnings (grep for `Warning`, `warnStyle`, or similar). If none exists, use a plain bold-red or bold string — do NOT create a new style module just for this. + +- [ ] **Step 6: Run test, verify it passes + rest of wizard tests** + +```bash +go test ./internal/tui/wizard/ -v +go test ./internal/tui/ -v +``` + +Expected: all green. + +- [ ] **Step 7: Commit** + +```bash +git add internal/tui/state.go internal/tui/wizard/ +git commit -m "feat(tui): wizard uses PlanForce preview when Force is set" +``` + +--- + +## Task 9: CLI — `--force` flag + +**Files:** +- Modify: `cmd/archduino/main.go` + +- [ ] **Step 1: Add the flag** + +In `newInstallCmd`, alongside the other flag declarations (`noTUI`, `dryRun`, `halt`, `noSudo`, `only`, `skip`, `overlayPath`), add: + +```go +var force bool +// ... +cmd.Flags().BoolVar(&force, "force", false, + "Reapply every selected module, ignoring current state") +``` + +- [ ] **Step 2: Propagate to engine and wizard** + +In the `RunE` body: +- Where `engine.RunOptions{…}` is constructed (there are currently two call sites: no-TUI path and TUI path — both must set `Force: force`). +- Where `tui.NewWizardState(cfg, reg)` is called: after construction, set `initial.Force = force`. + +- [ ] **Step 3: Smoke-test** + +```bash +cd /home/marvin/git-clones/ArduinoArchSetup +go build ./cmd/archduino +./archduino install --help | grep -A 2 force +./archduino install --force --dry-run --no-tui --no-sudo 2>&1 | head -20 +``` + +Expected: `--help` shows the new flag; a dry-run with `--force` completes without error and prints per-module force plans. + +- [ ] **Step 4: Commit** + +```bash +git add cmd/archduino/main.go +git commit -m "feat(cmd): add --force flag to install command" +``` + +--- + +## Task 10: End-to-end validation + +**Files:** none (validation only) + +- [ ] **Step 1: Full test suite with race detector** + +```bash +cd /home/marvin/git-clones/ArduinoArchSetup +go test -race ./... +``` + +Expected: every package `ok`. Failures here = STOP and fix. + +- [ ] **Step 2: Full build** + +```bash +go build ./... +``` + +Expected: exit 0. + +- [ ] **Step 3: gofmt + vet** + +```bash +diff=$(gofmt -l .); [ -z "$diff" ] && echo "gofmt clean" || { echo "$diff"; exit 1; } +go vet ./... +``` + +Expected: both clean. + +- [ ] **Step 4: Dry-run smoke (without and with force)** + +```bash +./archduino install --dry-run --no-tui --no-sudo 2>&1 | tail -10 +./archduino install --dry-run --no-tui --no-sudo --force 2>&1 | tail -10 +``` + +Expected: second output's per-module plan is non-empty for every selected module (even ones the first output reports as satisfied). + +- [ ] **Step 5: No commit — validation is informational** + +--- + +## Notes for the executing agent + +- **Interface change order matters.** Task 2's stubs MUST land before any per-module `PlanForce` test runs (Task 3 onward), otherwise the whole package is red. +- **Read each module's existing `Plan` before writing `PlanForce`.** The force version is almost always "strip the conditionals from Plan". Don't invent new actions. +- **Do not delete users' cloned repos during force.** Especially neovim's NvChad clone. Rewriting `chadrc.lua` is enough; use `git pull` if the user wants updates, not `rm -rf && git clone`. +- **If a module's test file doesn't already have a fake pacman/yay with a `Reinstall` method,** extend the fake in that file (or, if it lives in `internal/platform/testing.go` or similar, extend it once there). Do NOT duplicate fakes — reuse. +- **Conventional Commits:** every module's force implementation gets its own `feat():` commit. This keeps the changelog readable and svu happy. +- **CI is now live** (Plan 4d merged into this branch). Expect the workflow to run on every push of this plan — if it goes red, the task isn't done.