Skip to content

feat(cli): saasaloy remove — undo applied files via manifest - #37

Open
mimukit wants to merge 4 commits into
mainfrom
issue-27-saasaloy-remove-undo-applied-files-via-manifest
Open

feat(cli): saasaloy remove — undo applied files via manifest#37
mimukit wants to merge 4 commits into
mainfrom
issue-27-saasaloy-remove-undo-applied-files-via-manifest

Conversation

@mimukit

@mimukit mimukit commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Summary

saasaloy remove <module> is the offline undo for add: it deletes exactly the files .saasaloy/manifest.json attributes to a module, protects hand-edited (drifted) content, reconciles saasaloy.json/saasaloy-lock.json/the manifest, prunes emptied directories, drops dangling aliases, and warns (never reverses) about config patches the module applied elsewhere. To make that last part possible, this PR also teaches the add side to record every applied config patch in the manifest so remove has something to warn about.

Closes #27.

Changes

  • lib/manifest.ts / schemas/manifest.schema.json: add Manifest.patches[] ({module, file, patch}), loaded with an empty-array default for backward compatibility with manifests written before this change.
  • lib/applier.ts: executePlan appends a ManifestPatch entry whenever a config patch actually changes a file, deduped by structural equality so a --force re-apply landing the same op doesn't duplicate the entry. Repoints the "clean reverse-patching is a follow-up" comment from #27 to #36.
  • lib/remover.ts (new): buildRemovePlan / executeRemovePlan — classifies each manifest-owned file as delete/drift/missing against current disk state, handles owned .claude/.agents skill links, prunes emptied directories, drops dangling aliases, computes cross-module dependents, and surfaces this module's recorded config patches for warning (never reversal).
  • commands/remove.ts (new): CLI surface mirroring add's clack UX — --dry-run, --diff (renders deletions as an all-red unified diff), --yes, --force; a bare remove shows a picker over installed modules. Drifted files always require a per-file confirm and are never silently clobbered under --yes (labeled "drift → kept (untracked)" instead). Refuses to remove a module other installed modules still depend on unless --forced.
  • index.ts: wires remove into the command table.
  • docs/qa/qa-remove-command-2026-07-25.md (new): manual QA plan (see below).

Follow-up nit commit (e4e8ed8) fixed two review findings on the first pass: dropped a misleading pre-execute skill-link-conflict warning in favor of the accurate post-execute one, and corrected the drift label shown under --yes.

Assumptions

Mechanical choices made during implementation, called out for the reviewer:

  • Manifest patches entry shape is {module, file, patch: RegistryPatch}; schema validation mirrors registry-item.schema.json's loose pattern (require module+file+patch, don't deep-validate the patch payload itself).
  • Patch dedupe is structural equality of {module, file, patch} — an identical entry is never pushed twice (keeps a --force re-apply idempotent).
  • Module-not-installed and empty-installed[] cases error/exit-0 via clack cancel/note, mirroring add.ts's existing error UX rather than inventing a new pattern.
  • Empty-dir prune treats any remaining entry, including dotfiles, as "non-empty" (won't prune a directory that only looks empty at a glance).
  • No command-level (remove.ts) unit tests — matches repo convention: only lib/ is unit-tested, command behavior is covered by the manual QA plan instead.
  • --diff renders a deletion as an all-red diff of (oldContent → ""), reusing add's diff renderer and line cap.
  • applier.ts's patch-reversal comment was repointed from #27 to #36 (the reverse-patching follow-up, confirmed open: "reverse config patches on remove...").

Known behavior / follow-up (plan-sanctioned, not a defect)

  • dropDanglingAliases scans all aliases, so removing module A can also drop a pre-existing dangling alias that belongs to module B. This is plan-sanctioned ("drop any alias whose prefix directory no longer exists") — noted here for reviewer awareness, not treated as a bug.
  • Reverse config-patch removal is intentionally out of scope for this PR: remove drops the patch's manifest entry and warns per affected file, but does not revert the patch's content. Reversal is tracked in follow-up issue feat(cli): reverse config patches on remove via tracked manifest patches #36.

Test plan

  • pnpm --filter saasaloy-cli test → 9 test files, 100 tests, all passing (includes new remover.test.ts — 24 tests — and the patch-tracking additions to applier.test.ts — 31 tests).
  • pnpm --filter saasaloy-cli typecheck → 0 errors.
  • pnpm --filter saasaloy build → tsup build succeeds.
  • Full addremove round trip exercised manually end-to-end in .dev/playground against the repo's real api/database modules: install, dependents-refusal + --force override, --dry-run/--diff previews, config-patch record/warn (never reverse), unmanaged-file and prune-boundary handling, hand-edit drift survival, unknown-module/unknown-flag errors, and backward-compat load of a pre-existing manifest with no patches key.
  • QA plan: docs/qa/qa-remove-command-2026-07-25.md — 9 manual test cases covering the interactive-only surface (confirm prompts, the picker, Ctrl-C handling, clack TUI legibility) that the automated suite above can't reach.

mimukit added 4 commits July 25, 2026 01:31
Lays the ledger groundwork for `remove` (issue #27) — a structural config
patch mutates a file another module owns, so it can't be a clean managed
copy, but its application still needs to be recorded so `remove` can warn
which files it can't undo on its own.

- add `ManifestPatch` (module/file/patch) and `Manifest.patches[]`
- `executePlan` appends an entry whenever a patch actually changes a file
- dedupe on structural equality so a `--force` re-apply landing the same
  op again doesn't duplicate the entry
- extend `manifest.schema.json` with the `patches` array
Closes #27 — an offline undo for a module's applied files, mirroring
`add`'s plan/execute split and clack UX so the two commands feel like a
pair.

- `remover.ts`: `buildRemovePlan`/`executeRemovePlan` classify each
  manifest-owned file as delete/drift/missing against current disk state,
  handle owned `.claude/skills` links, prune emptied directories, drop
  dangling aliases, and report (never reverse) this module's config patches
- `commands/remove.ts`: CLI surface with `--dry-run`/`--diff`/`--yes`/
  `--force`; drifted (hand-edited) files always need per-file confirmation
  and are never silently clobbered under `--yes`; refuses to remove a
  module other installed modules still depend on unless `--force`d
- wire `remove` into `index.ts`'s command table
Two review nits on the just-added remove command surfaced misleading
output around drifted files and skill-link conflicts.

- drop the pre-execute conflict skill-link warning now that the
  post-execute warning is the sole, accurate report of what happened
- label a drifted file "drift -> kept (untracked)" under --yes instead
  of implying a confirmation prompt that never runs
Issue #27 adds the offline undo path (add -> remove); document the
human-only test cases that automated coverage can't reach.

- cover round-trip add/remove, dependents-refusal, and patch-warning
  flows using the repo's real api/database module pair
- focus on interactive confirm prompts, the picker, Ctrl-C handling,
  and clack TUI legibility, since deterministic behavior is already
  agent-verified via --yes/--force
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(cli): saasaloy remove — undo applied files via manifest

1 participant