727 lines
25 KiB
Markdown
727 lines
25 KiB
Markdown
# 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 `<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 `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/<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):
|
||
|
||
```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/<name>/ -v
|
||
```
|
||
|
||
Expected: PASS.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
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:
|
||
|
||
```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/<name>/ -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/<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:
|
||
|
||
- **`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(<name>): 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(<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.
|