plumbbob 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/verbs/revert.js +43 -3
- package/hooks/pre-edit.sh +17 -0
- package/package.json +1 -1
package/dist/verbs/revert.js
CHANGED
|
@@ -2,10 +2,18 @@
|
|
|
2
2
|
// recent step, or `--to n`, with the baseline as fallback), then remove untracked
|
|
3
3
|
// files under the SEAM only. The sidecar is git-excluded (D17), so the reset
|
|
4
4
|
// never touches it — park lines and intent edits survive the revert (C4).
|
|
5
|
-
|
|
5
|
+
//
|
|
6
|
+
// Plumbbob also installs its driver skills INTO the repo (.claude/skills/<driver>/
|
|
7
|
+
// for a self-contained install), so a blunt reset would discard an out-of-seam
|
|
8
|
+
// skill edit — or a `pnpm up plumbbob` re-setup — together with the half-done
|
|
9
|
+
// step. revert discards the step's WORK, never plumbbob's own machinery, so those
|
|
10
|
+
// paths are carried across the reset unchanged.
|
|
11
|
+
import { cpSync, existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from 'node:fs';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
6
13
|
import { join } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
7
15
|
import { findRepoRoot, resetHard, untrackedPaths } from "../lib/git.js";
|
|
8
|
-
import { checkpointsPath, hasSession, seamPath, stepPath, writeState } from "../lib/sidecar.js";
|
|
16
|
+
import { checkpointsPath, hasSession, seamPath, sidecarDir, stepPath, writeState } from "../lib/sidecar.js";
|
|
9
17
|
import { matchesSeam } from "../lib/intent.js";
|
|
10
18
|
export function revert(cwd, args) {
|
|
11
19
|
const root = findRepoRoot(cwd);
|
|
@@ -39,7 +47,7 @@ export function revert(cwd, args) {
|
|
|
39
47
|
// ignored files alone, so they must be removed explicitly afterward).
|
|
40
48
|
const seam = readSeamTokens(root);
|
|
41
49
|
const toRemove = untrackedPaths(root).filter((p) => matchesSeam(p, seam));
|
|
42
|
-
|
|
50
|
+
resetPreserving(root, sha, plumbbobOwnedPaths(root));
|
|
43
51
|
for (const rel of toRemove) {
|
|
44
52
|
rmSync(join(root, rel), { force: true, recursive: true });
|
|
45
53
|
}
|
|
@@ -49,6 +57,38 @@ export function revert(cwd, args) {
|
|
|
49
57
|
process.stdout.write(`plumbbob: reverted to ${sha.slice(0, 9)} — STATE=DESIGN. Park lines and intent edits were preserved.\n`);
|
|
50
58
|
return 0;
|
|
51
59
|
}
|
|
60
|
+
// The repo paths that belong to plumbbob, not to the work being reverted: the
|
|
61
|
+
// sidecar (already git-excluded, listed so revert is robust even where `.plumbbob/`
|
|
62
|
+
// was tracked by mistake) and each installed driver skill under .claude/skills/.
|
|
63
|
+
// Skill names come from plumbbob's own bundled `skills/` dir — the same source
|
|
64
|
+
// `setup` copies from — so only plumbbob's own skills are protected, never the
|
|
65
|
+
// user's. Only paths that currently exist are returned.
|
|
66
|
+
function plumbbobOwnedPaths(root) {
|
|
67
|
+
const paths = [sidecarDir(root)];
|
|
68
|
+
try {
|
|
69
|
+
for (const name of readdirSync(fileURLToPath(new URL('../../skills', import.meta.url)))) {
|
|
70
|
+
paths.push(join(root, '.claude', 'skills', name));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Bundled skills dir not resolvable (unexpected) — protect just the sidecar.
|
|
75
|
+
}
|
|
76
|
+
return paths.filter((p) => existsSync(p));
|
|
77
|
+
}
|
|
78
|
+
// `git reset --hard <sha>` is repo-wide, so paths that must survive it are copied
|
|
79
|
+
// to a temp snapshot first, then copied back over whatever the reset produced.
|
|
80
|
+
// Restoring on top (no pre-delete) keeps the live sidecar safe if a copy throws.
|
|
81
|
+
function resetPreserving(root, sha, preserve) {
|
|
82
|
+
const snap = mkdtempSync(join(tmpdir(), 'plumbbob-revert-'));
|
|
83
|
+
try {
|
|
84
|
+
preserve.forEach((p, i) => cpSync(p, join(snap, String(i)), { recursive: true }));
|
|
85
|
+
resetHard(root, sha);
|
|
86
|
+
preserve.forEach((p, i) => cpSync(join(snap, String(i)), p, { recursive: true }));
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
rmSync(snap, { force: true, recursive: true });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
52
92
|
function parseTo(args) {
|
|
53
93
|
const idx = args.indexOf('--to');
|
|
54
94
|
if (idx === -1) {
|
package/hooks/pre-edit.sh
CHANGED
|
@@ -84,6 +84,23 @@ case "$rel" in
|
|
|
84
84
|
;;
|
|
85
85
|
esac
|
|
86
86
|
|
|
87
|
+
# The seam muzzle below governs repo code only (D23). A path *outside* the repo
|
|
88
|
+
# (Claude's own plan-mode scratch in ~/.claude/plans, an editor tmpfile) and a
|
|
89
|
+
# git-ignored path *inside* it (fallow data, dist/, coverage/) are none of its
|
|
90
|
+
# business — never block them. `.plumbbob/` is itself git-ignored but is
|
|
91
|
+
# plumbbob's own control surface, so its non-doc files (STATE/SEAM/...) must stay
|
|
92
|
+
# governed by the muzzle, never skipped here.
|
|
93
|
+
case "$rel" in
|
|
94
|
+
.plumbbob/*) ;; # control state: stays governed by the muzzle, never skipped
|
|
95
|
+
*)
|
|
96
|
+
# git resolves the path via the repo toplevel (robust to symlinked cwds).
|
|
97
|
+
# check-ignore exit: 0 = ignored, 128 = outside the repo — neither is the
|
|
98
|
+
# muzzle's business; 1 = in-repo, tracked territory -> fall through to it.
|
|
99
|
+
git -C "$root" check-ignore -q -- "$path" 2>/dev/null
|
|
100
|
+
[ "$?" -ne 1 ] && exit 0
|
|
101
|
+
;;
|
|
102
|
+
esac
|
|
103
|
+
|
|
87
104
|
# 4. Everything else is code: allowed only in BUILD, confined to the SEAM (D23).
|
|
88
105
|
# SPIKE locks the main tree like DESIGN (D18) — spike edits live in dormant
|
|
89
106
|
# worktrees, so the muzzle never needs to allow SPIKE here.
|