@primitive.ai/prim 0.1.0-alpha.9 → 0.1.0-alpha.91
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/README.md +303 -37
- package/SKILL.md +269 -0
- package/dist/chunk-4GLVJLEQ.js +55 -0
- package/dist/chunk-5AFRMOPX.js +209 -0
- package/dist/chunk-5GYQ4J3D.js +126 -0
- package/dist/chunk-67VNLLBN.js +789 -0
- package/dist/chunk-BTNNEH2W.js +79 -0
- package/dist/chunk-EWZMDD44.js +1474 -0
- package/dist/chunk-EZXEWFDG.js +210 -0
- package/dist/chunk-FBAYCS3X.js +327 -0
- package/dist/chunk-FKXFUUWJ.js +36 -0
- package/dist/chunk-FOLKHXLJ.js +1529 -0
- package/dist/chunk-H4UNYFUC.js +196 -0
- package/dist/chunk-HU4PW6I6.js +264 -0
- package/dist/chunk-KFXF2V5G.js +155 -0
- package/dist/chunk-LFFLHSQO.js +78 -0
- package/dist/chunk-M6RIJVRE.js +18358 -0
- package/dist/chunk-NAHHFSLH.js +258 -0
- package/dist/chunk-OAAP2MY6.js +262 -0
- package/dist/chunk-PCVGBE5B.js +659 -0
- package/dist/chunk-VW3FDYRT.js +517 -0
- package/dist/chunk-WX52U7Z2.js +112 -0
- package/dist/chunk-ZB4F75SZ.js +843 -0
- package/dist/daemon/server.js +21633 -0
- package/dist/hook-runtime/daemon/server.js +21633 -0
- package/dist/hook-runtime/hooks/post-commit.js +19258 -0
- package/dist/hook-runtime/hooks/post-rewrite.js +19273 -0
- package/dist/hook-runtime/hooks/post-tool-use.js +26365 -0
- package/dist/hook-runtime/hooks/pre-commit.js +1314 -0
- package/dist/hook-runtime/hooks/pre-tool-use.js +25832 -0
- package/dist/hook-runtime/hooks/prim-hook.js +26386 -0
- package/dist/hook-runtime/hooks/session-end.js +1078 -0
- package/dist/hook-runtime/hooks/session-start.js +28779 -0
- package/dist/hook-runtime/index.js +50987 -0
- package/dist/hook-runtime/statusline-main.js +809 -0
- package/dist/hooks/post-commit.js +190 -0
- package/dist/hooks/post-rewrite.js +203 -0
- package/dist/hooks/post-tool-use.js +288 -0
- package/dist/hooks/pre-commit.js +16 -101
- package/dist/hooks/pre-tool-use.js +369 -0
- package/dist/hooks/prim-hook.js +229 -0
- package/dist/hooks/session-end.js +59 -0
- package/dist/hooks/session-start.js +316 -0
- package/dist/index.js +8875 -429
- package/dist/statusline-main.js +809 -0
- package/package.json +35 -13
- package/dist/chunk-3APLWTLB.js +0 -165
|
@@ -0,0 +1,1474 @@
|
|
|
1
|
+
import {
|
|
2
|
+
gitToplevel
|
|
3
|
+
} from "./chunk-H4UNYFUC.js";
|
|
4
|
+
import {
|
|
5
|
+
atomicWriteFile,
|
|
6
|
+
primConfigDirectory,
|
|
7
|
+
syncDirectory,
|
|
8
|
+
syncFile,
|
|
9
|
+
withFileLockSync
|
|
10
|
+
} from "./chunk-67VNLLBN.js";
|
|
11
|
+
|
|
12
|
+
// src/lib/bin-path.ts
|
|
13
|
+
import { existsSync, readFileSync } from "fs";
|
|
14
|
+
import { dirname, isAbsolute, join } from "path";
|
|
15
|
+
import { fileURLToPath } from "url";
|
|
16
|
+
|
|
17
|
+
// src/lib/semver.ts
|
|
18
|
+
function parseSemver(value) {
|
|
19
|
+
if (typeof value !== "string" || value.length > 256) return;
|
|
20
|
+
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u.exec(
|
|
21
|
+
value
|
|
22
|
+
);
|
|
23
|
+
if (!match) return;
|
|
24
|
+
const prerelease = match[4]?.split(".") ?? [];
|
|
25
|
+
if (prerelease.some((part) => /^\d+$/u.test(part) && part.length > 1 && part.startsWith("0"))) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
return { core: [BigInt(match[1]), BigInt(match[2]), BigInt(match[3])], prerelease };
|
|
29
|
+
}
|
|
30
|
+
function compareSemver(left, right) {
|
|
31
|
+
const a = parseSemver(left);
|
|
32
|
+
const b = parseSemver(right);
|
|
33
|
+
if (!a || !b) return;
|
|
34
|
+
for (let i = 0; i < a.core.length; i += 1) {
|
|
35
|
+
if (a.core[i] !== b.core[i]) return a.core[i] < b.core[i] ? -1 : 1;
|
|
36
|
+
}
|
|
37
|
+
if (a.prerelease.length === 0 || b.prerelease.length === 0) {
|
|
38
|
+
return a.prerelease.length === b.prerelease.length ? 0 : a.prerelease.length === 0 ? 1 : -1;
|
|
39
|
+
}
|
|
40
|
+
for (let i = 0; i < Math.max(a.prerelease.length, b.prerelease.length); i += 1) {
|
|
41
|
+
const x = a.prerelease[i];
|
|
42
|
+
const y = b.prerelease[i];
|
|
43
|
+
if (x === void 0 || y === void 0) return x === void 0 ? -1 : 1;
|
|
44
|
+
if (x === y) continue;
|
|
45
|
+
const xNumeric = /^\d+$/u.test(x);
|
|
46
|
+
const yNumeric = /^\d+$/u.test(y);
|
|
47
|
+
if (xNumeric && yNumeric) return BigInt(x) < BigInt(y) ? -1 : 1;
|
|
48
|
+
if (xNumeric !== yNumeric) return xNumeric ? -1 : 1;
|
|
49
|
+
return x < y ? -1 : 1;
|
|
50
|
+
}
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/lib/bin-path.ts
|
|
55
|
+
var PKG_NAME = "@primitive.ai/prim";
|
|
56
|
+
var ROOT_WALK_LIMIT = 6;
|
|
57
|
+
var STABLE_HOOK_LAUNCHER_NAME = "prim-hook-launcher-v1";
|
|
58
|
+
var resolvedRoot;
|
|
59
|
+
function locateRoot() {
|
|
60
|
+
if (resolvedRoot !== void 0) {
|
|
61
|
+
return resolvedRoot;
|
|
62
|
+
}
|
|
63
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
64
|
+
for (let depth = 0; depth < ROOT_WALK_LIMIT; depth++) {
|
|
65
|
+
const manifestPath = join(dir, "package.json");
|
|
66
|
+
if (existsSync(manifestPath)) {
|
|
67
|
+
try {
|
|
68
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
69
|
+
if (manifest.name === PKG_NAME && manifest.bin) {
|
|
70
|
+
resolvedRoot = { dir, version: manifest.version, bin: manifest.bin };
|
|
71
|
+
return resolvedRoot;
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const parent = dirname(dir);
|
|
77
|
+
if (parent === dir) {
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
dir = parent;
|
|
81
|
+
}
|
|
82
|
+
resolvedRoot = null;
|
|
83
|
+
return resolvedRoot;
|
|
84
|
+
}
|
|
85
|
+
function binFile(bin) {
|
|
86
|
+
const root = locateRoot();
|
|
87
|
+
const rel = root?.bin[bin];
|
|
88
|
+
if (!root || !rel) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
return isAbsolute(rel) ? rel : join(root.dir, rel);
|
|
92
|
+
}
|
|
93
|
+
function packageVersion() {
|
|
94
|
+
return locateRoot()?.version ?? null;
|
|
95
|
+
}
|
|
96
|
+
function packageRoot() {
|
|
97
|
+
return locateRoot()?.dir ?? null;
|
|
98
|
+
}
|
|
99
|
+
function shellQuote(value) {
|
|
100
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
101
|
+
}
|
|
102
|
+
function shellWord(value) {
|
|
103
|
+
return /^[A-Za-z0-9_@%+=:,./-]+$/u.test(value) ? value : shellQuote(value);
|
|
104
|
+
}
|
|
105
|
+
function pinnedNpxArgs(bin, args = [], options = {}) {
|
|
106
|
+
const version = packageVersion();
|
|
107
|
+
if (!version) throw new Error("cannot determine Primitive package version");
|
|
108
|
+
return [
|
|
109
|
+
"--yes",
|
|
110
|
+
"--ignore-scripts",
|
|
111
|
+
...options.preferOnline ? ["--prefer-online"] : [],
|
|
112
|
+
"-p",
|
|
113
|
+
`${PKG_NAME}@${version}`,
|
|
114
|
+
bin,
|
|
115
|
+
...args
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
function pinnedNpxCommand(bin, args = [], options = {}) {
|
|
119
|
+
return ["npx", ...pinnedNpxArgs(bin, args, options)].map(shellWord).join(" ");
|
|
120
|
+
}
|
|
121
|
+
function pinnedHookCommand(bin, args = "") {
|
|
122
|
+
const suffix = args ? ` ${args}` : "";
|
|
123
|
+
const fallback = `${pinnedNpxCommand(bin)}${suffix}`;
|
|
124
|
+
const file = binFile(bin);
|
|
125
|
+
if (!file) return fallback;
|
|
126
|
+
return `if [ -x ${shellQuote(process.execPath)} ] && [ -f ${shellQuote(file)} ]; then ${shellQuote(process.execPath)} ${shellQuote(file)}${suffix}; else ${fallback}; fi`;
|
|
127
|
+
}
|
|
128
|
+
var STABLE_HOOK_ARGUMENTS_RE = /^[-A-Za-z0-9_ ]*$/u;
|
|
129
|
+
var STABLE_HOOK_BIN_RE = /^[A-Za-z0-9][A-Za-z0-9-]*$/u;
|
|
130
|
+
function stableHookCommand(bin, args = "") {
|
|
131
|
+
if (!STABLE_HOOK_BIN_RE.test(bin) || !STABLE_HOOK_ARGUMENTS_RE.test(args)) {
|
|
132
|
+
throw new Error("invalid stable hook command");
|
|
133
|
+
}
|
|
134
|
+
const resolver = [
|
|
135
|
+
// #242 and earlier identify managed commands by this package marker. Keep
|
|
136
|
+
// it as inert data during the rolling window so an older uninstall or
|
|
137
|
+
// reinstall can remove the stable entry without invoking npm or PATH.
|
|
138
|
+
`prim_legacy_reader='-p ${PKG_NAME}@stable prim-shim.sh ${bin} '; `,
|
|
139
|
+
'prim_absolute() { case "$1" in [/]* ) ;; *) return 1 ;; esac; case "$1" in [[:space:]]*|*[[:space:]]|*[/][/]*|*[/].[/]*|*[/]..[/]*|*[/].|*[/]..|?*[/]) return 1 ;; esac; }; ',
|
|
140
|
+
'prim_config=${PRIM_CONFIG_DIR:-}; if ! prim_absolute "$prim_config"; then prim_config=${XDG_CONFIG_HOME:-}; if prim_absolute "$prim_config"; then ',
|
|
141
|
+
'case "$prim_config" in /) prim_config=/prim ;; *) prim_config="$prim_config/prim" ;; esac; ',
|
|
142
|
+
'else prim_home=${HOME:-}; prim_absolute "$prim_home" || exit 78; case "$prim_home" in /) prim_config=/.config/prim ;; *) prim_config="$prim_home/.config/prim" ;; esac; ',
|
|
143
|
+
"fi; fi; ",
|
|
144
|
+
`case "$prim_config" in /) prim_launcher=/${STABLE_HOOK_LAUNCHER_NAME} ;; *) prim_launcher="$prim_config/${STABLE_HOOK_LAUNCHER_NAME}" ;; esac; `,
|
|
145
|
+
'exec "$prim_launcher" "$@"'
|
|
146
|
+
].join("");
|
|
147
|
+
const suffix = args ? ` ${args}` : "";
|
|
148
|
+
return `/bin/sh -c ${shellQuote(resolver)} ${STABLE_HOOK_LAUNCHER_NAME} ${bin}${suffix}`;
|
|
149
|
+
}
|
|
150
|
+
function detachedHookShimCommand(bin, args = "") {
|
|
151
|
+
return `payload=$(cat); { trap '' HUP; printf '%s' "$payload" | { ${stableHookCommand(bin, args)}; }; } </dev/null >/dev/null 2>&1 &`;
|
|
152
|
+
}
|
|
153
|
+
function commandMatchesBin(command, bin) {
|
|
154
|
+
if (!command) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
const c = command.trim();
|
|
158
|
+
if (c === bin || c.startsWith(`${bin} `)) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
const exactBin = new RegExp(`(?:^|\\s)${bin.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:\\s|;|$)`);
|
|
162
|
+
return c.includes(`command -v ${bin} `) || c.includes(`-p ${PKG_NAME}@`) && exactBin.test(c) || c.includes(STABLE_HOOK_LAUNCHER_NAME) && exactBin.test(c);
|
|
163
|
+
}
|
|
164
|
+
function exactPinnedNpxVersion(command, bin) {
|
|
165
|
+
const escapedBin = bin.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
166
|
+
const shellQuoted = `'(?:[^']|'"'"')*'`;
|
|
167
|
+
const safeArgs = "(?: [A-Za-z0-9_@%+=:,./-]+)*";
|
|
168
|
+
const direct = new RegExp(
|
|
169
|
+
`^npx --yes --ignore-scripts -p @primitive\\.ai/prim@(?<version>[0-9A-Za-z.+-]+) ${escapedBin}${safeArgs}$`,
|
|
170
|
+
"u"
|
|
171
|
+
).exec(command);
|
|
172
|
+
const wrapped = new RegExp(
|
|
173
|
+
`^if \\[ -x (?<node>${shellQuoted}) \\] && \\[ -f (?<entry>${shellQuoted}) \\]; then \\k<node> \\k<entry>(?<args>${safeArgs}); else npx --yes --ignore-scripts -p @primitive\\.ai/prim@(?<version>[0-9A-Za-z.+-]+) ${escapedBin}\\k<args>; fi$`,
|
|
174
|
+
"u"
|
|
175
|
+
).exec(command);
|
|
176
|
+
const version = direct?.groups?.version ?? wrapped?.groups?.version;
|
|
177
|
+
return version && compareSemver(version, version) !== void 0 ? version : void 0;
|
|
178
|
+
}
|
|
179
|
+
function hookCommandResolution(command, bin) {
|
|
180
|
+
const trimmed = command?.trim();
|
|
181
|
+
if (!trimmed || !commandMatchesBin(trimmed, bin)) return void 0;
|
|
182
|
+
if (trimmed.includes(STABLE_HOOK_LAUNCHER_NAME)) return { kind: "stable_launcher" };
|
|
183
|
+
const version = exactPinnedNpxVersion(trimmed, bin);
|
|
184
|
+
return version ? { kind: "exact_npx_fallback", version } : { kind: "legacy_path" };
|
|
185
|
+
}
|
|
186
|
+
function hookCommandResolutions(commands, bins) {
|
|
187
|
+
const resolutions = [];
|
|
188
|
+
for (const command of commands) {
|
|
189
|
+
for (const bin of bins) {
|
|
190
|
+
const resolution = hookCommandResolution(command, bin);
|
|
191
|
+
if (resolution) resolutions.push(resolution);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return resolutions;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/commands/claude-install.ts
|
|
198
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
199
|
+
import { homedir } from "os";
|
|
200
|
+
import { join as join3 } from "path";
|
|
201
|
+
|
|
202
|
+
// src/lib/hook-runtime.ts
|
|
203
|
+
import { createHash, randomBytes } from "crypto";
|
|
204
|
+
import {
|
|
205
|
+
constants,
|
|
206
|
+
accessSync,
|
|
207
|
+
chmodSync,
|
|
208
|
+
copyFileSync,
|
|
209
|
+
existsSync as existsSync2,
|
|
210
|
+
lstatSync,
|
|
211
|
+
mkdirSync,
|
|
212
|
+
mkdtempSync,
|
|
213
|
+
readFileSync as readFileSync2,
|
|
214
|
+
readdirSync,
|
|
215
|
+
realpathSync,
|
|
216
|
+
renameSync,
|
|
217
|
+
rmSync,
|
|
218
|
+
statSync,
|
|
219
|
+
utimesSync,
|
|
220
|
+
writeFileSync
|
|
221
|
+
} from "fs";
|
|
222
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join2, resolve } from "path";
|
|
223
|
+
var HOOK_RUNTIME_SCHEMA_VERSION = 1;
|
|
224
|
+
var DIRECTORY_MODE = 448;
|
|
225
|
+
var DATA_FILE_MODE = 384;
|
|
226
|
+
var LAUNCHER_MODE = 448;
|
|
227
|
+
var RELEASE_PREFIX = "release-";
|
|
228
|
+
var RELEASE_NAME_RE = /^release-[0-9a-f]{64}$/u;
|
|
229
|
+
var MAX_MANIFEST_BYTES = 64 * 1024;
|
|
230
|
+
var MAX_RELEASES = 8;
|
|
231
|
+
var MAX_RELEASE_DELETIONS_PER_STAGE = 8;
|
|
232
|
+
var RETAIN_PREDECESSORS = 2;
|
|
233
|
+
var RELEASE_GRACE_MS = 10 * 60 * 1e3;
|
|
234
|
+
var HOOK_RUNTIME_ENTRIES = {
|
|
235
|
+
// SessionStart self-healing resolves this exact CLI entry to run
|
|
236
|
+
// `daemon ensure`; it must travel with the immutable hook release.
|
|
237
|
+
prim: "dist/index.js",
|
|
238
|
+
"prim-hook": "dist/hooks/prim-hook.js",
|
|
239
|
+
"prim-pre-commit": "dist/hooks/pre-commit.js",
|
|
240
|
+
"prim-post-commit": "dist/hooks/post-commit.js",
|
|
241
|
+
"prim-post-rewrite": "dist/hooks/post-rewrite.js",
|
|
242
|
+
"prim-pre-tool-use": "dist/hooks/pre-tool-use.js",
|
|
243
|
+
"prim-post-tool-use": "dist/hooks/post-tool-use.js",
|
|
244
|
+
"prim-session-start": "dist/hooks/session-start.js",
|
|
245
|
+
"prim-session-end": "dist/hooks/session-end.js",
|
|
246
|
+
"prim-daemon-server": "dist/daemon/server.js",
|
|
247
|
+
"prim-statusline": "dist/statusline-main.js"
|
|
248
|
+
};
|
|
249
|
+
var SOURCE_ENTRIES = Object.fromEntries(
|
|
250
|
+
Object.entries(HOOK_RUNTIME_ENTRIES).map(([bin, target]) => [
|
|
251
|
+
bin,
|
|
252
|
+
target.replace(/^dist\//u, "")
|
|
253
|
+
])
|
|
254
|
+
);
|
|
255
|
+
function hookRuntimePaths(options = {}) {
|
|
256
|
+
const env = options.env ?? process.env;
|
|
257
|
+
const configDir = primConfigDirectory({
|
|
258
|
+
...options,
|
|
259
|
+
env,
|
|
260
|
+
homeDir: options.homeDir ?? env.HOME ?? ""
|
|
261
|
+
});
|
|
262
|
+
const runtimeDir = join2(configDir, "hook-runtime");
|
|
263
|
+
return {
|
|
264
|
+
configDir,
|
|
265
|
+
launcher: join2(configDir, STABLE_HOOK_LAUNCHER_NAME),
|
|
266
|
+
runtimeDir,
|
|
267
|
+
releasesDir: join2(runtimeDir, "releases"),
|
|
268
|
+
current: join2(runtimeDir, "current"),
|
|
269
|
+
selectionLock: join2(runtimeDir, "selection.lock")
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function sha256(path) {
|
|
273
|
+
return createHash("sha256").update(readFileSync2(path)).digest("hex");
|
|
274
|
+
}
|
|
275
|
+
function exactManifest(manifest) {
|
|
276
|
+
return `${JSON.stringify(manifest, null, 2)}
|
|
277
|
+
`;
|
|
278
|
+
}
|
|
279
|
+
function exactKeys(value, expected) {
|
|
280
|
+
const actual = Object.keys(value).sort();
|
|
281
|
+
const wanted = [...expected].sort();
|
|
282
|
+
return actual.length === wanted.length && actual.every((key, index) => key === wanted[index]);
|
|
283
|
+
}
|
|
284
|
+
function releaseId(manifest) {
|
|
285
|
+
return createHash("sha256").update(exactManifest(manifest)).digest("hex");
|
|
286
|
+
}
|
|
287
|
+
function runtimePackageJson(manifest) {
|
|
288
|
+
return `${JSON.stringify(
|
|
289
|
+
{
|
|
290
|
+
name: "@primitive.ai/prim",
|
|
291
|
+
version: manifest.version,
|
|
292
|
+
type: "module",
|
|
293
|
+
bin: HOOK_RUNTIME_ENTRIES
|
|
294
|
+
},
|
|
295
|
+
null,
|
|
296
|
+
2
|
|
297
|
+
)}
|
|
298
|
+
`;
|
|
299
|
+
}
|
|
300
|
+
function validateRelease(releaseDir, manifest) {
|
|
301
|
+
try {
|
|
302
|
+
if (!directReleaseLayout(releaseDir)) return false;
|
|
303
|
+
if (readFileSync2(join2(releaseDir, "manifest.json"), "utf8") !== exactManifest(manifest)) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
if (readFileSync2(join2(releaseDir, "package.json"), "utf8") !== runtimePackageJson(manifest)) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
if (readFileSync2(join2(releaseDir, "node"), "utf8") !== `${manifest.nodePath}
|
|
310
|
+
`) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
for (const [bin, relativePath] of Object.entries(HOOK_RUNTIME_ENTRIES)) {
|
|
314
|
+
const target = join2(releaseDir, relativePath);
|
|
315
|
+
if (sha256(target) !== manifest.files[bin]) return false;
|
|
316
|
+
}
|
|
317
|
+
return true;
|
|
318
|
+
} catch {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function directReleaseLayout(releaseDir) {
|
|
323
|
+
try {
|
|
324
|
+
const directories = /* @__PURE__ */ new Set([releaseDir]);
|
|
325
|
+
for (const relativePath of Object.values(HOOK_RUNTIME_ENTRIES)) {
|
|
326
|
+
for (let directory = dirname2(relativePath); directory !== "."; directory = dirname2(directory)) {
|
|
327
|
+
directories.add(join2(releaseDir, directory));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (![...directories].every((directory) => lstatSync(directory).isDirectory())) return false;
|
|
331
|
+
const files = ["manifest.json", "package.json", "node", ...Object.values(HOOK_RUNTIME_ENTRIES)];
|
|
332
|
+
return files.every((relativePath) => lstatSync(join2(releaseDir, relativePath)).isFile());
|
|
333
|
+
} catch {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function readReleaseManifest(releaseDir, releaseName) {
|
|
338
|
+
try {
|
|
339
|
+
const manifestPath = join2(releaseDir, "manifest.json");
|
|
340
|
+
const manifestStat = statSync(manifestPath);
|
|
341
|
+
if (!manifestStat.isFile() || manifestStat.size > MAX_MANIFEST_BYTES) return null;
|
|
342
|
+
const raw = readFileSync2(manifestPath, "utf8");
|
|
343
|
+
const parsed = JSON.parse(raw);
|
|
344
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
345
|
+
if (!exactKeys(parsed, ["schemaVersion", "version", "nodePath", "files"])) return null;
|
|
346
|
+
const candidate = parsed;
|
|
347
|
+
if (candidate.schemaVersion !== HOOK_RUNTIME_SCHEMA_VERSION || typeof candidate.version !== "string" || compareSemver(candidate.version, candidate.version) !== 0 || typeof candidate.nodePath !== "string" || !isAbsolute2(candidate.nodePath) || /[\r\n]/u.test(candidate.nodePath) || !candidate.files || typeof candidate.files !== "object" || Array.isArray(candidate.files) || !exactKeys(candidate.files, Object.keys(HOOK_RUNTIME_ENTRIES)) || Object.values(candidate.files).some(
|
|
348
|
+
(digest) => typeof digest !== "string" || !/^[0-9a-f]{64}$/u.test(digest)
|
|
349
|
+
)) {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
const manifest = candidate;
|
|
353
|
+
if (raw !== exactManifest(manifest) || releaseName !== `${RELEASE_PREFIX}${releaseId(manifest)}` || !validateRelease(releaseDir, manifest)) {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
return manifest;
|
|
357
|
+
} catch {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function readSelectedRelease(paths) {
|
|
362
|
+
let raw;
|
|
363
|
+
try {
|
|
364
|
+
raw = readFileSync2(paths.current, "utf8");
|
|
365
|
+
} catch (error) {
|
|
366
|
+
if (error.code === "ENOENT") return null;
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
const name = raw.endsWith("\n") ? raw.slice(0, -1) : "";
|
|
370
|
+
if (`${name}
|
|
371
|
+
` !== raw || !RELEASE_NAME_RE.test(name)) {
|
|
372
|
+
throw new Error("cannot stage hook runtime: selected release is malformed");
|
|
373
|
+
}
|
|
374
|
+
const dir = join2(paths.releasesDir, name);
|
|
375
|
+
const manifest = readReleaseManifest(dir, name);
|
|
376
|
+
if (!manifest) {
|
|
377
|
+
throw new Error("cannot stage hook runtime: immutable release is corrupt");
|
|
378
|
+
}
|
|
379
|
+
return { name, dir, manifest };
|
|
380
|
+
}
|
|
381
|
+
function releaseEntries(paths) {
|
|
382
|
+
return readdirSync(paths.releasesDir, { withFileTypes: true }).flatMap((entry) => {
|
|
383
|
+
if (!entry.isDirectory() || !RELEASE_NAME_RE.test(entry.name)) return [];
|
|
384
|
+
const dir = join2(paths.releasesDir, entry.name);
|
|
385
|
+
try {
|
|
386
|
+
return [{ name: entry.name, dir, mtimeMs: statSync(dir).mtimeMs }];
|
|
387
|
+
} catch {
|
|
388
|
+
return [];
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
function pruneReleases(paths, protectedNames, reserveSlots, now) {
|
|
393
|
+
const entries = releaseEntries(paths).sort(
|
|
394
|
+
(left, right) => right.mtimeMs - left.mtimeMs || left.name.localeCompare(right.name)
|
|
395
|
+
);
|
|
396
|
+
const protectedWithPredecessors = new Set(protectedNames);
|
|
397
|
+
for (const entry of entries) {
|
|
398
|
+
if (protectedWithPredecessors.size >= protectedNames.size + RETAIN_PREDECESSORS) break;
|
|
399
|
+
if (!protectedWithPredecessors.has(entry.name)) protectedWithPredecessors.add(entry.name);
|
|
400
|
+
}
|
|
401
|
+
const candidates = entries.filter(
|
|
402
|
+
(entry) => !protectedWithPredecessors.has(entry.name) && now - entry.mtimeMs >= RELEASE_GRACE_MS
|
|
403
|
+
).sort((left, right) => left.mtimeMs - right.mtimeMs || left.name.localeCompare(right.name));
|
|
404
|
+
let remaining = entries.length;
|
|
405
|
+
let deleted = 0;
|
|
406
|
+
for (const entry of candidates) {
|
|
407
|
+
if (remaining + reserveSlots <= MAX_RELEASES || deleted >= MAX_RELEASE_DELETIONS_PER_STAGE) {
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
rmSync(entry.dir, { recursive: true, force: true });
|
|
411
|
+
remaining -= 1;
|
|
412
|
+
deleted += 1;
|
|
413
|
+
}
|
|
414
|
+
if (remaining + reserveSlots > MAX_RELEASES) {
|
|
415
|
+
throw new Error(
|
|
416
|
+
"cannot stage hook runtime: release retention is full while recent runtimes are protected"
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function sourceManifest(sourceDir, version, nodePath) {
|
|
421
|
+
const files = {};
|
|
422
|
+
for (const [bin, relativePath] of Object.entries(SOURCE_ENTRIES)) {
|
|
423
|
+
const source = join2(sourceDir, relativePath);
|
|
424
|
+
if (!existsSync2(source) || !statSync(source).isFile()) {
|
|
425
|
+
throw new Error(`cannot stage hook runtime: missing self-contained ${bin} bundle`);
|
|
426
|
+
}
|
|
427
|
+
files[bin] = sha256(source);
|
|
428
|
+
}
|
|
429
|
+
return { schemaVersion: HOOK_RUNTIME_SCHEMA_VERSION, version, nodePath, files };
|
|
430
|
+
}
|
|
431
|
+
var STABLE_HOOK_LAUNCHER_CONTENT = `#!/bin/sh
|
|
432
|
+
set -eu
|
|
433
|
+
case "$0" in */*) prim_launcher_dir=\${0%/*} ;; *) exit 78 ;; esac
|
|
434
|
+
IFS= read -r prim_release_name < "$prim_launcher_dir/hook-runtime/current" || exit 69
|
|
435
|
+
case "$prim_release_name" in ${RELEASE_PREFIX}*) ;; *) exit 69 ;; esac
|
|
436
|
+
prim_release_id=\${prim_release_name#${RELEASE_PREFIX}}
|
|
437
|
+
case "$prim_release_id" in *[!0-9a-f]*|"") exit 69 ;; esac
|
|
438
|
+
[ "\${#prim_release_id}" -eq 64 ] || exit 69
|
|
439
|
+
prim_release="$prim_launcher_dir/hook-runtime/releases/$prim_release_name"
|
|
440
|
+
case "\${1-}" in
|
|
441
|
+
${Object.entries(HOOK_RUNTIME_ENTRIES).map(([bin, entry]) => ` ${bin}) prim_entry="$prim_release/${entry}" ;;`).join("\n")}
|
|
442
|
+
*) exit 64 ;;
|
|
443
|
+
esac
|
|
444
|
+
shift
|
|
445
|
+
IFS= read -r prim_node < "$prim_release/node" || exit 69
|
|
446
|
+
case "$prim_node" in /*) ;; *) exit 69 ;; esac
|
|
447
|
+
[ -x "$prim_node" ] && [ -f "$prim_entry" ] || exit 69
|
|
448
|
+
exec "$prim_node" "$prim_entry" "$@"
|
|
449
|
+
`;
|
|
450
|
+
function privateDirectory(path) {
|
|
451
|
+
const stat = lstatIfPresent(path);
|
|
452
|
+
if (!stat?.isDirectory()) return false;
|
|
453
|
+
if (process.platform === "win32") return true;
|
|
454
|
+
const uid = process.getuid?.();
|
|
455
|
+
return (Number(stat.mode) & 511) === DIRECTORY_MODE && (uid === void 0 || stat.uid === uid);
|
|
456
|
+
}
|
|
457
|
+
function privateFile(path, mode) {
|
|
458
|
+
const stat = lstatIfPresent(path);
|
|
459
|
+
if (!stat?.isFile()) return false;
|
|
460
|
+
if (process.platform === "win32") return true;
|
|
461
|
+
const uid = process.getuid?.();
|
|
462
|
+
return (Number(stat.mode) & 511) === mode && (uid === void 0 || stat.uid === uid);
|
|
463
|
+
}
|
|
464
|
+
function hasSafeRuntimeLayout(paths, selected) {
|
|
465
|
+
const releaseDirectories = /* @__PURE__ */ new Set([selected.dir]);
|
|
466
|
+
for (const relativePath of Object.values(HOOK_RUNTIME_ENTRIES)) {
|
|
467
|
+
for (let directory = dirname2(relativePath); directory !== "."; directory = dirname2(directory)) {
|
|
468
|
+
releaseDirectories.add(join2(selected.dir, directory));
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (!privateDirectory(paths.configDir) || !privateDirectory(paths.runtimeDir) || !privateDirectory(paths.releasesDir) || ![...releaseDirectories].every(privateDirectory) || !privateFile(paths.launcher, LAUNCHER_MODE) || !privateFile(paths.current, DATA_FILE_MODE) || !privateFile(join2(selected.dir, "manifest.json"), DATA_FILE_MODE) || !privateFile(join2(selected.dir, "package.json"), DATA_FILE_MODE) || !privateFile(join2(selected.dir, "node"), DATA_FILE_MODE)) {
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
return Object.values(HOOK_RUNTIME_ENTRIES).every(
|
|
475
|
+
(relativePath) => privateFile(join2(selected.dir, relativePath), DATA_FILE_MODE)
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
function recordedNodeMatchesCurrentProcess(nodePath) {
|
|
479
|
+
try {
|
|
480
|
+
if (!statSync(nodePath).isFile()) return false;
|
|
481
|
+
accessSync(nodePath, constants.X_OK);
|
|
482
|
+
return realpathSync(nodePath) === realpathSync(process.execPath);
|
|
483
|
+
} catch {
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function inspectHookRuntime(options = {}) {
|
|
488
|
+
let paths;
|
|
489
|
+
try {
|
|
490
|
+
paths = hookRuntimePaths(options);
|
|
491
|
+
} catch {
|
|
492
|
+
return { state: "invalid" };
|
|
493
|
+
}
|
|
494
|
+
const launcher = lstatIfPresent(paths.launcher);
|
|
495
|
+
const selector = lstatIfPresent(paths.current);
|
|
496
|
+
if (!(launcher || selector)) return { state: "missing" };
|
|
497
|
+
if (!(launcher && selector)) return { state: "invalid" };
|
|
498
|
+
try {
|
|
499
|
+
if (readFileSync2(paths.launcher, "utf8") !== STABLE_HOOK_LAUNCHER_CONTENT) {
|
|
500
|
+
return { state: "invalid" };
|
|
501
|
+
}
|
|
502
|
+
const selected = readSelectedRelease(paths);
|
|
503
|
+
if (!selected) return { state: "invalid" };
|
|
504
|
+
assertOwnedHookRuntime(paths);
|
|
505
|
+
if (!hasSafeRuntimeLayout(paths, selected)) return { state: "invalid" };
|
|
506
|
+
return recordedNodeMatchesCurrentProcess(selected.manifest.nodePath) ? { state: "ready", version: selected.manifest.version } : { state: "invalid" };
|
|
507
|
+
} catch {
|
|
508
|
+
return { state: "invalid" };
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function writeRelease(sourceDir, releaseDir, manifest, releasesDir) {
|
|
512
|
+
const stagingDir = mkdtempSync(join2(releasesDir, ".stage-"));
|
|
513
|
+
chmodSync(stagingDir, DIRECTORY_MODE);
|
|
514
|
+
try {
|
|
515
|
+
const directories = /* @__PURE__ */ new Set([stagingDir]);
|
|
516
|
+
const syncStagedFile = (path) => {
|
|
517
|
+
syncFile(path);
|
|
518
|
+
for (let directory = dirname2(path); ; directory = dirname2(directory)) {
|
|
519
|
+
directories.add(directory);
|
|
520
|
+
if (directory === stagingDir) break;
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
for (const [bin, targetRelative] of Object.entries(HOOK_RUNTIME_ENTRIES)) {
|
|
524
|
+
const source = join2(sourceDir, SOURCE_ENTRIES[bin]);
|
|
525
|
+
const target = join2(stagingDir, targetRelative);
|
|
526
|
+
mkdirSync(dirname2(target), { recursive: true, mode: DIRECTORY_MODE });
|
|
527
|
+
chmodSync(dirname2(target), DIRECTORY_MODE);
|
|
528
|
+
copyFileSync(source, target);
|
|
529
|
+
chmodSync(target, DATA_FILE_MODE);
|
|
530
|
+
if (sha256(target) !== manifest.files[bin]) {
|
|
531
|
+
throw new Error(`cannot stage hook runtime: ${bin} changed while it was copied`);
|
|
532
|
+
}
|
|
533
|
+
syncStagedFile(target);
|
|
534
|
+
}
|
|
535
|
+
const nodePath = join2(stagingDir, "node");
|
|
536
|
+
writeFileSync(nodePath, `${manifest.nodePath}
|
|
537
|
+
`, {
|
|
538
|
+
encoding: "utf8",
|
|
539
|
+
flag: "wx",
|
|
540
|
+
mode: DATA_FILE_MODE
|
|
541
|
+
});
|
|
542
|
+
syncStagedFile(nodePath);
|
|
543
|
+
const packageJsonPath = join2(stagingDir, "package.json");
|
|
544
|
+
writeFileSync(packageJsonPath, runtimePackageJson(manifest), {
|
|
545
|
+
encoding: "utf8",
|
|
546
|
+
flag: "wx",
|
|
547
|
+
mode: DATA_FILE_MODE
|
|
548
|
+
});
|
|
549
|
+
syncStagedFile(packageJsonPath);
|
|
550
|
+
const manifestPath = join2(stagingDir, "manifest.json");
|
|
551
|
+
writeFileSync(manifestPath, exactManifest(manifest), {
|
|
552
|
+
encoding: "utf8",
|
|
553
|
+
flag: "wx",
|
|
554
|
+
mode: DATA_FILE_MODE
|
|
555
|
+
});
|
|
556
|
+
syncStagedFile(manifestPath);
|
|
557
|
+
for (const directory of [...directories].sort((left, right) => right.length - left.length)) {
|
|
558
|
+
syncDirectory(directory);
|
|
559
|
+
}
|
|
560
|
+
try {
|
|
561
|
+
renameSync(stagingDir, releaseDir);
|
|
562
|
+
} catch (error) {
|
|
563
|
+
const code = error.code;
|
|
564
|
+
if (code !== "EEXIST" && code !== "ENOTEMPTY" || !validateRelease(releaseDir, manifest)) {
|
|
565
|
+
throw error;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
} finally {
|
|
569
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
function stageHookRuntime(options = {}) {
|
|
573
|
+
const root = packageRoot();
|
|
574
|
+
const sourceDir = options.sourceDir ?? (root ? join2(root, "dist", "hook-runtime") : null);
|
|
575
|
+
const version = options.version ?? packageVersion();
|
|
576
|
+
const nodePath = resolve(options.nodePath ?? process.execPath);
|
|
577
|
+
if (!sourceDir || !version || compareSemver(version, version) !== 0 || !isAbsolute2(nodePath) || /[\r\n]/u.test(nodePath)) {
|
|
578
|
+
throw new Error("cannot stage hook runtime: package identity is unavailable");
|
|
579
|
+
}
|
|
580
|
+
const manifest = sourceManifest(sourceDir, version, nodePath);
|
|
581
|
+
const paths = hookRuntimePaths(options);
|
|
582
|
+
const releaseName = `${RELEASE_PREFIX}${releaseId(manifest)}`;
|
|
583
|
+
const releaseDir = join2(paths.releasesDir, releaseName);
|
|
584
|
+
mkdirSync(paths.releasesDir, { recursive: true, mode: DIRECTORY_MODE });
|
|
585
|
+
chmodSync(paths.configDir, DIRECTORY_MODE);
|
|
586
|
+
chmodSync(paths.runtimeDir, DIRECTORY_MODE);
|
|
587
|
+
chmodSync(paths.releasesDir, DIRECTORY_MODE);
|
|
588
|
+
return withFileLockSync(paths.selectionLock, () => {
|
|
589
|
+
const now = Date.now();
|
|
590
|
+
const selected = readSelectedRelease(paths);
|
|
591
|
+
if (selected) utimesSync(selected.dir, now / 1e3, now / 1e3);
|
|
592
|
+
const precedence = selected ? compareSemver(selected.manifest.version, manifest.version) : -1;
|
|
593
|
+
if (precedence === void 0) {
|
|
594
|
+
throw new Error("cannot stage hook runtime: selected release version is malformed");
|
|
595
|
+
}
|
|
596
|
+
const selectRequested = !selected || precedence < 0;
|
|
597
|
+
const requestedExists = existsSync2(releaseDir);
|
|
598
|
+
const protectedBefore = new Set(selected ? [selected.name] : []);
|
|
599
|
+
if (selectRequested && requestedExists) protectedBefore.add(releaseName);
|
|
600
|
+
pruneReleases(paths, protectedBefore, selectRequested && !requestedExists ? 1 : 0, now);
|
|
601
|
+
if (selectRequested && !validateRelease(releaseDir, manifest)) {
|
|
602
|
+
if (requestedExists) {
|
|
603
|
+
throw new Error("cannot stage hook runtime: immutable release is corrupt");
|
|
604
|
+
}
|
|
605
|
+
writeRelease(sourceDir, releaseDir, manifest, paths.releasesDir);
|
|
606
|
+
}
|
|
607
|
+
const finalRelease = selectRequested ? { name: releaseName, dir: releaseDir, manifest } : selected;
|
|
608
|
+
const launcherCurrent = (() => {
|
|
609
|
+
try {
|
|
610
|
+
return readFileSync2(paths.launcher, "utf8") === STABLE_HOOK_LAUNCHER_CONTENT;
|
|
611
|
+
} catch {
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
})();
|
|
615
|
+
const selectorCurrent = selected?.name === finalRelease.name;
|
|
616
|
+
if (!launcherCurrent) {
|
|
617
|
+
atomicWriteFile(paths.launcher, STABLE_HOOK_LAUNCHER_CONTENT, {
|
|
618
|
+
ensureParent: true,
|
|
619
|
+
mode: LAUNCHER_MODE
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
if (!selectorCurrent) {
|
|
623
|
+
syncDirectory(paths.releasesDir);
|
|
624
|
+
atomicWriteFile(paths.current, `${finalRelease.name}
|
|
625
|
+
`, {
|
|
626
|
+
ensureParent: true,
|
|
627
|
+
mode: DATA_FILE_MODE
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
utimesSync(finalRelease.dir, now / 1e3, now / 1e3);
|
|
631
|
+
pruneReleases(
|
|
632
|
+
paths,
|
|
633
|
+
/* @__PURE__ */ new Set([finalRelease.name, ...selected ? [selected.name] : []]),
|
|
634
|
+
0,
|
|
635
|
+
now
|
|
636
|
+
);
|
|
637
|
+
return {
|
|
638
|
+
changed: !launcherCurrent || !selectorCurrent,
|
|
639
|
+
releaseDir: finalRelease.dir,
|
|
640
|
+
manifest: finalRelease.manifest,
|
|
641
|
+
paths
|
|
642
|
+
};
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
function lstatIfPresent(path) {
|
|
646
|
+
try {
|
|
647
|
+
return lstatSync(path);
|
|
648
|
+
} catch (error) {
|
|
649
|
+
const code = error.code;
|
|
650
|
+
if (code === "ENOENT" || code === "ENOTDIR") return void 0;
|
|
651
|
+
throw error;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
function assertOwnedHookRuntime(paths) {
|
|
655
|
+
const launcher = lstatIfPresent(paths.launcher);
|
|
656
|
+
if (launcher) {
|
|
657
|
+
if (!launcher.isFile() || readFileSync2(paths.launcher, "utf8") !== STABLE_HOOK_LAUNCHER_CONTENT) {
|
|
658
|
+
throw new Error(`refusing to remove unrecognized hook launcher at ${paths.launcher}`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
const runtime = lstatIfPresent(paths.runtimeDir);
|
|
662
|
+
if (!runtime) return;
|
|
663
|
+
if (!runtime.isDirectory()) {
|
|
664
|
+
throw new Error(`refusing to remove non-directory hook runtime at ${paths.runtimeDir}`);
|
|
665
|
+
}
|
|
666
|
+
const allowed = /* @__PURE__ */ new Set(["current", "releases", "selection.lock"]);
|
|
667
|
+
const unexpected = readdirSync(paths.runtimeDir).filter((entry) => !allowed.has(entry));
|
|
668
|
+
if (unexpected.length > 0) {
|
|
669
|
+
throw new Error(
|
|
670
|
+
`refusing to remove hook runtime with unrecognized entries: ${unexpected.sort().join(", ")}`
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
const current = lstatIfPresent(paths.current);
|
|
674
|
+
const releases = lstatIfPresent(paths.releasesDir);
|
|
675
|
+
if (current) {
|
|
676
|
+
if (!current.isFile() || !releases || !readSelectedRelease(paths)) {
|
|
677
|
+
throw new Error(`refusing to remove malformed hook runtime selection at ${paths.current}`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (!releases) return;
|
|
681
|
+
if (!releases.isDirectory()) {
|
|
682
|
+
throw new Error(`refusing to remove non-directory hook releases at ${paths.releasesDir}`);
|
|
683
|
+
}
|
|
684
|
+
for (const entry of readdirSync(paths.releasesDir, { withFileTypes: true })) {
|
|
685
|
+
const releaseDir = join2(paths.releasesDir, entry.name);
|
|
686
|
+
if (!entry.isDirectory() || !RELEASE_NAME_RE.test(entry.name) || !readReleaseManifest(releaseDir, entry.name)) {
|
|
687
|
+
throw new Error(`refusing to remove unrecognized hook release ${releaseDir}`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
function removeHookRuntime(options = {}) {
|
|
692
|
+
const paths = hookRuntimePaths(options);
|
|
693
|
+
const hadLauncher = lstatIfPresent(paths.launcher) !== void 0;
|
|
694
|
+
const hadRuntime = lstatIfPresent(paths.runtimeDir) !== void 0;
|
|
695
|
+
if (!hadLauncher && !hadRuntime) return { changed: false, paths };
|
|
696
|
+
assertOwnedHookRuntime(paths);
|
|
697
|
+
if (!hadRuntime) {
|
|
698
|
+
rmSync(paths.launcher, { force: true });
|
|
699
|
+
return { changed: hadLauncher, paths };
|
|
700
|
+
}
|
|
701
|
+
withFileLockSync(paths.selectionLock, () => {
|
|
702
|
+
assertOwnedHookRuntime(paths);
|
|
703
|
+
if (lstatIfPresent(paths.launcher)) rmSync(paths.launcher, { force: true });
|
|
704
|
+
const quarantined = `${paths.runtimeDir}.uninstall-${String(process.pid)}-${randomBytes(8).toString("hex")}`;
|
|
705
|
+
renameSync(paths.runtimeDir, quarantined);
|
|
706
|
+
rmSync(quarantined, { recursive: true, force: true });
|
|
707
|
+
});
|
|
708
|
+
return { changed: true, paths };
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// src/commands/claude-install.ts
|
|
712
|
+
var CAPTURE_BIN = "prim-hook";
|
|
713
|
+
var GATE_BIN = "prim-pre-tool-use";
|
|
714
|
+
var POST_TOOL_USE_BIN = "prim-post-tool-use";
|
|
715
|
+
var SESSION_START_BIN = "prim-session-start";
|
|
716
|
+
var SESSION_END_BIN = "prim-session-end";
|
|
717
|
+
var STATUSLINE_BIN = "prim-statusline";
|
|
718
|
+
var STATUSLINE_ARGS = "";
|
|
719
|
+
var STATUSLINE_COMMAND = stableHookCommand(STATUSLINE_BIN, STATUSLINE_ARGS);
|
|
720
|
+
var STATUSLINE_REFRESH_SECONDS = 5;
|
|
721
|
+
var PRIM_BINS = [
|
|
722
|
+
CAPTURE_BIN,
|
|
723
|
+
GATE_BIN,
|
|
724
|
+
POST_TOOL_USE_BIN,
|
|
725
|
+
SESSION_START_BIN,
|
|
726
|
+
SESSION_END_BIN
|
|
727
|
+
];
|
|
728
|
+
var JSON_INDENT = 2;
|
|
729
|
+
var USER_SCOPE_PATH = join3(homedir(), ".claude", "settings.json");
|
|
730
|
+
function projectRoot() {
|
|
731
|
+
return gitToplevel() ?? process.cwd();
|
|
732
|
+
}
|
|
733
|
+
var projectScopePath = () => join3(projectRoot(), ".claude", "settings.json");
|
|
734
|
+
var CAPTURE_EVENTS = [
|
|
735
|
+
"SessionStart",
|
|
736
|
+
"UserPromptSubmit",
|
|
737
|
+
"PreToolUse",
|
|
738
|
+
"PostToolUse",
|
|
739
|
+
"PostToolUseFailure",
|
|
740
|
+
"Stop",
|
|
741
|
+
"SessionEnd",
|
|
742
|
+
"SubagentStop"
|
|
743
|
+
];
|
|
744
|
+
function makeRegistration(event, matcher, bin, args = "") {
|
|
745
|
+
return {
|
|
746
|
+
event,
|
|
747
|
+
matcher,
|
|
748
|
+
bin,
|
|
749
|
+
command: stableHookCommand(bin, args)
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
function makeDetachedRegistration(event, matcher, bin, args = "") {
|
|
753
|
+
return { event, matcher, bin, command: detachedHookShimCommand(bin, args) };
|
|
754
|
+
}
|
|
755
|
+
var REGISTRATIONS = [
|
|
756
|
+
...CAPTURE_EVENTS.map(
|
|
757
|
+
(event) => event === "SessionEnd" ? makeDetachedRegistration(event, "*", CAPTURE_BIN) : makeRegistration(event, "*", CAPTURE_BIN)
|
|
758
|
+
),
|
|
759
|
+
makeRegistration("PreToolUse", "Edit|Write|MultiEdit|NotebookEdit|Bash", GATE_BIN),
|
|
760
|
+
makeRegistration("PostToolUse", "Edit|Write|MultiEdit|NotebookEdit|Bash", POST_TOOL_USE_BIN),
|
|
761
|
+
makeRegistration(
|
|
762
|
+
"PostToolUseFailure",
|
|
763
|
+
"Edit|Write|MultiEdit|NotebookEdit|Bash",
|
|
764
|
+
POST_TOOL_USE_BIN
|
|
765
|
+
),
|
|
766
|
+
makeRegistration("SessionStart", "*", SESSION_START_BIN),
|
|
767
|
+
makeDetachedRegistration("SessionEnd", "*", SESSION_END_BIN)
|
|
768
|
+
];
|
|
769
|
+
var PRIM_PERMISSION_RULE = "Bash(npx --yes @primitive.ai/prim*)";
|
|
770
|
+
var LEGACY_PRIM_PERMISSION_RULES = [
|
|
771
|
+
"Bash(npx --yes @primitive.ai/prim@latest:*)",
|
|
772
|
+
"Bash(npx --yes @primitive.ai/prim:*)"
|
|
773
|
+
];
|
|
774
|
+
var ALL_PRIM_PERMISSION_RULES = /* @__PURE__ */ new Set([
|
|
775
|
+
PRIM_PERMISSION_RULE,
|
|
776
|
+
...LEGACY_PRIM_PERMISSION_RULES
|
|
777
|
+
]);
|
|
778
|
+
var PRIM_AUTOMODE_TRUST = "Primitive CLI (prim): the @primitive.ai/prim npm package \u2014 run via `npx --yes @primitive.ai/prim \u2026` \u2014 and its API at api.getprimitive.ai are trusted developer tooling this user installed for passive decision capture, conflict checks, and team presence. Running prim and its routine API/daemon calls is expected, not untrusted code execution or data exfiltration.";
|
|
779
|
+
var AUTOMODE_DEFAULTS = "$defaults";
|
|
780
|
+
function settingsPathFor(scope) {
|
|
781
|
+
return scope === "user" ? USER_SCOPE_PATH : projectScopePath();
|
|
782
|
+
}
|
|
783
|
+
function readSettings(path) {
|
|
784
|
+
if (!existsSync3(path)) {
|
|
785
|
+
return {};
|
|
786
|
+
}
|
|
787
|
+
const raw = readFileSync3(path, "utf-8");
|
|
788
|
+
try {
|
|
789
|
+
return JSON.parse(raw);
|
|
790
|
+
} catch (err) {
|
|
791
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
792
|
+
throw new Error(`${path} is not valid JSON: ${detail}`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
function entryHasCommand(entry, bin) {
|
|
796
|
+
return entry.hooks?.some((h) => commandMatchesBin(h.command, bin)) ?? false;
|
|
797
|
+
}
|
|
798
|
+
function ownedRegistrationEntries(settings) {
|
|
799
|
+
return Object.entries(settings.hooks ?? {}).flatMap(
|
|
800
|
+
([event, entries]) => (entries ?? []).filter((entry) => PRIM_BINS.some((bin) => entryHasCommand(entry, bin))).map((entry) => ({ event, entry }))
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
function hasAnyHookRegistration(settings) {
|
|
804
|
+
return ownedRegistrationEntries(settings).length > 0;
|
|
805
|
+
}
|
|
806
|
+
function entryMatchesRegistration(owned, registration) {
|
|
807
|
+
return owned.event === registration.event && owned.entry.matcher === registration.matcher && owned.entry.hooks?.length === 1 && owned.entry.hooks[0].type === "command" && owned.entry.hooks[0].command === registration.command;
|
|
808
|
+
}
|
|
809
|
+
function hasCompleteHookRegistration(settings) {
|
|
810
|
+
const owned = ownedRegistrationEntries(settings);
|
|
811
|
+
return owned.length === REGISTRATIONS.length && REGISTRATIONS.every(
|
|
812
|
+
(registration) => owned.filter((entry) => entryMatchesRegistration(entry, registration)).length === 1
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
function canonicalEntry(reg) {
|
|
816
|
+
return { matcher: reg.matcher, hooks: [{ type: "command", command: reg.command }] };
|
|
817
|
+
}
|
|
818
|
+
function stripCommand(list, bin) {
|
|
819
|
+
const out = [];
|
|
820
|
+
for (const e of list) {
|
|
821
|
+
const hooks = (e.hooks ?? []).filter((h) => !commandMatchesBin(h.command, bin));
|
|
822
|
+
if (hooks.length > 0) {
|
|
823
|
+
out.push({ ...e, hooks });
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return out;
|
|
827
|
+
}
|
|
828
|
+
function ensureRegistration(list, reg, force) {
|
|
829
|
+
const isCanonical = (e) => e.matcher === reg.matcher && e.hooks?.length === 1 && e.hooks[0].command === reg.command;
|
|
830
|
+
const hasStray = (e) => !isCanonical(e) && (e.hooks ?? []).some((h) => commandMatchesBin(h.command, reg.bin));
|
|
831
|
+
if (list.some(isCanonical) && !list.some(hasStray) && !force) {
|
|
832
|
+
return list;
|
|
833
|
+
}
|
|
834
|
+
return [...stripCommand(list, reg.bin), canonicalEntry(reg)];
|
|
835
|
+
}
|
|
836
|
+
var LEGACY_STATUSLINE_COMMAND = "prim statusline";
|
|
837
|
+
var RUNTIME_STATUSLINE_SUFFIXES = [
|
|
838
|
+
"/prim/runtime/prim-statusline",
|
|
839
|
+
// One pre-launcher build persisted the JS entry directly. Keep recognizing
|
|
840
|
+
// it so reinstall migrates and uninstall removes that older staged form.
|
|
841
|
+
"/prim/runtime/current/prim-statusline"
|
|
842
|
+
];
|
|
843
|
+
function isPrimStatusLine(settings) {
|
|
844
|
+
const s = settings.statusLine;
|
|
845
|
+
if (s?.type !== "command") {
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
const c = (s.command ?? "").trim();
|
|
849
|
+
return c === LEGACY_STATUSLINE_COMMAND || c.includes("@primitive.ai/prim") && c.includes("statusline") || c.includes("prim-hook-launcher-v1") && commandMatchesBin(c, STATUSLINE_BIN) || RUNTIME_STATUSLINE_SUFFIXES.some((suffix) => c.includes(suffix));
|
|
850
|
+
}
|
|
851
|
+
function hookRuntimeResolutions(settings) {
|
|
852
|
+
const resolutions = hookCommandResolutions(
|
|
853
|
+
Object.values(settings.hooks ?? {}).flatMap(
|
|
854
|
+
(entries) => (entries ?? []).flatMap((entry) => (entry.hooks ?? []).map((hook) => hook.command))
|
|
855
|
+
),
|
|
856
|
+
PRIM_BINS
|
|
857
|
+
);
|
|
858
|
+
if (!isPrimStatusLine(settings)) return resolutions;
|
|
859
|
+
const statusline = hookCommandResolution(settings.statusLine?.command, STATUSLINE_BIN);
|
|
860
|
+
resolutions.push(statusline ?? { kind: "legacy_path" });
|
|
861
|
+
return resolutions;
|
|
862
|
+
}
|
|
863
|
+
function inspectHookRuntimeResolutions() {
|
|
864
|
+
return [
|
|
865
|
+
...hookRuntimeResolutions(readSettings(USER_SCOPE_PATH)),
|
|
866
|
+
...hookRuntimeResolutions(readSettings(projectScopePath()))
|
|
867
|
+
];
|
|
868
|
+
}
|
|
869
|
+
function applyStatusLine(settings, command = STATUSLINE_COMMAND) {
|
|
870
|
+
if (command === false) {
|
|
871
|
+
return isPrimStatusLine(settings) ? { ...settings, statusLine: void 0 } : settings;
|
|
872
|
+
}
|
|
873
|
+
if (settings.statusLine && !isPrimStatusLine(settings)) {
|
|
874
|
+
return settings;
|
|
875
|
+
}
|
|
876
|
+
return {
|
|
877
|
+
...settings,
|
|
878
|
+
statusLine: {
|
|
879
|
+
type: "command",
|
|
880
|
+
command,
|
|
881
|
+
refreshInterval: STATUSLINE_REFRESH_SECONDS
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
function applyPermissions(settings) {
|
|
886
|
+
const permissions = settings.permissions ?? {};
|
|
887
|
+
const allow = permissions.allow ?? [];
|
|
888
|
+
const hasCanonical = allow.includes(PRIM_PERMISSION_RULE);
|
|
889
|
+
const hasLegacy = allow.some((rule) => LEGACY_PRIM_PERMISSION_RULES.includes(rule));
|
|
890
|
+
if (hasCanonical && !hasLegacy) {
|
|
891
|
+
return settings;
|
|
892
|
+
}
|
|
893
|
+
const withoutPrim = allow.filter((rule) => !ALL_PRIM_PERMISSION_RULES.has(rule));
|
|
894
|
+
return {
|
|
895
|
+
...settings,
|
|
896
|
+
permissions: { ...permissions, allow: [...withoutPrim, PRIM_PERMISSION_RULE] }
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
function removePrimPermission(settings) {
|
|
900
|
+
const permissions = settings.permissions;
|
|
901
|
+
if (!permissions?.allow?.some((rule) => ALL_PRIM_PERMISSION_RULES.has(rule))) {
|
|
902
|
+
return settings;
|
|
903
|
+
}
|
|
904
|
+
const allow = permissions.allow.filter((rule) => !ALL_PRIM_PERMISSION_RULES.has(rule));
|
|
905
|
+
const nextPermissions = {
|
|
906
|
+
...permissions,
|
|
907
|
+
allow: allow.length > 0 ? allow : void 0
|
|
908
|
+
};
|
|
909
|
+
const hasOtherPerms = Object.values(nextPermissions).some((v) => v !== void 0);
|
|
910
|
+
return { ...settings, permissions: hasOtherPerms ? nextPermissions : void 0 };
|
|
911
|
+
}
|
|
912
|
+
function applyAutoModeTrust(settings) {
|
|
913
|
+
const autoMode = settings.autoMode ?? {};
|
|
914
|
+
const env = autoMode.environment;
|
|
915
|
+
if (env?.includes(PRIM_AUTOMODE_TRUST)) {
|
|
916
|
+
return settings;
|
|
917
|
+
}
|
|
918
|
+
const nextEnv = env ? [...env, PRIM_AUTOMODE_TRUST] : [AUTOMODE_DEFAULTS, PRIM_AUTOMODE_TRUST];
|
|
919
|
+
return { ...settings, autoMode: { ...autoMode, environment: nextEnv } };
|
|
920
|
+
}
|
|
921
|
+
function removeAutoModeTrust(settings) {
|
|
922
|
+
const autoMode = settings.autoMode;
|
|
923
|
+
if (!autoMode?.environment?.includes(PRIM_AUTOMODE_TRUST)) {
|
|
924
|
+
return settings;
|
|
925
|
+
}
|
|
926
|
+
const env = autoMode.environment.filter((e) => e !== PRIM_AUTOMODE_TRUST);
|
|
927
|
+
const onlyDefaultsLeft = env.length === 0 || env.length === 1 && env[0] === AUTOMODE_DEFAULTS;
|
|
928
|
+
const nextAutoMode = {
|
|
929
|
+
...autoMode,
|
|
930
|
+
environment: onlyDefaultsLeft ? void 0 : env
|
|
931
|
+
};
|
|
932
|
+
const hasOther = Object.values(nextAutoMode).some((v) => v !== void 0);
|
|
933
|
+
return { ...settings, autoMode: hasOther ? nextAutoMode : void 0 };
|
|
934
|
+
}
|
|
935
|
+
function applyInstall(settings, options = {}) {
|
|
936
|
+
const hooks = { ...settings.hooks ?? {} };
|
|
937
|
+
for (const reg of REGISTRATIONS) {
|
|
938
|
+
hooks[reg.event] = ensureRegistration(hooks[reg.event] ?? [], reg, options.force ?? false);
|
|
939
|
+
}
|
|
940
|
+
return applyPermissions(
|
|
941
|
+
applyStatusLine(
|
|
942
|
+
{ ...settings, hooks },
|
|
943
|
+
options.installStatusline === false ? false : options.statuslineCommand ?? STATUSLINE_COMMAND
|
|
944
|
+
)
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
function applyUninstall(settings) {
|
|
948
|
+
const source = settings.hooks ?? {};
|
|
949
|
+
const hooks = {};
|
|
950
|
+
for (const event of Object.keys(source)) {
|
|
951
|
+
let list = source[event] ?? [];
|
|
952
|
+
for (const bin of PRIM_BINS) {
|
|
953
|
+
list = stripCommand(list, bin);
|
|
954
|
+
}
|
|
955
|
+
if (list.length > 0) {
|
|
956
|
+
hooks[event] = list;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
const next = { ...settings, hooks };
|
|
960
|
+
if (isPrimStatusLine(next)) {
|
|
961
|
+
next.statusLine = void 0;
|
|
962
|
+
}
|
|
963
|
+
return removeAutoModeTrust(removePrimPermission(next));
|
|
964
|
+
}
|
|
965
|
+
function captureInstalled(settings) {
|
|
966
|
+
return CAPTURE_EVENTS.some(
|
|
967
|
+
(event) => (settings.hooks?.[event] ?? []).some((e) => entryHasCommand(e, CAPTURE_BIN))
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
function feedbackInstalled(settings) {
|
|
971
|
+
const stopCapture = (settings.hooks?.Stop ?? []).some(
|
|
972
|
+
(entry) => entryHasCommand(entry, CAPTURE_BIN)
|
|
973
|
+
);
|
|
974
|
+
const sessionStart = (settings.hooks?.SessionStart ?? []).some(
|
|
975
|
+
(entry) => entryHasCommand(entry, SESSION_START_BIN)
|
|
976
|
+
);
|
|
977
|
+
return stopCapture && sessionStart;
|
|
978
|
+
}
|
|
979
|
+
function statuslineInstalled(settings) {
|
|
980
|
+
return isPrimStatusLine(settings);
|
|
981
|
+
}
|
|
982
|
+
function statuslineState(settings) {
|
|
983
|
+
if (isPrimStatusLine(settings)) {
|
|
984
|
+
return "primitive";
|
|
985
|
+
}
|
|
986
|
+
return settings.statusLine ? "custom" : "empty";
|
|
987
|
+
}
|
|
988
|
+
function isGateInstalled(settings) {
|
|
989
|
+
return (settings.hooks?.PreToolUse ?? []).some((e) => entryHasCommand(e, GATE_BIN));
|
|
990
|
+
}
|
|
991
|
+
function atomicWrite(path, content) {
|
|
992
|
+
atomicWriteFile(path, `${JSON.stringify(content, null, JSON_INDENT)}
|
|
993
|
+
`, {
|
|
994
|
+
ensureParent: true
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
function userStatuslineBlocksProjectInstall(scope, userSettings) {
|
|
998
|
+
return scope === "project" && statuslineState(userSettings) === "custom";
|
|
999
|
+
}
|
|
1000
|
+
function performInstall(scope, force) {
|
|
1001
|
+
stageHookRuntime();
|
|
1002
|
+
const path = settingsPathFor(scope);
|
|
1003
|
+
const before = readSettings(path);
|
|
1004
|
+
const blockedByUserStatusline = userStatuslineBlocksProjectInstall(
|
|
1005
|
+
scope,
|
|
1006
|
+
scope === "project" ? readSettings(USER_SCOPE_PATH) : {}
|
|
1007
|
+
);
|
|
1008
|
+
const after = applyInstall(before, {
|
|
1009
|
+
force,
|
|
1010
|
+
statuslineCommand: STATUSLINE_COMMAND,
|
|
1011
|
+
installStatusline: !blockedByUserStatusline
|
|
1012
|
+
});
|
|
1013
|
+
const changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
1014
|
+
if (changed) {
|
|
1015
|
+
atomicWrite(path, after);
|
|
1016
|
+
}
|
|
1017
|
+
return {
|
|
1018
|
+
scope,
|
|
1019
|
+
path,
|
|
1020
|
+
gate: isGateInstalled(after),
|
|
1021
|
+
capture: captureInstalled(after),
|
|
1022
|
+
feedback: feedbackInstalled(after),
|
|
1023
|
+
statusline: statuslineInstalled(after),
|
|
1024
|
+
statuslineState: statuslineState(after),
|
|
1025
|
+
changed
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
function performUninstall(scope) {
|
|
1029
|
+
const path = settingsPathFor(scope);
|
|
1030
|
+
const before = readSettings(path);
|
|
1031
|
+
const after = applyUninstall(before);
|
|
1032
|
+
const changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
1033
|
+
if (changed) {
|
|
1034
|
+
atomicWrite(path, after);
|
|
1035
|
+
}
|
|
1036
|
+
return {
|
|
1037
|
+
scope,
|
|
1038
|
+
path,
|
|
1039
|
+
gate: isGateInstalled(after),
|
|
1040
|
+
capture: captureInstalled(after),
|
|
1041
|
+
feedback: feedbackInstalled(after),
|
|
1042
|
+
statusline: statuslineInstalled(after),
|
|
1043
|
+
statuslineState: statuslineState(after),
|
|
1044
|
+
changed
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
function performPermissionInstall(scope) {
|
|
1048
|
+
const path = settingsPathFor(scope);
|
|
1049
|
+
const before = readSettings(path);
|
|
1050
|
+
const after = applyAutoModeTrust(applyPermissions(before));
|
|
1051
|
+
const changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
1052
|
+
if (changed) {
|
|
1053
|
+
atomicWrite(path, after);
|
|
1054
|
+
}
|
|
1055
|
+
return {
|
|
1056
|
+
scope,
|
|
1057
|
+
path,
|
|
1058
|
+
allowed: (after.permissions?.allow ?? []).includes(PRIM_PERMISSION_RULE),
|
|
1059
|
+
autoModeTrusted: (after.autoMode?.environment ?? []).includes(PRIM_AUTOMODE_TRUST),
|
|
1060
|
+
changed
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
function performStatus() {
|
|
1064
|
+
const statusFor2 = (path) => {
|
|
1065
|
+
const settings = readSettings(path);
|
|
1066
|
+
return {
|
|
1067
|
+
path,
|
|
1068
|
+
present: hasAnyHookRegistration(settings) || statuslineInstalled(settings),
|
|
1069
|
+
gate: isGateInstalled(settings),
|
|
1070
|
+
capture: captureInstalled(settings),
|
|
1071
|
+
feedback: feedbackInstalled(settings),
|
|
1072
|
+
complete: hasCompleteHookRegistration(settings),
|
|
1073
|
+
statusline: statuslineInstalled(settings),
|
|
1074
|
+
statuslineState: statuslineState(settings)
|
|
1075
|
+
};
|
|
1076
|
+
};
|
|
1077
|
+
return { user: statusFor2(USER_SCOPE_PATH), project: statusFor2(projectScopePath()) };
|
|
1078
|
+
}
|
|
1079
|
+
function resolveScope(input) {
|
|
1080
|
+
if (input === void 0 || input === "project") {
|
|
1081
|
+
return "project";
|
|
1082
|
+
}
|
|
1083
|
+
if (input === "user") {
|
|
1084
|
+
return "user";
|
|
1085
|
+
}
|
|
1086
|
+
console.error(`[prim] unknown --scope "${input}" (expected: user or project)`);
|
|
1087
|
+
process.exit(1);
|
|
1088
|
+
}
|
|
1089
|
+
function registerClaudeCommands(program) {
|
|
1090
|
+
const claude = program.command("claude").description("Manage the prim Claude Code integration (capture, gate, ingest, presence)");
|
|
1091
|
+
claude.command("install").description("Register the prim hooks + statusline in Claude Code's settings.json").option(
|
|
1092
|
+
"--scope <scope>",
|
|
1093
|
+
"project (default, the repo's .claude/settings.json) or user (~/.claude/settings.json)"
|
|
1094
|
+
).option("--force", "Replace any drifted prim hook entries").action((opts) => {
|
|
1095
|
+
const scope = resolveScope(opts.scope);
|
|
1096
|
+
const result = performInstall(scope, opts.force ?? false);
|
|
1097
|
+
if (result.changed) {
|
|
1098
|
+
console.error(
|
|
1099
|
+
`[prim] Claude Code integration installed (${scope} scope) at ${result.path}`
|
|
1100
|
+
);
|
|
1101
|
+
} else {
|
|
1102
|
+
console.error(
|
|
1103
|
+
`[prim] Claude Code integration already present at ${result.path} (no changes)`
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
const projectState = scope === "user" ? performStatus().project.statuslineState : void 0;
|
|
1107
|
+
const userState = scope === "project" ? performStatus().user.statuslineState : void 0;
|
|
1108
|
+
if (result.statuslineState === "custom" || projectState === "custom" || userState === "custom") {
|
|
1109
|
+
const owner = result.statuslineState === "custom" ? result.path : projectState === "custom" ? projectScopePath() : USER_SCOPE_PATH;
|
|
1110
|
+
console.error(
|
|
1111
|
+
`[prim] \u26A0 statusline skipped: an existing custom statusLine is preserved at ${owner}; run \`prim daemon status\` for Primitive health`
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
console.log(JSON.stringify(result, null, JSON_INDENT));
|
|
1115
|
+
});
|
|
1116
|
+
claude.command("preauth").description(
|
|
1117
|
+
"Pre-authorize prim's own calls (allow-rule + auto-mode trust, no hooks) so they never prompt"
|
|
1118
|
+
).option(
|
|
1119
|
+
"--scope <scope>",
|
|
1120
|
+
"user (default for preauth \u2014 covers every repo) or project (this repo's .claude/settings.json)"
|
|
1121
|
+
).action((opts) => {
|
|
1122
|
+
const scope = resolveScope(opts.scope ?? "user");
|
|
1123
|
+
const result = performPermissionInstall(scope);
|
|
1124
|
+
console.error(
|
|
1125
|
+
result.changed ? `[prim] prim pre-authorization written (${scope} scope) at ${result.path} (allow-rule + auto-mode trust)` : `[prim] prim pre-authorization already present at ${result.path} (no changes)`
|
|
1126
|
+
);
|
|
1127
|
+
console.log(JSON.stringify(result, null, JSON_INDENT));
|
|
1128
|
+
});
|
|
1129
|
+
claude.command("uninstall").description("Remove all prim hooks + the prim statusline from settings.json").option(
|
|
1130
|
+
"--scope <scope>",
|
|
1131
|
+
"project (default, the repo's .claude/settings.json) or user (~/.claude/settings.json)"
|
|
1132
|
+
).action((opts) => {
|
|
1133
|
+
const scope = resolveScope(opts.scope);
|
|
1134
|
+
const result = performUninstall(scope);
|
|
1135
|
+
if (result.changed) {
|
|
1136
|
+
console.error(`[prim] prim hooks removed from ${result.path}`);
|
|
1137
|
+
} else {
|
|
1138
|
+
console.error(`[prim] no prim hooks to remove at ${result.path} (nothing changed)`);
|
|
1139
|
+
}
|
|
1140
|
+
console.log(JSON.stringify(result, null, JSON_INDENT));
|
|
1141
|
+
});
|
|
1142
|
+
claude.command("status").description(
|
|
1143
|
+
"Report whether each prim surface (gate, capture, feedback, statusline) is installed per scope"
|
|
1144
|
+
).action(() => {
|
|
1145
|
+
const result = performStatus();
|
|
1146
|
+
const mark = (b) => b ? "\u2713" : "\u2717";
|
|
1147
|
+
const line = (label, s) => `[prim] ${label}: gate ${mark(s.gate)} \xB7 capture ${mark(s.capture)} \xB7 feedback ${mark(s.feedback)} \xB7 statusline ${s.statuslineState} (${s.path})`;
|
|
1148
|
+
console.error(`${line("user", result.user)}
|
|
1149
|
+
${line("project", result.project)}`);
|
|
1150
|
+
console.log(JSON.stringify(result, null, JSON_INDENT));
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// src/commands/cursor-install.ts
|
|
1155
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
1156
|
+
import { homedir as homedir2 } from "os";
|
|
1157
|
+
import { isAbsolute as isAbsolute3, join as join4 } from "path";
|
|
1158
|
+
var CAPTURE_BIN2 = "prim-hook";
|
|
1159
|
+
var GATE_BIN2 = "prim-pre-tool-use";
|
|
1160
|
+
var POST_TOOL_BIN = "prim-post-tool-use";
|
|
1161
|
+
var SESSION_START_BIN2 = "prim-session-start";
|
|
1162
|
+
var SESSION_END_BIN2 = "prim-session-end";
|
|
1163
|
+
var STATUSLINE_BIN2 = "prim-statusline";
|
|
1164
|
+
var PRIM_BINS2 = [CAPTURE_BIN2, GATE_BIN2, POST_TOOL_BIN, SESSION_START_BIN2, SESSION_END_BIN2];
|
|
1165
|
+
var CURSOR_EVENTS = [
|
|
1166
|
+
"sessionStart",
|
|
1167
|
+
"beforeSubmitPrompt",
|
|
1168
|
+
"preToolUse",
|
|
1169
|
+
"postToolUse",
|
|
1170
|
+
"postToolUseFailure",
|
|
1171
|
+
"afterAgentResponse",
|
|
1172
|
+
"subagentStop",
|
|
1173
|
+
"stop",
|
|
1174
|
+
"sessionEnd"
|
|
1175
|
+
];
|
|
1176
|
+
var REGISTRATIONS2 = [
|
|
1177
|
+
...CURSOR_EVENTS.map((event) => ({ event, bin: CAPTURE_BIN2 })),
|
|
1178
|
+
{ event: "preToolUse", bin: GATE_BIN2, matcher: "Write|Delete|Shell", timeout: 10 },
|
|
1179
|
+
{ event: "postToolUse", bin: POST_TOOL_BIN },
|
|
1180
|
+
{ event: "postToolUseFailure", bin: POST_TOOL_BIN },
|
|
1181
|
+
{ event: "sessionStart", bin: SESSION_START_BIN2 },
|
|
1182
|
+
{ event: "sessionEnd", bin: SESSION_END_BIN2 }
|
|
1183
|
+
];
|
|
1184
|
+
function cursorConfigDirectory(env = process.env, home = homedir2(), platform = process.platform) {
|
|
1185
|
+
const explicit = env.CURSOR_CONFIG_DIR?.trim();
|
|
1186
|
+
if (explicit && isAbsolute3(explicit)) return explicit;
|
|
1187
|
+
const xdg = env.XDG_CONFIG_HOME?.trim();
|
|
1188
|
+
if (platform !== "darwin" && xdg && isAbsolute3(xdg)) return join4(xdg, "cursor");
|
|
1189
|
+
return join4(home, ".cursor");
|
|
1190
|
+
}
|
|
1191
|
+
function cursorHooksPath(scope, root = projectRoot()) {
|
|
1192
|
+
return scope === "user" ? join4(cursorConfigDirectory(), "hooks.json") : join4(root, ".cursor", "hooks.json");
|
|
1193
|
+
}
|
|
1194
|
+
function cursorCliConfigPath() {
|
|
1195
|
+
return join4(cursorConfigDirectory(), "cli-config.json");
|
|
1196
|
+
}
|
|
1197
|
+
function plainObject(value) {
|
|
1198
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1199
|
+
}
|
|
1200
|
+
function readCursorHooks(path) {
|
|
1201
|
+
if (!existsSync4(path)) return {};
|
|
1202
|
+
let parsed;
|
|
1203
|
+
try {
|
|
1204
|
+
parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
1205
|
+
} catch {
|
|
1206
|
+
throw new Error(`invalid Cursor hooks JSON at ${path}`);
|
|
1207
|
+
}
|
|
1208
|
+
if (!plainObject(parsed)) throw new Error(`Cursor hooks config must be an object: ${path}`);
|
|
1209
|
+
if (parsed.version !== void 0 && parsed.version !== 1) {
|
|
1210
|
+
throw new Error(`unsupported Cursor hooks version at ${path} (expected version 1)`);
|
|
1211
|
+
}
|
|
1212
|
+
if (parsed.hooks !== void 0) {
|
|
1213
|
+
if (!plainObject(parsed.hooks)) throw new Error(`Cursor hooks must be an object: ${path}`);
|
|
1214
|
+
for (const [event, entries] of Object.entries(parsed.hooks)) {
|
|
1215
|
+
if (!Array.isArray(entries) || !entries.every(plainObject)) {
|
|
1216
|
+
throw new Error(`Cursor hook list must contain objects: ${event} in ${path}`);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
return parsed;
|
|
1221
|
+
}
|
|
1222
|
+
function readObject(path) {
|
|
1223
|
+
if (!existsSync4(path)) return {};
|
|
1224
|
+
let parsed;
|
|
1225
|
+
try {
|
|
1226
|
+
parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
1227
|
+
} catch {
|
|
1228
|
+
throw new Error(`invalid Cursor CLI config JSON at ${path}`);
|
|
1229
|
+
}
|
|
1230
|
+
if (!plainObject(parsed)) throw new Error(`Cursor CLI config must be an object: ${path}`);
|
|
1231
|
+
return parsed;
|
|
1232
|
+
}
|
|
1233
|
+
function writeJson(path, value) {
|
|
1234
|
+
atomicWriteFile(path, `${JSON.stringify(value, null, 2)}
|
|
1235
|
+
`, { ensureParent: true });
|
|
1236
|
+
}
|
|
1237
|
+
function commandFor(registration, scope) {
|
|
1238
|
+
return stableHookCommand(
|
|
1239
|
+
registration.bin,
|
|
1240
|
+
`--agent cursor --event ${registration.event} --scope ${scope}`
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
var OWNED_COMMANDS = new Set(
|
|
1244
|
+
["project", "user"].flatMap(
|
|
1245
|
+
(scope) => REGISTRATIONS2.map((registration) => commandFor(registration, scope))
|
|
1246
|
+
)
|
|
1247
|
+
);
|
|
1248
|
+
function jsoncUnsafeCommand(command) {
|
|
1249
|
+
return command.replace('case "$1" in [/]* ', 'case "$1" in /*').replace("*[/][/]*", "*//*").replace("*[/].[/]*", "*/./*").replace("*[/]..[/]*", "*/../*").replace("*[/].", "*/.").replace("*[/]..", "*/..").replace("?*[/]", "?*/");
|
|
1250
|
+
}
|
|
1251
|
+
var JSONC_UNSAFE_OWNED_COMMANDS = new Set([...OWNED_COMMANDS].map(jsoncUnsafeCommand));
|
|
1252
|
+
function isOwnedCursorHookCommand(command) {
|
|
1253
|
+
return typeof command === "string" && (OWNED_COMMANDS.has(command) || JSONC_UNSAFE_OWNED_COMMANDS.has(command));
|
|
1254
|
+
}
|
|
1255
|
+
function hookFor(registration, scope) {
|
|
1256
|
+
return {
|
|
1257
|
+
command: commandFor(registration, scope),
|
|
1258
|
+
...registration.matcher ? { matcher: registration.matcher } : {},
|
|
1259
|
+
...registration.timeout ? { timeout: registration.timeout } : {}
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
function exactHook(entry, registration, scope) {
|
|
1263
|
+
return JSON.stringify(entry) === JSON.stringify(hookFor(registration, scope));
|
|
1264
|
+
}
|
|
1265
|
+
function applyCursorInstall(settings, scope) {
|
|
1266
|
+
const hooks = {};
|
|
1267
|
+
for (const [event, entries] of Object.entries(settings.hooks ?? {})) {
|
|
1268
|
+
hooks[event] = entries.filter((entry) => !isOwnedCursorHookCommand(entry.command));
|
|
1269
|
+
}
|
|
1270
|
+
for (const registration of REGISTRATIONS2) {
|
|
1271
|
+
const entries = hooks[registration.event] ?? [];
|
|
1272
|
+
entries.push(hookFor(registration, scope));
|
|
1273
|
+
hooks[registration.event] = entries;
|
|
1274
|
+
}
|
|
1275
|
+
return { ...settings, version: 1, hooks };
|
|
1276
|
+
}
|
|
1277
|
+
function applyCursorUninstall(settings) {
|
|
1278
|
+
const hooks = {};
|
|
1279
|
+
for (const [event, entries] of Object.entries(settings.hooks ?? {})) {
|
|
1280
|
+
const retained = entries.filter((entry) => !isOwnedCursorHookCommand(entry.command));
|
|
1281
|
+
if (retained.length > 0) hooks[event] = retained;
|
|
1282
|
+
}
|
|
1283
|
+
return { ...settings, hooks };
|
|
1284
|
+
}
|
|
1285
|
+
function ownedEntries(settings) {
|
|
1286
|
+
return Object.entries(settings.hooks ?? {}).flatMap(
|
|
1287
|
+
([event, entries]) => entries.filter((entry) => isOwnedCursorHookCommand(entry.command)).map((entry) => ({ event, entry }))
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
function hasExactCursorHandler(settings, event, bin, scope) {
|
|
1291
|
+
const registration = REGISTRATIONS2.find(
|
|
1292
|
+
(candidate) => candidate.event === event && candidate.bin === bin
|
|
1293
|
+
);
|
|
1294
|
+
return registration ? (settings.hooks?.[event] ?? []).some((entry) => exactHook(entry, registration, scope)) : false;
|
|
1295
|
+
}
|
|
1296
|
+
function complete(settings, scope) {
|
|
1297
|
+
const owned = ownedEntries(settings);
|
|
1298
|
+
return owned.length === REGISTRATIONS2.length && REGISTRATIONS2.every(
|
|
1299
|
+
(registration) => owned.filter(
|
|
1300
|
+
({ event, entry }) => event === registration.event && exactHook(entry, registration, scope)
|
|
1301
|
+
).length === 1
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
function captureInstalled2(settings, scope) {
|
|
1305
|
+
return CURSOR_EVENTS.every((event) => hasExactCursorHandler(settings, event, CAPTURE_BIN2, scope));
|
|
1306
|
+
}
|
|
1307
|
+
function gateInstalled(settings, scope) {
|
|
1308
|
+
return hasExactCursorHandler(settings, "preToolUse", GATE_BIN2, scope);
|
|
1309
|
+
}
|
|
1310
|
+
var FOOTER = {
|
|
1311
|
+
type: "command",
|
|
1312
|
+
command: stableHookCommand(STATUSLINE_BIN2),
|
|
1313
|
+
updateIntervalMs: 1e3,
|
|
1314
|
+
timeoutMs: 2e3
|
|
1315
|
+
};
|
|
1316
|
+
var JSONC_UNSAFE_FOOTER = {
|
|
1317
|
+
...FOOTER,
|
|
1318
|
+
command: jsoncUnsafeCommand(FOOTER.command)
|
|
1319
|
+
};
|
|
1320
|
+
function exactFooter(value) {
|
|
1321
|
+
return plainObject(value) && JSON.stringify(value) === JSON.stringify(FOOTER);
|
|
1322
|
+
}
|
|
1323
|
+
function jsoncUnsafeFooter(value) {
|
|
1324
|
+
return plainObject(value) && JSON.stringify(value) === JSON.stringify(JSONC_UNSAFE_FOOTER);
|
|
1325
|
+
}
|
|
1326
|
+
function applyCursorFooterInstall(config) {
|
|
1327
|
+
if (config.statusLine === void 0)
|
|
1328
|
+
return { config: { ...config, statusLine: FOOTER }, installed: true, preservedCustom: false };
|
|
1329
|
+
if (exactFooter(config.statusLine)) return { config, installed: true, preservedCustom: false };
|
|
1330
|
+
if (jsoncUnsafeFooter(config.statusLine)) {
|
|
1331
|
+
return { config: { ...config, statusLine: FOOTER }, installed: true, preservedCustom: false };
|
|
1332
|
+
}
|
|
1333
|
+
return { config, installed: false, preservedCustom: true };
|
|
1334
|
+
}
|
|
1335
|
+
function applyCursorFooterUninstall(config) {
|
|
1336
|
+
if (!exactFooter(config.statusLine) && !jsoncUnsafeFooter(config.statusLine)) {
|
|
1337
|
+
return { config, removed: false };
|
|
1338
|
+
}
|
|
1339
|
+
return {
|
|
1340
|
+
config: Object.fromEntries(Object.entries(config).filter(([key]) => key !== "statusLine")),
|
|
1341
|
+
removed: true
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
function statusFor(scope, path = cursorHooksPath(scope)) {
|
|
1345
|
+
const settings = readCursorHooks(path);
|
|
1346
|
+
const base = {
|
|
1347
|
+
path,
|
|
1348
|
+
present: ownedEntries(settings).length > 0,
|
|
1349
|
+
gate: gateInstalled(settings, scope),
|
|
1350
|
+
capture: captureInstalled2(settings, scope),
|
|
1351
|
+
complete: complete(settings, scope)
|
|
1352
|
+
};
|
|
1353
|
+
if (scope === "user") {
|
|
1354
|
+
const cliConfig = readObject(cursorCliConfigPath());
|
|
1355
|
+
base.footer = exactFooter(cliConfig.statusLine);
|
|
1356
|
+
base.footerPreservedCustom = cliConfig.statusLine !== void 0 && !base.footer;
|
|
1357
|
+
}
|
|
1358
|
+
return base;
|
|
1359
|
+
}
|
|
1360
|
+
function performInstall2(scope, _force = false) {
|
|
1361
|
+
stageHookRuntime();
|
|
1362
|
+
const path = cursorHooksPath(scope);
|
|
1363
|
+
const before = readCursorHooks(path);
|
|
1364
|
+
const after = applyCursorInstall(before, scope);
|
|
1365
|
+
let changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
1366
|
+
if (changed) writeJson(path, after);
|
|
1367
|
+
let restartRequired = false;
|
|
1368
|
+
if (scope === "user") {
|
|
1369
|
+
const cliPath = cursorCliConfigPath();
|
|
1370
|
+
const cliBefore = readObject(cliPath);
|
|
1371
|
+
const footer = applyCursorFooterInstall(cliBefore);
|
|
1372
|
+
if (JSON.stringify(cliBefore) !== JSON.stringify(footer.config)) {
|
|
1373
|
+
writeJson(cliPath, footer.config);
|
|
1374
|
+
changed = true;
|
|
1375
|
+
restartRequired = true;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
return { scope, ...statusFor(scope, path), changed, restartRequired };
|
|
1379
|
+
}
|
|
1380
|
+
function performUninstall2(scope) {
|
|
1381
|
+
const path = cursorHooksPath(scope);
|
|
1382
|
+
const before = readCursorHooks(path);
|
|
1383
|
+
const after = applyCursorUninstall(before);
|
|
1384
|
+
let changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
1385
|
+
if (changed) writeJson(path, after);
|
|
1386
|
+
let restartRequired = false;
|
|
1387
|
+
if (scope === "user") {
|
|
1388
|
+
const cliPath = cursorCliConfigPath();
|
|
1389
|
+
const cliBefore = readObject(cliPath);
|
|
1390
|
+
const footer = applyCursorFooterUninstall(cliBefore);
|
|
1391
|
+
if (footer.removed) {
|
|
1392
|
+
writeJson(cliPath, footer.config);
|
|
1393
|
+
changed = true;
|
|
1394
|
+
restartRequired = true;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
return { scope, ...statusFor(scope, path), changed, restartRequired };
|
|
1398
|
+
}
|
|
1399
|
+
function performStatus2() {
|
|
1400
|
+
return { user: statusFor("user"), project: statusFor("project") };
|
|
1401
|
+
}
|
|
1402
|
+
function inspectHookRuntimeResolutions2() {
|
|
1403
|
+
const commands = ["user", "project"].flatMap(
|
|
1404
|
+
(scope) => ownedEntries(readCursorHooks(cursorHooksPath(scope))).map(({ entry }) => entry.command)
|
|
1405
|
+
);
|
|
1406
|
+
return hookCommandResolutions(commands, PRIM_BINS2);
|
|
1407
|
+
}
|
|
1408
|
+
function resolveScope2(input) {
|
|
1409
|
+
if (input === void 0 || input === "project") return "project";
|
|
1410
|
+
if (input === "user") return "user";
|
|
1411
|
+
throw new Error(`unknown --scope "${input}" (expected: user or project)`);
|
|
1412
|
+
}
|
|
1413
|
+
function registerCursorCommands(program) {
|
|
1414
|
+
const cursor = program.command("cursor").description("Manage the native Cursor integration (capture, gate, ingest, presence)");
|
|
1415
|
+
cursor.command("install").description("Register Primitive in Cursor's native hooks.json").option("--scope <scope>", "project (default) or user").option("--force", "Repair drifted Primitive entries; custom Cursor fields remain untouched").action((opts) => {
|
|
1416
|
+
const result = performInstall2(resolveScope2(opts.scope), opts.force);
|
|
1417
|
+
console.error(
|
|
1418
|
+
result.changed ? `[prim] Cursor integration installed (${result.scope} scope) at ${result.path}` : `[prim] Cursor integration already present at ${result.path}`
|
|
1419
|
+
);
|
|
1420
|
+
if (result.restartRequired) console.error("[prim] restart Cursor CLI to load the footer");
|
|
1421
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1422
|
+
});
|
|
1423
|
+
cursor.command("uninstall").description("Remove only Primitive-owned Cursor hooks and footer").option("--scope <scope>", "project (default) or user").action((opts) => {
|
|
1424
|
+
const result = performUninstall2(resolveScope2(opts.scope));
|
|
1425
|
+
console.error(
|
|
1426
|
+
result.changed ? `[prim] Cursor integration removed (${result.scope} scope)` : `[prim] no Primitive-owned Cursor integration found (${result.scope} scope)`
|
|
1427
|
+
);
|
|
1428
|
+
if (result.restartRequired) console.error("[prim] restart Cursor CLI to remove the footer");
|
|
1429
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1430
|
+
});
|
|
1431
|
+
cursor.command("status").description("Report native Cursor hook and footer status").action(() => {
|
|
1432
|
+
const result = performStatus2();
|
|
1433
|
+
const mark = (value) => value ? "\u2713" : "\u2717";
|
|
1434
|
+
console.error(
|
|
1435
|
+
`[prim] user: gate ${mark(result.user.gate)} \xB7 capture ${mark(result.user.capture)} \xB7 footer ${mark(result.user.footer)} (${result.user.path})
|
|
1436
|
+
[prim] project: gate ${mark(result.project.gate)} \xB7 capture ${mark(result.project.capture)} (${result.project.path})`
|
|
1437
|
+
);
|
|
1438
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
export {
|
|
1443
|
+
compareSemver,
|
|
1444
|
+
binFile,
|
|
1445
|
+
packageVersion,
|
|
1446
|
+
pinnedNpxArgs,
|
|
1447
|
+
pinnedNpxCommand,
|
|
1448
|
+
pinnedHookCommand,
|
|
1449
|
+
stableHookCommand,
|
|
1450
|
+
commandMatchesBin,
|
|
1451
|
+
hookCommandResolutions,
|
|
1452
|
+
hookRuntimePaths,
|
|
1453
|
+
inspectHookRuntime,
|
|
1454
|
+
stageHookRuntime,
|
|
1455
|
+
assertOwnedHookRuntime,
|
|
1456
|
+
removeHookRuntime,
|
|
1457
|
+
projectRoot,
|
|
1458
|
+
makeRegistration,
|
|
1459
|
+
readSettings,
|
|
1460
|
+
entryHasCommand,
|
|
1461
|
+
stripCommand,
|
|
1462
|
+
ensureRegistration,
|
|
1463
|
+
inspectHookRuntimeResolutions,
|
|
1464
|
+
atomicWrite,
|
|
1465
|
+
performStatus,
|
|
1466
|
+
registerClaudeCommands,
|
|
1467
|
+
cursorConfigDirectory,
|
|
1468
|
+
cursorHooksPath,
|
|
1469
|
+
readCursorHooks,
|
|
1470
|
+
hasExactCursorHandler,
|
|
1471
|
+
performStatus2,
|
|
1472
|
+
inspectHookRuntimeResolutions2,
|
|
1473
|
+
registerCursorCommands
|
|
1474
|
+
};
|