25 KiB
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— addReinstallinternal/platform/aur.go+_test.go— addReinstallinternal/module/module.go— addPlanForceto interfaceinternal/engine/engine.go+_test.go— addRunOptions.Forceinternal/tui/state.go— addWizardState.Forceinternal/tui/wizard/wizard.go+_test.go— force-aware previewcmd/archduino/main.go— add--forceflag- 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.
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:
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
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:
// 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
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
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
PlanForceto the interface
Edit internal/module/module.go. In the Module interface block:
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:
// 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
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
PlanForceto 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):
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:
internal/modules/packages/packages.gointernal/modules/gittools/gittools.gointernal/modules/fonts/fonts.gointernal/modules/renode/renode.gointernal/modules/hello/hello.gointernal/modules/starship/starship.gointernal/modules/yazi/yazi.gointernal/modules/zsh/zsh.gointernal/modules/zellij/zellij.gointernal/modules/shellconfig/shellconfig.gointernal/modules/themes/themes.gointernal/modules/neovim/neovim.gointernal/modules/arduinocli/arduinocli.go
- Step 4: Verify compile + existing tests pass
go build ./...
go test ./...
Expected: compile clean, all existing tests green. The new stubs aren't exercised yet.
- Step 5: Commit
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:
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.PlanplanForceCalls int- Implement
PlanForce(ctx) (module.Plan, error)that increments the counter and returnsplanForce. - If
detectCalls/planCallsdon'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
go test ./internal/engine/ -run TestRunForceSkipsDetectAndCallsPlanForce -v
Expected: FAIL — likely unknown field Force in RunOptions at compile time.
- Step 3: Add
ForcetoRunOptions
In internal/engine/engine.go, find the RunOptions struct and add:
// 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
RunonForce
Find the per-module loop inside Run. The current shape is roughly:
cur, err := mod.Detect(ctx)
if err != nil { /* handle */ }
plan, err := mod.Plan(ctx, cur)
Replace with:
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
go test ./internal/engine/ -run TestRunForceSkipsDetectAndCallsPlanForce -v
go test ./internal/engine/ -v
Expected: both green.
- Step 6: Commit
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 <name>.go and <name>_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
TestPlanForcein 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):
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
go test ./internal/modules/<name>/ -run TestPlanForce -v
Expected: FAIL — the stub returns an error.
- Step 3: Replace stub with real
PlanForce
In <name>.go, replace the stub with (adjust receiver/field names):
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
go test ./internal/modules/<name>/ -v
Expected: PASS.
- Step 5: Commit
git add internal/modules/<name>/
git commit -m "feat(<name>): implement PlanForce to reinstall all packages"
hello module
The hello module is a placeholder. Implement PlanForce as:
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
Planto see what it does
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
go test ./internal/modules/<name>/ -run TestPlanForce -v
- Step 4: Replace stub with real
PlanForce
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
go test ./internal/modules/<name>/ -v
git add internal/modules/<name>/
git commit -m "feat(<name>): 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(<name>): implement PlanForce.
Module-specific notes:
shellconfighas no packages;PlanForceonly produces file-write actions.themeswrites under~/.config/archduino/overlay.yaml— force rewrites the overlay's theme fields unconditionally.zshclones plugins (oh-my-zsh, etc.) viagit;PlanForceshould clone even if the target already exists — add a pre-step that removes the directory, or use thegithelper'sForceCloneif one exists (grepinternal/platform/git.go; if missing, note it as a concern and fall back to rm-then-clone via an action that usesos.RemoveAllbefore 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 inPlanbecomes an unconditional branch inPlanForce. - 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(<name>): implement PlanForce.
Neovim specifics:
- If
NvChadis already cloned,PlanForceshould eithergit pull+ rewritechadrc.luaORrm -rf && git clone. Pick the simpler: rewritechadrc.luaunconditionally, do NOT delete the existing clone. Users' local NvChad modifications shouldn't be wiped by--force. Document this in a code comment.
arduinocli specifics:
PlanForceshould re-download the binary tarball, re-runarduino-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 booltoWizardState
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:
// 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:
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
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:
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:
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
go test ./internal/tui/wizard/ -v
go test ./internal/tui/ -v
Expected: all green.
- Step 7: Commit
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:
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 setForce: force). -
Where
tui.NewWizardState(cfg, reg)is called: after construction, setinitial.Force = force. -
Step 3: Smoke-test
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
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
cd /home/marvin/git-clones/ArduinoArchSetup
go test -race ./...
Expected: every package ok. Failures here = STOP and fix.
- Step 2: Full build
go build ./...
Expected: exit 0.
- Step 3: gofmt + vet
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)
./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
PlanForcetest runs (Task 3 onward), otherwise the whole package is red. - Read each module's existing
Planbefore writingPlanForce. 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.luais enough; usegit pullif the user wants updates, notrm -rf && git clone. - If a module's test file doesn't already have a fake pacman/yay with a
Reinstallmethod, extend the fake in that file (or, if it lives ininternal/platform/testing.goor similar, extend it once there). Do NOT duplicate fakes — reuse. - Conventional Commits: every module's force implementation gets its own
feat(<name>):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.