@ericrisco/rsc 1.0.30 → 1.0.32
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/lib/backups.js +13 -2
- package/targets/claude.js +32 -7
- package/targets/session-start.mjs +12 -1
package/manifest.json
CHANGED
package/package.json
CHANGED
package/scripts/lib/backups.js
CHANGED
|
@@ -146,9 +146,20 @@ function safeRelative(cwd, absPath) {
|
|
|
146
146
|
return rel.split(sep).join('/');
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
// A manifest path is split on BOTH separators, because the input is file content — readManifest()
|
|
150
|
+
// reads it off disk — and not every manifest was written by safeRelative() on this platform. A path
|
|
151
|
+
// stored with Windows separators (`..\..\somewhere`) has no '/' to split on, so the old check saw a
|
|
152
|
+
// single segment, found no '..', and handed the raw string to join() — which honours backslashes on
|
|
153
|
+
// Windows. The guard only guarded on POSIX.
|
|
154
|
+
//
|
|
155
|
+
// Splitting into real segments does both jobs at once: `..` becomes visible to the check, and the
|
|
156
|
+
// path is rebuilt by join() rather than passed through, so the separator the manifest happened to
|
|
157
|
+
// use stops mattering. Found while fixing the Windows hook-duplication bug: same family.
|
|
158
|
+
const pathSegments = (value) => value.split(/[/\\]+/).filter(Boolean);
|
|
159
|
+
|
|
149
160
|
function safeJoin(cwd, relPath) {
|
|
150
|
-
if (!relPath || isAbsolute(relPath) || relPath
|
|
161
|
+
if (!relPath || isAbsolute(relPath) || pathSegments(relPath).includes('..')) {
|
|
151
162
|
throw new Error(`path is outside project root: ${relPath}`);
|
|
152
163
|
}
|
|
153
|
-
return join(cwd, ...relPath
|
|
164
|
+
return join(cwd, ...pathSegments(relPath));
|
|
154
165
|
}
|
package/targets/claude.js
CHANGED
|
@@ -14,6 +14,31 @@ export function writeSkill(id, fromDir, toPath) {
|
|
|
14
14
|
return linkOrCopy(fromDir, toPath);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// A hook entry, stringified with its path separators normalized to `/`.
|
|
18
|
+
//
|
|
19
|
+
// This exists because of a Windows bug that cost real users 4× the context budget. `join()` yields
|
|
20
|
+
// `.rsc\session-start.mjs` on Windows, `JSON.stringify` escapes that to `.rsc\\session-start.mjs`,
|
|
21
|
+
// and all seven dedup filters below searched for a forward-slash needle (`.rsc/session-start.`).
|
|
22
|
+
// The needle never matched, so every `install` / `add` / `sync` APPENDED a hook instead of replacing
|
|
23
|
+
// it: a real Windows box reported every hook present four times, which means the `suggest` always-on
|
|
24
|
+
// body was injected 4× per session start and all six guards ran 4× per command.
|
|
25
|
+
//
|
|
26
|
+
// Worse, and unreported: `unwireHook` uses the same needles, so on Windows `uninstall` and `purge`
|
|
27
|
+
// removed NONE of the hooks they claim to remove — a cleanup that silently cleans nothing.
|
|
28
|
+
//
|
|
29
|
+
// ONE replacement, and only the JSON-escaped form. `JSON.stringify` escapes every backslash as
|
|
30
|
+
// `\\`, so a separator always arrives doubled and a LONE backslash in the JSON text is never a
|
|
31
|
+
// separator — it is some other escape (`\"`, `\n`, `\uXXXX`). Collapsing those too would mangle
|
|
32
|
+
// them for no gain, which is why the second `.replace(/\\/g, '/')` this started with is gone: a
|
|
33
|
+
// mutation pass showed nothing depended on it.
|
|
34
|
+
//
|
|
35
|
+
// Normalizing the HAYSTACK (never the needle) keeps every call site readable as a plain POSIX path,
|
|
36
|
+
// and keeps a user's own `C:\...\my-own-hook.mjs` from ever looking like an rsc hook.
|
|
37
|
+
// `scripts/doctor.js` already handled both forms; this is that knowledge, shared.
|
|
38
|
+
export function hookWiringOf(entry) {
|
|
39
|
+
return JSON.stringify(entry).replace(/\\\\/g, '/');
|
|
40
|
+
}
|
|
41
|
+
|
|
17
42
|
// Inverse of wireHook: drop every rsc-wired hook entry (any command pointing at a
|
|
18
43
|
// .rsc/ script — session-start, worklog-checkpoint, ship-guard, danger-guard, … —
|
|
19
44
|
// plus the legacy cat-form) from settings.json, across all events. User hooks and
|
|
@@ -27,7 +52,7 @@ export function unwireHook(paths) {
|
|
|
27
52
|
if (!settings.hooks) return [];
|
|
28
53
|
for (const event of Object.keys(settings.hooks)) {
|
|
29
54
|
settings.hooks[event] = (settings.hooks[event] || []).filter((e) => {
|
|
30
|
-
const s =
|
|
55
|
+
const s = hookWiringOf(e);
|
|
31
56
|
return !s.includes('.rsc/') && !s.includes('skills/rsc/suggest');
|
|
32
57
|
});
|
|
33
58
|
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
@@ -61,7 +86,7 @@ export function wireHook(paths) {
|
|
|
61
86
|
settings.hooks ||= {};
|
|
62
87
|
settings.hooks.SessionStart ||= [];
|
|
63
88
|
settings.hooks.SessionStart = settings.hooks.SessionStart.filter((e) => {
|
|
64
|
-
const s =
|
|
89
|
+
const s = hookWiringOf(e);
|
|
65
90
|
// drop legacy cat-form and any prior session-start script (.sh from the bash era, or .mjs)
|
|
66
91
|
return !s.includes('skills/rsc/suggest') && !s.includes('.rsc/session-start.');
|
|
67
92
|
});
|
|
@@ -78,7 +103,7 @@ export function wireHook(paths) {
|
|
|
78
103
|
for (const event of ['PreCompact', 'SessionEnd']) {
|
|
79
104
|
settings.hooks[event] ||= [];
|
|
80
105
|
settings.hooks[event] = settings.hooks[event].filter(
|
|
81
|
-
(e) => !
|
|
106
|
+
(e) => !hookWiringOf(e).includes('.rsc/worklog-checkpoint.'),
|
|
82
107
|
);
|
|
83
108
|
settings.hooks[event].push({ hooks: [{ type: 'command', command: wlCmd }] });
|
|
84
109
|
}
|
|
@@ -96,7 +121,7 @@ export function wireHook(paths) {
|
|
|
96
121
|
const sgCmd = `node "${sgDest}" "${paths.projectRoot}"`;
|
|
97
122
|
settings.hooks.PreToolUse ||= [];
|
|
98
123
|
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
|
|
99
|
-
(e) => !
|
|
124
|
+
(e) => !hookWiringOf(e).includes('.rsc/ship-guard.'),
|
|
100
125
|
);
|
|
101
126
|
settings.hooks.PreToolUse.push({ matcher: 'Bash', hooks: [{ type: 'command', command: sgCmd }] });
|
|
102
127
|
|
|
@@ -109,7 +134,7 @@ export function wireHook(paths) {
|
|
|
109
134
|
copyFileSync(join(HERE, 'danger-guard.mjs'), dgDest);
|
|
110
135
|
const dgCmd = `node "${dgDest}" "${paths.projectRoot}"`;
|
|
111
136
|
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
|
|
112
|
-
(e) => !
|
|
137
|
+
(e) => !hookWiringOf(e).includes('.rsc/danger-guard.'),
|
|
113
138
|
);
|
|
114
139
|
settings.hooks.PreToolUse.push({ matcher: 'Bash', hooks: [{ type: 'command', command: dgCmd }] });
|
|
115
140
|
|
|
@@ -124,7 +149,7 @@ export function wireHook(paths) {
|
|
|
124
149
|
copyFileSync(join(HERE, 'gitmoji-guard.mjs'), gmDest);
|
|
125
150
|
const gmCmd = `node "${gmDest}" "${paths.projectRoot}"`;
|
|
126
151
|
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
|
|
127
|
-
(e) => !
|
|
152
|
+
(e) => !hookWiringOf(e).includes('.rsc/gitmoji-guard.'),
|
|
128
153
|
);
|
|
129
154
|
settings.hooks.PreToolUse.push({ matcher: 'Bash', hooks: [{ type: 'command', command: gmCmd }] });
|
|
130
155
|
|
|
@@ -141,7 +166,7 @@ export function wireHook(paths) {
|
|
|
141
166
|
const fgCmd = `node "${fgDest}" "${paths.projectRoot}"`;
|
|
142
167
|
settings.hooks.UserPromptSubmit ||= [];
|
|
143
168
|
settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter(
|
|
144
|
-
(e) => !
|
|
169
|
+
(e) => !hookWiringOf(e).includes('.rsc/userprompt-gate.'),
|
|
145
170
|
);
|
|
146
171
|
settings.hooks.UserPromptSubmit.push({ hooks: [{ type: 'command', command: fgCmd }] });
|
|
147
172
|
|
|
@@ -57,7 +57,18 @@ function versionAt(scopeRoot) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
function isWiredScope(scopeRoot) {
|
|
60
|
-
|
|
60
|
+
// settings.json stores the wired command with the separator the installing platform produced, and
|
|
61
|
+
// JSON escapes a Windows backslash as a pair. Looking only for `.rsc/` therefore answered "not
|
|
62
|
+
// wired" for every Windows scope — which made THIS detector, the one that warns about a duplicated
|
|
63
|
+
// always-on body, blind on the exact platform where the body was being injected four times.
|
|
64
|
+
//
|
|
65
|
+
// Normalized inline rather than imported: hooks are materialized file by file, so this script can
|
|
66
|
+
// only import a sibling in `.rsc/`. `targets/claude.js` (hookWiringOf) and `scripts/doctor.js`
|
|
67
|
+
// carry the same one-liner for the same reason; the duplication is the price of the hook layout.
|
|
68
|
+
try {
|
|
69
|
+
const raw = readFileSync(join(scopeRoot, '.claude', 'settings.json'), 'utf8');
|
|
70
|
+
return raw.replace(/\\\\/g, '/').includes('.rsc/');
|
|
71
|
+
} catch { return false; }
|
|
61
72
|
}
|
|
62
73
|
|
|
63
74
|
function olderThan(a, b) {
|