6.7 KiB
Force Reapply Design (Plan 4b)
Date: 2026-04-14
Scope: Plan 4b of the Archduino Go rewrite — add archduino install --force that re-runs every selected module's Apply unconditionally.
Goal
Let the user bypass the four-phase contract's idempotency guard when they explicitly want to reapply everything — typically after a manual edit, corruption, or "I'm not sure what's broken, redo it" scenario. Scope is narrow: force a full reapply of currently-selected modules. No uninstall, no revert, no selective force-by-category.
Non-goals
- No uninstall / reset / revert behavior. Modules have no
Revertmethod today; adding one is a separate, much larger plan. - No selective force (
--force-configs,--force-packages). The user picked "full re-everything" during brainstorming; single flag only. - No new subcommand (
archduino reinstall). A flag oninstallis enough. - No changes to
Verify; force affects only whatApplydoes, not how we confirm it.
Interface change
Add one method to module.Module:
// 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 "install from
// scratch" — reinstall all packages (without pacman's --needed guard)
// and rewrite all managed config files.
PlanForce(ctx context.Context) (Plan, error)
Every existing module implements this. Two categories:
Config-writing modules
zsh, neovim, themes, starship, shellconfig, zellij, yazi (config bits), arduinocli (sketch dirs, indexes).
PlanForce produces a plan that:
- Reinstalls packages via
platform.Pacman.Reinstall/platform.Yay.Reinstall(no--needed). - Rewrites every managed config file, overwriting existing content.
- Re-runs any post-install steps (e.g., arduino-cli core update, zsh plugin clone).
Package-only modules
packages, gittools, fonts, renode, and the package-only portions of starship / yazi.
PlanForce produces a plan that calls platform.Pacman.Reinstall(ctx, pkgs) — identical to the install step but without --needed, so pacman physically re-fetches and reinstalls the package even if already present.
Platform layer additions
internal/platform/pacman.go:
// Reinstall runs `pacman -S --noconfirm pkg...` without the --needed flag
// so pacman unconditionally reinstalls each package.
func (p Pacman) Reinstall(ctx context.Context, pkgs []string) error
internal/platform/aur.go:
// Reinstall runs `yay -S --noconfirm pkg...` without the --needed flag.
func (y Yay) Reinstall(ctx context.Context, pkgs []string) error
Both are straight copies of the existing Install methods with --needed omitted from the command-line args.
Engine changes
internal/engine/engine.go:
RunOptionsgains a field:Force boolRunper-module path becomes:Whenvar plan module.Plan if opts.Force { plan, err = mod.PlanForce(ctx) } else { cur, derr := mod.Detect(ctx) if derr != nil { /* existing error handling */ } plan, err = mod.Plan(ctx, cur) }Forceis true,Detectis not called.
The Apply and Verify phases are unchanged. Force does not affect DAG ordering, reporter output, or halt-on-error behavior.
CLI changes
cmd/archduino/main.go::newInstallCmd():
var force bool
cmd.Flags().BoolVar(&force, "force", false,
"Reapply every selected module, ignoring current state")
In RunE, propagate to both engine and wizard:
runOpts.Force = force
initialState.Force = force
Wizard / TUI integration
internal/tui/state.go — WizardState gains:
Force bool
internal/tui/wizard/wizard.go — the planCache path branches:
- When
state.Forceis true, callPlanForce(ctx)instead ofDetect+Plan. - When a user toggles a module's selection, the cache key includes
forceso caches from non-force runs are not reused. - Header gains a persistent banner:
⚠ Force mode: all selected modules will be reapplied.
Data flow
user: archduino install --force
→ main.go: force=true → RunOptions.Force = true, WizardState.Force = true
→ wizard.Run: per-module preview uses PlanForce (skips Detect)
→ user confirms
→ engine.Run: per-module PlanForce → Apply → Verify
Error handling
No new failure modes. PlanForce errors are handled identically to Plan errors: reported via the reporter, counted in the engine summary, respected by --halt-on-error.
Testing
Per-module tests — each existing *_test.go gains a TestPlanForce case. The test arranges a "satisfied" system state (mocked platform says everything is installed, configs exist), calls PlanForce(ctx), and asserts the returned plan is non-empty and contains the expected reinstall/rewrite actions. This is the core behavioral contract — without this test, a module could silently "forget" to implement force.
Engine test — engine_test.go gets a case using a fake module that records whether Detect, Plan, and PlanForce were called. Runs with RunOptions.Force: true and asserts Detect was NOT called and PlanForce WAS.
Platform tests — pacman_test.go and aur_test.go each gain a TestReinstall that verifies the command-line args include -S and --noconfirm but do NOT include --needed.
No TUI test for the banner — visual-only, covered by the existing wizard tests for preview-cache behavior which will get a new "force preview" case.
Interaction with existing features
--dry-run --forceprints the force plan and exits. Existing dry-run code path is unchanged.--only/--skiprespect force: force applies only to the selected set.--halt-on-errorrespects force identically.- Theme application at end of install (Plan 4a feature) runs unchanged; force doesn't skip it.
File touch list
Modified:
internal/module/module.go— addPlanForcetoModuleinterfaceinternal/engine/engine.go— addRunOptions.Force, branch inRuninternal/engine/engine_test.go— new force testinternal/tui/state.go— addWizardState.Forceinternal/tui/wizard/wizard.go— force-aware preview cache + bannerinternal/tui/wizard/wizard_test.go— new preview test with forceinternal/platform/pacman.go— addReinstallmethodinternal/platform/pacman_test.go— new testinternal/platform/aur.go— addReinstallmethodinternal/platform/aur_test.go— new testcmd/archduino/main.go— add--forceflag, wire through- 13 module files (
internal/modules/*/*.go) — implementPlanForce - 13 module test files —
TestPlanForceeach
New: none.