@ericrisco/rsc 2.0.1 → 2.0.2
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/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/doctor.js +14 -1
- package/scripts/install-apply.js +8 -3
- package/targets/worktree-reaper.mjs +40 -2
package/manifest.json
CHANGED
package/package.json
CHANGED
package/scripts/doctor.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hooksDir, hooksDirIsOurs } from '../targets/worktree-reaper.mjs';
|
|
1
2
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
3
|
import { join, dirname } from 'node:path';
|
|
3
4
|
import { homedir } from 'node:os';
|
|
@@ -84,9 +85,21 @@ export function missingHookScripts({ target, home = homedir(), cwd = process.cwd
|
|
|
84
85
|
// different answers: armed, absent (repair it), or someone else's (leave it — chaining is our job
|
|
85
86
|
// at install time, and overwriting a husky hook to fix a convenience would be indefensible).
|
|
86
87
|
function worktreeCleanupStatus(root) {
|
|
87
|
-
const hook = join(root, '.git', 'hooks', 'post-merge');
|
|
88
88
|
if (!existsSync(join(root, '.git'))) return { state: 'not-a-repo' };
|
|
89
89
|
if (existsSync(join(root, '.rsc', '.no-worktree-cleanup'))) return { state: 'off-by-choice' };
|
|
90
|
+
// Ask git where hooks live instead of assuming `.git/hooks`. Until 2.0.1 this read the assumed
|
|
91
|
+
// path, so on a repo with its own `core.hooksPath` it found our inert file there and answered
|
|
92
|
+
// `armed` about a hook git would never run — the check confirming the exact failure it exists to
|
|
93
|
+
// catch. `unreachable` is its own state on purpose: `repair` re-runs the same install, so
|
|
94
|
+
// recommending it here would send the reader round the same loop (P6 wants a way out, not a ring).
|
|
95
|
+
const dir = hooksDir(root);
|
|
96
|
+
if (!hooksDirIsOurs(root, dir)) {
|
|
97
|
+
return {
|
|
98
|
+
state: 'unreachable',
|
|
99
|
+
action: `git reads hooks from ${dir}, which belongs to this repo or to another tool (husky, lefthook, a committed dispatcher). rsc will not write there. To arm the cleanup, add \`node "$(git rev-parse --show-toplevel)/.rsc/worktree-reaper.mjs" "$(git rev-parse --show-toplevel)" auto\` to your own post-merge hook.`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const hook = join(dir, 'post-merge');
|
|
90
103
|
if (!existsSync(hook)) {
|
|
91
104
|
return { state: 'absent', action: 'Run `npx @ericrisco/rsc repair` to arm the post-merge cleanup in this clone.' };
|
|
92
105
|
}
|
package/scripts/install-apply.js
CHANGED
|
@@ -252,15 +252,20 @@ export async function applyInstall({ skillIds = [], agentIds = [], target, home,
|
|
|
252
252
|
// content — declares a narrower set of paths, and writing a git hook it never agreed to would be
|
|
253
253
|
// acting outside the plan the user accepted (P4). Caught by the onboarding route-inventory gate,
|
|
254
254
|
// which is exactly the kind of catch that justifies its existence.
|
|
255
|
+
// The answer used to be discarded, so a hook that could not arm looked exactly like one that
|
|
256
|
+
// did — and the only place that could have noticed, `doctor`, was reading the same wrong path.
|
|
257
|
+
// Reported 2026-09-18. Carried out, never thrown: the harness installing is the point, and the
|
|
258
|
+
// merge hook is a convenience layered on top of it.
|
|
259
|
+
let mergeHook = { installed: false, state: 'skipped' };
|
|
255
260
|
try {
|
|
256
261
|
if (policy?.codeHooks === false) throw new Error('code hooks not governed by the accepted plan');
|
|
257
262
|
mkdirSync(join(cwd, '.rsc'), { recursive: true });
|
|
258
263
|
cpSync(join(ROOT, 'targets', 'worktree-reaper.mjs'), join(cwd, '.rsc', 'worktree-reaper.mjs'));
|
|
259
264
|
const { installMergeHook } = await import('../targets/worktree-reaper.mjs');
|
|
260
|
-
if (existsSync(join(cwd, '.git'))) installMergeHook(cwd);
|
|
261
|
-
} catch {
|
|
265
|
+
if (existsSync(join(cwd, '.git'))) mergeHook = installMergeHook(cwd);
|
|
266
|
+
} catch (err) { mergeHook = { installed: false, state: 'skipped', reason: err.message }; }
|
|
262
267
|
ignoreLocalState(cwd, target);
|
|
263
|
-
return { ...state, backup };
|
|
268
|
+
return { ...state, backup, mergeHook };
|
|
264
269
|
}
|
|
265
270
|
|
|
266
271
|
// `.rsc/` holds local machine state — hook scripts, install markers, the sello's
|
|
@@ -428,6 +428,41 @@ node "$root/.rsc/worktree-reaper.mjs" "$root" auto 2>/dev/null || true
|
|
|
428
428
|
exit 0
|
|
429
429
|
`;
|
|
430
430
|
|
|
431
|
+
/**
|
|
432
|
+
* Where git will ACTUALLY look for hooks, asked of git rather than assumed.
|
|
433
|
+
*
|
|
434
|
+
* The assumption was `<root>/.git/hooks`, and it is wrong in three ordinary situations at once:
|
|
435
|
+
* a repo that sets `core.hooksPath` (husky v9 writes `.husky/_`, lefthook and committed
|
|
436
|
+
* `.githooks/` dispatchers do the same), a linked worktree where `.git` is a FILE and the mkdir
|
|
437
|
+
* raised ENOTDIR, and a submodule. Reported from a user's repo on 2026-09-18 against 2.0.1: rsc
|
|
438
|
+
* wrote the hook where git does not look, `doctor` read the same wrong path back, and the report
|
|
439
|
+
* said `armed` about something that could never run. Verified there and here that a single
|
|
440
|
+
* `rev-parse` answers all three.
|
|
441
|
+
*
|
|
442
|
+
* `--path-format=absolute` needs git 2.31 (Mar 2021). Older git gets the old path — which is
|
|
443
|
+
* correct whenever `core.hooksPath` is unset, i.e. the case it was always right about.
|
|
444
|
+
*/
|
|
445
|
+
export function hooksDir(root) {
|
|
446
|
+
const r = git(root, ['rev-parse', '--path-format=absolute', '--git-path', 'hooks']);
|
|
447
|
+
if (r.ok && r.out) return r.out;
|
|
448
|
+
return join(real(root), '.git', 'hooks');
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Whether that directory is ours to write in. Inside the repository's own git dir: yes, it is
|
|
452
|
+
// private plumbing that is never cloned and never committed. Anywhere else it belongs to the user
|
|
453
|
+
// or to another tool — a committed `.githooks/` is their source tree (writing there would put rsc
|
|
454
|
+
// in their diff, and moving an existing hook aside would rename a TRACKED file), and `.husky/_` is
|
|
455
|
+
// regenerated by husky, so anything we left there disappears on their next install. Report it and
|
|
456
|
+
// let the person decide; that is P4, and it is the same reasoning the comment on this function has
|
|
457
|
+
// claimed since the hook shipped, finally applied instead of only stated.
|
|
458
|
+
export function hooksDirIsOurs(root, dir) {
|
|
459
|
+
const r = git(root, ['rev-parse', '--path-format=absolute', '--git-common-dir']);
|
|
460
|
+
if (!r.ok || !r.out) return false;
|
|
461
|
+
const gitDir = real(r.out);
|
|
462
|
+
const target = real(dir);
|
|
463
|
+
return target === gitDir || target.startsWith(gitDir + sep);
|
|
464
|
+
}
|
|
465
|
+
|
|
431
466
|
/**
|
|
432
467
|
* Install the trigger. The judgement has existed and been tested for a while; what never existed was
|
|
433
468
|
* something that runs it at the moment work lands. `post-merge` is that moment, and it covers both
|
|
@@ -442,7 +477,10 @@ exit 0
|
|
|
442
477
|
*/
|
|
443
478
|
export function installMergeHook(root) {
|
|
444
479
|
try {
|
|
445
|
-
const dir =
|
|
480
|
+
const dir = hooksDir(root);
|
|
481
|
+
if (!hooksDirIsOurs(root, dir)) {
|
|
482
|
+
return { installed: false, state: 'unreachable', reason: `git reads hooks from ${dir}, which belongs to this repository or to another tool — rsc will not write there.` };
|
|
483
|
+
}
|
|
446
484
|
const hook = join(dir, 'post-merge');
|
|
447
485
|
mkdirSync(dir, { recursive: true });
|
|
448
486
|
if (existsSync(hook)) {
|
|
@@ -461,7 +499,7 @@ export function installMergeHook(root) {
|
|
|
461
499
|
chmodSync(hook, 0o755);
|
|
462
500
|
return { installed: true, chained: existsSync(join(dir, 'post-merge.rsc-local')) };
|
|
463
501
|
} catch (err) {
|
|
464
|
-
return { installed: false, reason: err.message };
|
|
502
|
+
return { installed: false, state: 'failed', reason: err.message };
|
|
465
503
|
}
|
|
466
504
|
}
|
|
467
505
|
|