@primitive.ai/prim 0.1.0-alpha.8 → 0.1.0-alpha.81
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 +295 -37
- package/SKILL.md +269 -0
- package/dist/chunk-3EEFL6NJ.js +529 -0
- package/dist/chunk-4CZTNXIS.js +505 -0
- package/dist/chunk-4GLVJLEQ.js +55 -0
- package/dist/chunk-BQUJ4AFJ.js +771 -0
- package/dist/chunk-CUE2LKLR.js +155 -0
- package/dist/chunk-EZXEWFDG.js +210 -0
- package/dist/chunk-FI3LIRCB.js +262 -0
- package/dist/chunk-HWSS4BV5.js +1400 -0
- package/dist/chunk-LEMX3IOG.js +79 -0
- package/dist/chunk-LMRYZL3D.js +209 -0
- package/dist/chunk-OUXH3UEP.js +247 -0
- package/dist/chunk-PMRDE2DA.js +310 -0
- package/dist/chunk-RO3F6JYV.js +126 -0
- package/dist/chunk-TJTVHDJ5.js +321 -0
- package/dist/chunk-WB7AUZ4C.js +52 -0
- package/dist/chunk-XGADQPCI.js +26 -0
- package/dist/chunk-YWO7O6QG.js +327 -0
- package/dist/chunk-Z7YPFWAZ.js +917 -0
- package/dist/chunk-ZOSNEO5N.js +77 -0
- package/dist/daemon/server.js +15329 -0
- package/dist/hook-runtime/daemon/server.js +15329 -0
- package/dist/hook-runtime/hooks/post-commit.js +1128 -0
- package/dist/hook-runtime/hooks/post-rewrite.js +1148 -0
- package/dist/hook-runtime/hooks/post-tool-use.js +7586 -0
- package/dist/hook-runtime/hooks/pre-commit.js +1314 -0
- package/dist/hook-runtime/hooks/pre-tool-use.js +7099 -0
- package/dist/hook-runtime/hooks/prim-hook.js +7691 -0
- package/dist/hook-runtime/hooks/session-end.js +495 -0
- package/dist/hook-runtime/hooks/session-start.js +11313 -0
- package/dist/hook-runtime/index.js +44696 -0
- package/dist/hook-runtime/statusline-main.js +765 -0
- package/dist/hooks/post-commit.js +179 -0
- package/dist/hooks/post-rewrite.js +197 -0
- package/dist/hooks/post-tool-use.js +250 -0
- package/dist/hooks/pre-commit.js +21 -74
- package/dist/hooks/pre-tool-use.js +348 -0
- package/dist/hooks/prim-hook.js +195 -0
- package/dist/hooks/session-end.js +70 -0
- package/dist/hooks/session-start.js +299 -0
- package/dist/index.js +20981 -519
- package/dist/statusline-main.js +765 -0
- package/package.json +35 -13
- package/dist/chunk-3APLWTLB.js +0 -165
|
@@ -0,0 +1,1400 @@
|
|
|
1
|
+
import {
|
|
2
|
+
binFile,
|
|
3
|
+
compareSemver,
|
|
4
|
+
pinnedNpxCommand
|
|
5
|
+
} from "./chunk-PMRDE2DA.js";
|
|
6
|
+
import {
|
|
7
|
+
gitToplevel,
|
|
8
|
+
githubRepositoryFullName,
|
|
9
|
+
isValidRepoSyncId,
|
|
10
|
+
setRepoSyncId,
|
|
11
|
+
setRepositoryBindingState
|
|
12
|
+
} from "./chunk-TJTVHDJ5.js";
|
|
13
|
+
import {
|
|
14
|
+
HttpError,
|
|
15
|
+
atomicWriteFile,
|
|
16
|
+
getClient,
|
|
17
|
+
withFileLock
|
|
18
|
+
} from "./chunk-Z7YPFWAZ.js";
|
|
19
|
+
|
|
20
|
+
// src/lib/bin-cache.ts
|
|
21
|
+
import { chmodSync, mkdirSync } from "fs";
|
|
22
|
+
import { homedir } from "os";
|
|
23
|
+
import { join } from "path";
|
|
24
|
+
var GIT_HOOK_CACHE_SHELL_DIR = "${XDG_CACHE_HOME:-$HOME/.cache}/prim/bin";
|
|
25
|
+
var GIT_HOOK_CACHE_TTL_MINUTES = 1440;
|
|
26
|
+
var GIT_HOOK_CACHED_BINS = ["prim-post-commit", "prim-post-rewrite"];
|
|
27
|
+
function binCacheDir() {
|
|
28
|
+
const base = process.env.XDG_CACHE_HOME || join(homedir(), ".cache");
|
|
29
|
+
return join(base, "prim", "bin");
|
|
30
|
+
}
|
|
31
|
+
function writeAtomic(path, content) {
|
|
32
|
+
atomicWriteFile(path, content, { mode: 384 });
|
|
33
|
+
}
|
|
34
|
+
function warmBinCache() {
|
|
35
|
+
try {
|
|
36
|
+
if (process.env.PRIM_BIN_CACHE === "0") {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const dir = binCacheDir();
|
|
40
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
41
|
+
chmodSync(dir, 448);
|
|
42
|
+
writeAtomic(join(dir, "node"), process.execPath);
|
|
43
|
+
for (const bin of GIT_HOOK_CACHED_BINS) {
|
|
44
|
+
const file = binFile(bin);
|
|
45
|
+
if (file) {
|
|
46
|
+
writeAtomic(join(dir, bin), file);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/commands/skill.ts
|
|
54
|
+
import {
|
|
55
|
+
constants as constants2,
|
|
56
|
+
closeSync as closeSync2,
|
|
57
|
+
existsSync as existsSync2,
|
|
58
|
+
fstatSync as fstatSync2,
|
|
59
|
+
lstatSync as lstatSync2,
|
|
60
|
+
openSync as openSync2,
|
|
61
|
+
readFileSync as readFileSync2,
|
|
62
|
+
readSync as readSync2
|
|
63
|
+
} from "fs";
|
|
64
|
+
import { homedir as homedir3 } from "os";
|
|
65
|
+
import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
|
|
66
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
67
|
+
import { createPatch } from "diff";
|
|
68
|
+
import { parse as parseYaml2 } from "yaml";
|
|
69
|
+
|
|
70
|
+
// src/output.ts
|
|
71
|
+
function printJson(data) {
|
|
72
|
+
console.log(JSON.stringify(data, null, 2));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/commands/claude-plugin.ts
|
|
76
|
+
import { createHash } from "crypto";
|
|
77
|
+
import {
|
|
78
|
+
constants,
|
|
79
|
+
closeSync,
|
|
80
|
+
existsSync,
|
|
81
|
+
fstatSync,
|
|
82
|
+
lstatSync,
|
|
83
|
+
mkdirSync as mkdirSync2,
|
|
84
|
+
openSync,
|
|
85
|
+
readFileSync,
|
|
86
|
+
readSync,
|
|
87
|
+
readdirSync,
|
|
88
|
+
realpathSync,
|
|
89
|
+
rmSync,
|
|
90
|
+
rmdirSync
|
|
91
|
+
} from "fs";
|
|
92
|
+
import { homedir as homedir2 } from "os";
|
|
93
|
+
import { dirname, isAbsolute, join as join2, relative, resolve, sep } from "path";
|
|
94
|
+
import { fileURLToPath } from "url";
|
|
95
|
+
import { parse as parseYaml } from "yaml";
|
|
96
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
97
|
+
var PLUGIN_DESCRIPTION = "Primitive decision-graph guidance for the prim CLI \u2014 a model-invoked skill installed by prim skill install.";
|
|
98
|
+
var MAX_MANIFEST_BYTES = 64 * 1024;
|
|
99
|
+
var MAX_SKILL_BYTES = 1024 * 1024;
|
|
100
|
+
var REFRESH_LOCK_TIMEOUT_MS = 150;
|
|
101
|
+
var REFRESH_LOCK_POLL_MS = 10;
|
|
102
|
+
function pluginDirFor(cwd, scope) {
|
|
103
|
+
const base = scope === "user" ? join2(homedir2(), ".claude") : join2(gitToplevel(cwd) ?? cwd, ".claude");
|
|
104
|
+
return join2(base, "skills", "prim");
|
|
105
|
+
}
|
|
106
|
+
function resolvePluginDir(cwd, scope) {
|
|
107
|
+
if (scope && scope !== "user" && scope !== "project") {
|
|
108
|
+
console.error(`Unknown --scope "${scope}" (expected user or project)`);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
return pluginDirFor(cwd, scope === "user" ? "user" : "project");
|
|
112
|
+
}
|
|
113
|
+
function packageVersion() {
|
|
114
|
+
let dir = __dirname;
|
|
115
|
+
while (dir !== dirname(dir)) {
|
|
116
|
+
const p = resolve(dir, "package.json");
|
|
117
|
+
if (existsSync(p)) return JSON.parse(readFileSync(p, "utf-8")).version;
|
|
118
|
+
dir = dirname(dir);
|
|
119
|
+
}
|
|
120
|
+
return "0.0.0";
|
|
121
|
+
}
|
|
122
|
+
function pluginManifest(version) {
|
|
123
|
+
const manifest = { name: "prim", description: PLUGIN_DESCRIPTION, version };
|
|
124
|
+
return `${JSON.stringify(manifest, null, 2)}
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
127
|
+
function renderClaudePlugin() {
|
|
128
|
+
const version = packageVersion();
|
|
129
|
+
return { manifest: pluginManifest(version), skill: loadSkill(), version };
|
|
130
|
+
}
|
|
131
|
+
function removeDirIfEmpty(dir) {
|
|
132
|
+
if (existsSync(dir) && readdirSync(dir).length === 0) rmdirSync(dir);
|
|
133
|
+
}
|
|
134
|
+
function pluginPaths(dir) {
|
|
135
|
+
return {
|
|
136
|
+
manifestPath: join2(dir, ".claude-plugin", "plugin.json"),
|
|
137
|
+
skillPath: join2(dir, "SKILL.md")
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function pathIsInside(root, path) {
|
|
141
|
+
const rel = relative(root, path);
|
|
142
|
+
return rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
143
|
+
}
|
|
144
|
+
function managedPathsAreSafe(paths, projectRoot) {
|
|
145
|
+
try {
|
|
146
|
+
const managed = [paths.manifestPath, paths.skillPath];
|
|
147
|
+
if (managed.some((path) => !lstatSync(path).isFile())) return false;
|
|
148
|
+
if (projectRoot === void 0) return true;
|
|
149
|
+
const realRoot = realpathSync(projectRoot);
|
|
150
|
+
return managed.every((path) => pathIsInside(realRoot, realpathSync(path)));
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function readBoundedRegularFile(path, maxBytes) {
|
|
156
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
157
|
+
try {
|
|
158
|
+
const stat = fstatSync(fd);
|
|
159
|
+
if (!stat.isFile() || stat.size > maxBytes) throw new Error("unsafe managed file");
|
|
160
|
+
const chunks = [];
|
|
161
|
+
let total = 0;
|
|
162
|
+
while (total <= maxBytes) {
|
|
163
|
+
const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, maxBytes + 1 - total));
|
|
164
|
+
const bytesRead = readSync(fd, chunk, 0, chunk.length, null);
|
|
165
|
+
if (bytesRead === 0) break;
|
|
166
|
+
chunks.push(chunk.subarray(0, bytesRead));
|
|
167
|
+
total += bytesRead;
|
|
168
|
+
}
|
|
169
|
+
if (total > maxBytes) throw new Error("unsafe managed file");
|
|
170
|
+
return Buffer.concat(chunks, total).toString("utf8");
|
|
171
|
+
} finally {
|
|
172
|
+
closeSync(fd);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function hasUsablePrimFrontmatter(skill) {
|
|
176
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)([\s\S]*)$/u.exec(skill);
|
|
177
|
+
if (!match) return false;
|
|
178
|
+
try {
|
|
179
|
+
const parsed = parseYaml(match[1]);
|
|
180
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return false;
|
|
181
|
+
const frontmatter = parsed;
|
|
182
|
+
return frontmatter.name === "prim" && typeof frontmatter.description === "string" && frontmatter.description.trim().length > 0 && match[2].trim().length > 0;
|
|
183
|
+
} catch {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function inspectRecognizedInstallation(paths, projectRoot) {
|
|
188
|
+
if (!existsSync(paths.manifestPath) || !existsSync(paths.skillPath)) return;
|
|
189
|
+
if (!managedPathsAreSafe(paths, projectRoot)) return;
|
|
190
|
+
const manifest = readBoundedRegularFile(paths.manifestPath, MAX_MANIFEST_BYTES);
|
|
191
|
+
const parsed = JSON.parse(manifest);
|
|
192
|
+
if (parsed.name !== "prim") return;
|
|
193
|
+
const skill = readBoundedRegularFile(paths.skillPath, MAX_SKILL_BYTES);
|
|
194
|
+
return {
|
|
195
|
+
manifest,
|
|
196
|
+
manifestVersion: parsed.version,
|
|
197
|
+
skill,
|
|
198
|
+
usable: hasUsablePrimFrontmatter(skill)
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function refreshLockDir(physicalPluginDir) {
|
|
202
|
+
const cacheRoot = process.env.XDG_CACHE_HOME || join2(homedir2(), ".cache");
|
|
203
|
+
const identity = createHash("sha256").update(physicalPluginDir).digest("hex");
|
|
204
|
+
return join2(cacheRoot, "prim", "skill-refresh-locks", `${identity}.lock`);
|
|
205
|
+
}
|
|
206
|
+
async function refreshClaudePlugins(cwd, options = {}) {
|
|
207
|
+
const result = { installed: 0, refreshed: 0 };
|
|
208
|
+
const candidates = [];
|
|
209
|
+
if (options.includeProject !== false) {
|
|
210
|
+
const projectRoot = options.projectRoot ?? gitToplevel(cwd);
|
|
211
|
+
if (projectRoot !== null && isAbsolute(projectRoot)) {
|
|
212
|
+
candidates.push({ dir: join2(projectRoot, ".claude", "skills", "prim"), projectRoot });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
candidates.push({ dir: pluginDirFor(cwd, "user") });
|
|
216
|
+
const seen = /* @__PURE__ */ new Set();
|
|
217
|
+
let desired;
|
|
218
|
+
for (const { dir, projectRoot } of candidates) {
|
|
219
|
+
let counted = false;
|
|
220
|
+
const markInstalled = () => {
|
|
221
|
+
if (!counted) {
|
|
222
|
+
result.installed += 1;
|
|
223
|
+
counted = true;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
const refreshCandidate = () => {
|
|
227
|
+
const { manifestPath, skillPath } = pluginPaths(dir);
|
|
228
|
+
const paths = { manifestPath, skillPath };
|
|
229
|
+
try {
|
|
230
|
+
let changed = false;
|
|
231
|
+
const current = inspectRecognizedInstallation(paths, projectRoot);
|
|
232
|
+
if (!current) return;
|
|
233
|
+
desired ??= renderClaudePlugin();
|
|
234
|
+
if (projectRoot !== void 0 && current.usable && compareSemver(current.manifestVersion, desired.version) === 1) {
|
|
235
|
+
markInstalled();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (current.manifest !== desired.manifest) {
|
|
239
|
+
if (!managedPathsAreSafe(paths, projectRoot)) throw new Error("unsafe managed path");
|
|
240
|
+
atomicWrite(manifestPath, desired.manifest);
|
|
241
|
+
if (!changed) result.refreshed += 1;
|
|
242
|
+
changed = true;
|
|
243
|
+
}
|
|
244
|
+
if (current.skill !== desired.skill) {
|
|
245
|
+
if (!managedPathsAreSafe(paths, projectRoot)) throw new Error("unsafe managed path");
|
|
246
|
+
atomicWrite(skillPath, desired.skill);
|
|
247
|
+
if (!changed) result.refreshed += 1;
|
|
248
|
+
changed = true;
|
|
249
|
+
}
|
|
250
|
+
markInstalled();
|
|
251
|
+
} catch {
|
|
252
|
+
try {
|
|
253
|
+
if (inspectRecognizedInstallation(paths, projectRoot)?.usable) markInstalled();
|
|
254
|
+
} catch {
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
try {
|
|
259
|
+
const paths = pluginPaths(dir);
|
|
260
|
+
if (!existsSync(paths.manifestPath) || !existsSync(paths.skillPath)) continue;
|
|
261
|
+
const physicalPluginDir = realpathSync(dir);
|
|
262
|
+
if (seen.has(physicalPluginDir)) continue;
|
|
263
|
+
seen.add(physicalPluginDir);
|
|
264
|
+
await withFileLock(refreshLockDir(physicalPluginDir), refreshCandidate, {
|
|
265
|
+
timeoutMs: REFRESH_LOCK_TIMEOUT_MS,
|
|
266
|
+
pollMs: REFRESH_LOCK_POLL_MS
|
|
267
|
+
});
|
|
268
|
+
} catch {
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return result;
|
|
272
|
+
}
|
|
273
|
+
function installClaudePlugin(cwd, opts) {
|
|
274
|
+
const dir = resolvePluginDir(cwd, opts.scope);
|
|
275
|
+
if (dir === null) return 1;
|
|
276
|
+
const { manifestPath, skillPath } = pluginPaths(dir);
|
|
277
|
+
const { manifest, skill } = renderClaudePlugin();
|
|
278
|
+
const manifestCurrent = existsSync(manifestPath) ? readFileSync(manifestPath, "utf-8") : null;
|
|
279
|
+
const skillCurrent = existsSync(skillPath) ? readFileSync(skillPath, "utf-8") : null;
|
|
280
|
+
if (manifestCurrent === manifest && skillCurrent === skill) {
|
|
281
|
+
console.log("No changes \u2014 prim skill plugin already up to date.");
|
|
282
|
+
return 0;
|
|
283
|
+
}
|
|
284
|
+
if (opts.dryRun) {
|
|
285
|
+
console.log(`Would write plugin to ${dir} (.claude-plugin/plugin.json + SKILL.md)`);
|
|
286
|
+
return 0;
|
|
287
|
+
}
|
|
288
|
+
mkdirSync2(join2(dir, ".claude-plugin"), { recursive: true });
|
|
289
|
+
atomicWrite(manifestPath, manifest);
|
|
290
|
+
atomicWrite(skillPath, skill);
|
|
291
|
+
console.log(`Installed prim skill plugin at ${dir}`);
|
|
292
|
+
console.log("Restart Claude Code or run /reload-plugins to load it.");
|
|
293
|
+
if (opts.scope !== "user") {
|
|
294
|
+
console.log("Project-scope plugins load only when Claude Code launches from this directory.");
|
|
295
|
+
}
|
|
296
|
+
return 0;
|
|
297
|
+
}
|
|
298
|
+
function uninstallClaudePlugin(cwd, opts) {
|
|
299
|
+
const dir = resolvePluginDir(cwd, opts.scope);
|
|
300
|
+
if (dir === null) return 1;
|
|
301
|
+
const { manifestPath, skillPath } = pluginPaths(dir);
|
|
302
|
+
if (!existsSync(manifestPath) && !existsSync(skillPath)) {
|
|
303
|
+
console.log(`prim skill plugin not present at ${dir}`);
|
|
304
|
+
return 0;
|
|
305
|
+
}
|
|
306
|
+
rmSync(manifestPath, { force: true });
|
|
307
|
+
rmSync(skillPath, { force: true });
|
|
308
|
+
removeDirIfEmpty(join2(dir, ".claude-plugin"));
|
|
309
|
+
removeDirIfEmpty(dir);
|
|
310
|
+
console.log(`Removed prim skill plugin from ${dir}`);
|
|
311
|
+
return 0;
|
|
312
|
+
}
|
|
313
|
+
function statusClaudePlugin(cwd, opts) {
|
|
314
|
+
const dir = resolvePluginDir(cwd, opts.scope);
|
|
315
|
+
if (dir === null) return 1;
|
|
316
|
+
const { skillPath } = pluginPaths(dir);
|
|
317
|
+
const installed = existsSync(skillPath);
|
|
318
|
+
if (opts.json) {
|
|
319
|
+
printJson({ installed, target: dir });
|
|
320
|
+
return installed ? 0 : 1;
|
|
321
|
+
}
|
|
322
|
+
if (installed) {
|
|
323
|
+
console.log(`prim skill plugin installed at ${dir}`);
|
|
324
|
+
return 0;
|
|
325
|
+
}
|
|
326
|
+
console.log(`No prim skill plugin at ${dir}`);
|
|
327
|
+
return 1;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// src/commands/skill.ts
|
|
331
|
+
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
332
|
+
var SKILL_BEGIN = "<!-- BEGIN PRIM SKILL v1 -->";
|
|
333
|
+
var SKILL_END = "<!-- END PRIM SKILL v1 -->";
|
|
334
|
+
var CODEX_GUIDANCE_LIMIT_BYTES = 1024 * 1024;
|
|
335
|
+
var CODEX_DEFAULT_PROJECT_GUIDANCE_BYTES = 32 * 1024;
|
|
336
|
+
var SKILL_BEGIN_BUFFER = Buffer.from(SKILL_BEGIN);
|
|
337
|
+
var SKILL_END_BUFFER = Buffer.from(SKILL_END);
|
|
338
|
+
function readCodexGuidance(path) {
|
|
339
|
+
try {
|
|
340
|
+
const entry = lstatSync2(path);
|
|
341
|
+
if (entry.isSymbolicLink() || !entry.isFile()) return null;
|
|
342
|
+
const fd = openSync2(path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
|
|
343
|
+
try {
|
|
344
|
+
const opened = fstatSync2(fd);
|
|
345
|
+
if (!opened.isFile() || opened.size > CODEX_GUIDANCE_LIMIT_BYTES) return null;
|
|
346
|
+
const bytes = Buffer.allocUnsafe(CODEX_GUIDANCE_LIMIT_BYTES + 1);
|
|
347
|
+
let length = 0;
|
|
348
|
+
while (length < bytes.length) {
|
|
349
|
+
const read = readSync2(fd, bytes, length, bytes.length - length, length);
|
|
350
|
+
if (read === 0) break;
|
|
351
|
+
length += read;
|
|
352
|
+
}
|
|
353
|
+
return length > CODEX_GUIDANCE_LIMIT_BYTES ? null : bytes.subarray(0, length);
|
|
354
|
+
} finally {
|
|
355
|
+
closeSync2(fd);
|
|
356
|
+
}
|
|
357
|
+
} catch (error) {
|
|
358
|
+
const code = error.code;
|
|
359
|
+
return code === "ENOENT" || code === "ENOTDIR" ? void 0 : null;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function effectiveCodexGuidance(scope) {
|
|
363
|
+
const override = readCodexGuidance(join3(scope, "AGENTS.override.md"));
|
|
364
|
+
if (override === null) return null;
|
|
365
|
+
if (override !== void 0 && override.toString("utf8").trim().length > 0) return override;
|
|
366
|
+
const base = readCodexGuidance(join3(scope, "AGENTS.md"));
|
|
367
|
+
return base ?? null;
|
|
368
|
+
}
|
|
369
|
+
function hasUsablePrimBlock(content, visibleBytes) {
|
|
370
|
+
const begin = content.indexOf(SKILL_BEGIN_BUFFER);
|
|
371
|
+
const end = content.indexOf(SKILL_END_BUFFER);
|
|
372
|
+
if (begin === -1 || end <= begin + SKILL_BEGIN_BUFFER.length || end + SKILL_END_BUFFER.length > visibleBytes || content.indexOf(SKILL_BEGIN_BUFFER, begin + SKILL_BEGIN_BUFFER.length) !== -1 || content.indexOf(SKILL_END_BUFFER, end + SKILL_END_BUFFER.length) !== -1) {
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
const block = content.subarray(begin + SKILL_BEGIN_BUFFER.length, end).toString("utf8").trim();
|
|
376
|
+
const match = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)([\s\S]*)$/u.exec(block);
|
|
377
|
+
if (!match || match[2].trim().length === 0) return false;
|
|
378
|
+
try {
|
|
379
|
+
const metadata = parseYaml2(match[1]);
|
|
380
|
+
if (typeof metadata !== "object" || metadata === null || Array.isArray(metadata)) return false;
|
|
381
|
+
const fields = metadata;
|
|
382
|
+
return fields.name === "prim" && typeof fields.description === "string" && fields.description.trim().length > 0;
|
|
383
|
+
} catch {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
function hasUsableCodexGuidance(projectRoot) {
|
|
388
|
+
try {
|
|
389
|
+
const codexHome = process.env.CODEX_HOME?.trim() ? resolve2(process.env.CODEX_HOME) : join3(homedir3(), ".codex");
|
|
390
|
+
const scopes = [
|
|
391
|
+
{ path: codexHome, visibleBytes: CODEX_GUIDANCE_LIMIT_BYTES },
|
|
392
|
+
{ path: projectRoot, visibleBytes: CODEX_DEFAULT_PROJECT_GUIDANCE_BYTES }
|
|
393
|
+
];
|
|
394
|
+
return scopes.some(({ path, visibleBytes }) => {
|
|
395
|
+
const guidance = effectiveCodexGuidance(path);
|
|
396
|
+
return guidance !== null && hasUsablePrimBlock(guidance, visibleBytes);
|
|
397
|
+
});
|
|
398
|
+
} catch {
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
var TARGET_CANDIDATES = [
|
|
403
|
+
"CLAUDE.md",
|
|
404
|
+
"AGENTS.md",
|
|
405
|
+
".hermes.md",
|
|
406
|
+
".cursor/rules",
|
|
407
|
+
".windsurfrules",
|
|
408
|
+
".github/instructions/primitive.md"
|
|
409
|
+
];
|
|
410
|
+
var DEFAULT_TARGET = "CLAUDE.md";
|
|
411
|
+
var AGENT_TARGET = {
|
|
412
|
+
claude: "CLAUDE.md",
|
|
413
|
+
codex: "AGENTS.md",
|
|
414
|
+
hermes: ".hermes.md"
|
|
415
|
+
};
|
|
416
|
+
function userTargetFor(agent) {
|
|
417
|
+
if (agent === "claude") return join3(homedir3(), ".claude", "CLAUDE.md");
|
|
418
|
+
if (agent === "codex") return join3(homedir3(), ".codex", "AGENTS.md");
|
|
419
|
+
if (agent === "hermes") {
|
|
420
|
+
return join3(process.env.HERMES_HOME ?? join3(homedir3(), ".hermes"), ".hermes.md");
|
|
421
|
+
}
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
function loadSkill() {
|
|
425
|
+
let dir = __dirname2;
|
|
426
|
+
while (dir !== dirname2(dir)) {
|
|
427
|
+
const p = resolve2(dir, "SKILL.md");
|
|
428
|
+
if (existsSync2(p)) return readFileSync2(p, "utf-8");
|
|
429
|
+
dir = dirname2(dir);
|
|
430
|
+
}
|
|
431
|
+
throw new Error("SKILL.md not found in package");
|
|
432
|
+
}
|
|
433
|
+
function detectTargets(cwd) {
|
|
434
|
+
return TARGET_CANDIDATES.filter((p) => existsSync2(resolve2(cwd, p)));
|
|
435
|
+
}
|
|
436
|
+
function detectNewline(content) {
|
|
437
|
+
return content.includes("\r\n") ? "\r\n" : "\n";
|
|
438
|
+
}
|
|
439
|
+
function composeBlock(skill, eol) {
|
|
440
|
+
const body = skill.replace(/\r?\n/g, eol);
|
|
441
|
+
return `${SKILL_BEGIN}${eol}${body}${eol}${SKILL_END}`;
|
|
442
|
+
}
|
|
443
|
+
function applyBlock(existing, block, eol) {
|
|
444
|
+
const b = existing.indexOf(SKILL_BEGIN);
|
|
445
|
+
const e = existing.indexOf(SKILL_END);
|
|
446
|
+
if (b !== -1 && e !== -1) {
|
|
447
|
+
return existing.slice(0, b) + block + existing.slice(e + SKILL_END.length);
|
|
448
|
+
}
|
|
449
|
+
if (existing.length === 0) return `${block}${eol}`;
|
|
450
|
+
const sep2 = existing.endsWith(eol) ? "" : eol;
|
|
451
|
+
return `${existing}${sep2}${block}${eol}`;
|
|
452
|
+
}
|
|
453
|
+
function removeBlock(existing) {
|
|
454
|
+
const b = existing.indexOf(SKILL_BEGIN);
|
|
455
|
+
const e = existing.indexOf(SKILL_END);
|
|
456
|
+
if (b === -1 || e === -1) return null;
|
|
457
|
+
const out = existing.slice(0, b) + existing.slice(e + SKILL_END.length);
|
|
458
|
+
return out.replace(/(\r?\n){2,}$/, "$1");
|
|
459
|
+
}
|
|
460
|
+
function atomicWrite(target, content) {
|
|
461
|
+
atomicWriteFile(target, content);
|
|
462
|
+
}
|
|
463
|
+
function resolveTarget(cwd, opts) {
|
|
464
|
+
if (opts.scope && opts.scope !== "user" && opts.scope !== "project") {
|
|
465
|
+
console.error(`Unknown --scope "${opts.scope}" (expected user or project)`);
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
if (opts.target) return resolve2(cwd, opts.target);
|
|
469
|
+
if (opts.scope === "user") {
|
|
470
|
+
if (!opts.agent) {
|
|
471
|
+
console.error("--scope user requires --agent (claude, codex, or hermes)");
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
const userTarget = userTargetFor(opts.agent);
|
|
475
|
+
if (userTarget) return userTarget;
|
|
476
|
+
console.error(`Unknown --agent "${opts.agent}" (expected claude, codex, or hermes)`);
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
if (opts.agent) {
|
|
480
|
+
const mapped = AGENT_TARGET[opts.agent];
|
|
481
|
+
if (typeof mapped === "string") return resolve2(cwd, mapped);
|
|
482
|
+
console.error(`Unknown --agent "${opts.agent}" (expected claude, codex, or hermes)`);
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
const matches = detectTargets(cwd);
|
|
486
|
+
if (matches.length === 0) return resolve2(cwd, DEFAULT_TARGET);
|
|
487
|
+
if (matches.length === 1) return resolve2(cwd, matches[0]);
|
|
488
|
+
console.error("Multiple rules files detected. Use --target to disambiguate:");
|
|
489
|
+
for (const m of matches) console.error(` ${m}`);
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
function runInstall(cwd, opts) {
|
|
493
|
+
if (opts.agent === "claude" && !opts.target) return installClaudePlugin(cwd, opts);
|
|
494
|
+
const target = resolveTarget(cwd, opts);
|
|
495
|
+
if (target === null) return 1;
|
|
496
|
+
const existing = existsSync2(target) ? readFileSync2(target, "utf-8") : "";
|
|
497
|
+
const eol = existing ? detectNewline(existing) : "\n";
|
|
498
|
+
const block = composeBlock(loadSkill(), eol);
|
|
499
|
+
const next = applyBlock(existing, block, eol);
|
|
500
|
+
if (next === existing) {
|
|
501
|
+
console.log("No changes \u2014 skill block already up to date.");
|
|
502
|
+
return 0;
|
|
503
|
+
}
|
|
504
|
+
if (opts.dryRun) {
|
|
505
|
+
process.stdout.write(createPatch(target, existing, next, "current", "proposed"));
|
|
506
|
+
return 0;
|
|
507
|
+
}
|
|
508
|
+
atomicWrite(target, next);
|
|
509
|
+
console.log(`Wrote ${Buffer.byteLength(next)} bytes to ${target}`);
|
|
510
|
+
return 0;
|
|
511
|
+
}
|
|
512
|
+
function runUninstall(cwd, opts) {
|
|
513
|
+
if (opts.agent === "claude" && !opts.target) return uninstallClaudePlugin(cwd, opts);
|
|
514
|
+
const target = resolveTarget(cwd, opts);
|
|
515
|
+
if (target === null) return 1;
|
|
516
|
+
if (!existsSync2(target)) {
|
|
517
|
+
console.log(`Skill block not present at ${target}`);
|
|
518
|
+
return 0;
|
|
519
|
+
}
|
|
520
|
+
const existing = readFileSync2(target, "utf-8");
|
|
521
|
+
const next = removeBlock(existing);
|
|
522
|
+
if (next === null) {
|
|
523
|
+
console.log(`Skill block not present at ${target}`);
|
|
524
|
+
return 0;
|
|
525
|
+
}
|
|
526
|
+
atomicWrite(target, next);
|
|
527
|
+
console.log(`Removed skill block from ${target}`);
|
|
528
|
+
return 0;
|
|
529
|
+
}
|
|
530
|
+
function runStatus(cwd, opts) {
|
|
531
|
+
if (opts.agent === "claude" && !opts.target) return statusClaudePlugin(cwd, opts);
|
|
532
|
+
const target = resolveTarget(cwd, opts);
|
|
533
|
+
if (target === null) return 1;
|
|
534
|
+
const fileExists = existsSync2(target);
|
|
535
|
+
let installed = false;
|
|
536
|
+
if (fileExists) {
|
|
537
|
+
const content = readFileSync2(target, "utf-8");
|
|
538
|
+
installed = content.includes(SKILL_BEGIN) && content.includes(SKILL_END);
|
|
539
|
+
}
|
|
540
|
+
if (opts.json) {
|
|
541
|
+
printJson({ installed, target });
|
|
542
|
+
return installed ? 0 : 1;
|
|
543
|
+
}
|
|
544
|
+
if (!fileExists) {
|
|
545
|
+
console.log(`No rules file at ${target}`);
|
|
546
|
+
return 1;
|
|
547
|
+
}
|
|
548
|
+
if (installed) {
|
|
549
|
+
console.log(`PRIM SKILL v1 installed at ${target}`);
|
|
550
|
+
return 0;
|
|
551
|
+
}
|
|
552
|
+
console.log(`No PRIM SKILL block at ${target}`);
|
|
553
|
+
return 1;
|
|
554
|
+
}
|
|
555
|
+
function registerSkillCommands(program) {
|
|
556
|
+
const skill = program.command("skill").description("Manage the prim skill in your project rules file");
|
|
557
|
+
skill.command("install").description("Install the prim skill block into your project rules file").option("--target <path>", "Path to the rules file (overrides auto-detection)").option("--agent <agent>", "claude, codex, or hermes (selects the default rules file)").option("--scope <scope>", "project (default, this repo) or user (machine-global \u2014 every repo)").option("--dry-run", "Print a unified diff without writing").action((opts) => {
|
|
558
|
+
try {
|
|
559
|
+
process.exit(runInstall(process.cwd(), opts));
|
|
560
|
+
} catch (err) {
|
|
561
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
562
|
+
process.exit(2);
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
skill.command("uninstall").description("Remove the prim skill block from your project rules file").option("--target <path>", "Path to the rules file (overrides auto-detection)").option("--agent <agent>", "claude, codex, or hermes (selects the default rules file)").option("--scope <scope>", "project (default, this repo) or user (machine-global \u2014 every repo)").action((opts) => {
|
|
566
|
+
process.exit(runUninstall(process.cwd(), opts));
|
|
567
|
+
});
|
|
568
|
+
skill.command("status").description("Report whether the prim skill block is installed").option("--target <path>", "Path to the rules file (overrides auto-detection)").option("--agent <agent>", "claude, codex, or hermes (selects the default rules file)").option("--scope <scope>", "project (default, this repo) or user (machine-global \u2014 every repo)").option("--json", "Output as JSON").action((opts) => {
|
|
569
|
+
process.exit(runStatus(process.cwd(), opts));
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// src/lib/post-commit-hook.ts
|
|
574
|
+
import { execFileSync } from "child_process";
|
|
575
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
576
|
+
import {
|
|
577
|
+
constants as constants3,
|
|
578
|
+
closeSync as closeSync3,
|
|
579
|
+
existsSync as existsSync3,
|
|
580
|
+
fchmodSync,
|
|
581
|
+
fsyncSync,
|
|
582
|
+
lstatSync as lstatSync3,
|
|
583
|
+
mkdirSync as mkdirSync3,
|
|
584
|
+
openSync as openSync3,
|
|
585
|
+
readFileSync as readFileSync3,
|
|
586
|
+
renameSync,
|
|
587
|
+
unlinkSync,
|
|
588
|
+
writeFileSync
|
|
589
|
+
} from "fs";
|
|
590
|
+
import { basename, dirname as dirname3, isAbsolute as isAbsolute2, resolve as resolve3 } from "path";
|
|
591
|
+
var PRIM_POST_COMMIT_BLOCK_START = "# >>> prim post-commit hook >>>";
|
|
592
|
+
var PRIM_POST_COMMIT_BLOCK_END = "# <<< prim post-commit hook <<<";
|
|
593
|
+
var PRIM_CREATED_MARK = "prim-created-post-commit-hook";
|
|
594
|
+
var PRIM_POST_REWRITE_BLOCK_START = "# >>> prim post-rewrite hook >>>";
|
|
595
|
+
var PRIM_POST_REWRITE_BLOCK_END = "# <<< prim post-rewrite hook <<<";
|
|
596
|
+
var PRIM_POST_REWRITE_CREATED_MARK = "prim-created-post-rewrite-hook";
|
|
597
|
+
var LEGACY_PRIM_CREATED_MARK = "prim-created-hook";
|
|
598
|
+
var LEGACY_PRIM_OWNED_HEADER = "# prim post-commit hook \u2014 installed by: prim hooks install (prim-managed-hook)";
|
|
599
|
+
var LEGACY_GLOBAL_OWNED_HEADER = "# prim global post-commit hook (core.hooksPath) \u2014 managed by prim; do not edit.";
|
|
600
|
+
var MAX_HOOK_BYTES = 1048576;
|
|
601
|
+
var GIT_TIMEOUT_MS = 1e3;
|
|
602
|
+
var SUPPORTED_SHEBANGS = /* @__PURE__ */ new Set([
|
|
603
|
+
"#!/bin/sh",
|
|
604
|
+
"#!/usr/bin/env sh",
|
|
605
|
+
"#!/bin/bash",
|
|
606
|
+
"#!/usr/bin/env bash",
|
|
607
|
+
"#!/bin/zsh",
|
|
608
|
+
"#!/usr/bin/env zsh"
|
|
609
|
+
]);
|
|
610
|
+
var HUSKY_V9_DISPATCHERS = /* @__PURE__ */ new Set([
|
|
611
|
+
'#!/usr/bin/env sh\n. "${0%/*}/h"',
|
|
612
|
+
'#!/usr/bin/env sh\n. "$(dirname "$0")/h"'
|
|
613
|
+
]);
|
|
614
|
+
var HUSKY_DIRECT_DISPATCHER = '#!/bin/sh\nexec sh "$(dirname "$0")/../post-commit"\n';
|
|
615
|
+
var HUSKY_V9_RUNTIME_HASHES = /* @__PURE__ */ new Set([
|
|
616
|
+
"d6aaa6f3f7c11008c525e649161df4a93152f1e55d0887fa1d7b8d622735d380",
|
|
617
|
+
"b02cb91cab7125a9e906be723a1b85297fc435a8307d429ae845546b4ee0ecde",
|
|
618
|
+
"9db5d129c4eb822a773157d98cd8fb8f1b156b85e3193117fefe527ca6d45e91",
|
|
619
|
+
"3b2cb21335e544b4ebce658ca47af4a69d2c77026cc4d3f8b2227c34d062b207",
|
|
620
|
+
"548990e1c11285694096184993d907b258d41235461a7ca2551e3b53ff9d3a38",
|
|
621
|
+
"c029a943acea5d6e2ddbaa271a1fb22c2da55d56b5c6fb59ca804ac9a4018708",
|
|
622
|
+
"ae3413a8fe2b39372de48bdc9691f42055f089c0c7529834b2fb07a131585b6a",
|
|
623
|
+
"70200b200ca709b0622784f93839a5b2872333a917a09afddefd7dc2d8cdc680"
|
|
624
|
+
]);
|
|
625
|
+
function currentPostCommitBlock() {
|
|
626
|
+
return `${PRIM_POST_COMMIT_BLOCK_START}
|
|
627
|
+
if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
|
|
628
|
+
prim_commit_sha=$(git rev-parse --verify HEAD 2>/dev/null) || prim_commit_sha=
|
|
629
|
+
case "$prim_commit_sha" in *[!0-9a-f]*|"") prim_commit_sha= ;; esac
|
|
630
|
+
case "\${#prim_commit_sha}" in 40|64) ;; *) prim_commit_sha= ;; esac
|
|
631
|
+
if [ -n "$prim_commit_sha" ]; then
|
|
632
|
+
prim_commit_observed_file=$(mktemp "\${TMPDIR:-/tmp}/prim-post-commit-observed.XXXXXXXX" 2>/dev/null) || prim_commit_observed_file=
|
|
633
|
+
prim_commit_branch=$(git symbolic-ref --quiet --short HEAD 2>/dev/null) || prim_commit_branch=
|
|
634
|
+
prim_cache_dir="${GIT_HOOK_CACHE_SHELL_DIR}"
|
|
635
|
+
prim_post_commit_ran=0
|
|
636
|
+
if [ "\${PRIM_BIN_CACHE:-1}" != "0" ] && [ -f "$prim_cache_dir/prim-post-commit" ] && [ -f "$prim_cache_dir/node" ] && [ -n "$(find "$prim_cache_dir/prim-post-commit" -mmin "-\${PRIM_BIN_CACHE_TTL_MIN:-${GIT_HOOK_CACHE_TTL_MINUTES}}" 2>/dev/null)" ]; then
|
|
637
|
+
prim_node=$(cat "$prim_cache_dir/node")
|
|
638
|
+
prim_entry=$(cat "$prim_cache_dir/prim-post-commit")
|
|
639
|
+
if [ -x "$prim_node" ] && [ -f "$prim_entry" ]; then
|
|
640
|
+
( trap '' HUP; trap 'if [ -n "$prim_commit_observed_file" ]; then rm -f "$prim_commit_observed_file"; fi' 0; export PRIM_COMMIT_SHA="$prim_commit_sha" PRIM_COMMIT_BRANCH="$prim_commit_branch" PRIM_COMMIT_OBSERVED_FILE="$prim_commit_observed_file"; "$prim_node" "$prim_entry" ) </dev/null >/dev/null 2>&1 &
|
|
641
|
+
prim_post_commit_ran=1
|
|
642
|
+
fi
|
|
643
|
+
fi
|
|
644
|
+
if [ "$prim_post_commit_ran" -eq 0 ] && command -v npx >/dev/null 2>&1; then
|
|
645
|
+
( trap '' HUP; trap 'if [ -n "$prim_commit_observed_file" ]; then rm -f "$prim_commit_observed_file"; fi' 0; export PRIM_COMMIT_SHA="$prim_commit_sha" PRIM_COMMIT_BRANCH="$prim_commit_branch" PRIM_COMMIT_OBSERVED_FILE="$prim_commit_observed_file"; ${pinnedNpxCommand("prim-post-commit")} ) </dev/null >/dev/null 2>&1 &
|
|
646
|
+
prim_post_commit_ran=1
|
|
647
|
+
fi
|
|
648
|
+
if [ "$prim_post_commit_ran" -eq 0 ] && [ -n "$prim_commit_observed_file" ]; then
|
|
649
|
+
rm -f "$prim_commit_observed_file"
|
|
650
|
+
fi
|
|
651
|
+
unset prim_cache_dir prim_post_commit_ran prim_node prim_entry prim_commit_branch prim_commit_observed_file
|
|
652
|
+
fi
|
|
653
|
+
unset prim_commit_sha
|
|
654
|
+
fi
|
|
655
|
+
${PRIM_POST_COMMIT_BLOCK_END}`;
|
|
656
|
+
}
|
|
657
|
+
function postCommitHookBlock() {
|
|
658
|
+
return currentPostCommitBlock();
|
|
659
|
+
}
|
|
660
|
+
function postRewriteHookBlock() {
|
|
661
|
+
return `${PRIM_POST_REWRITE_BLOCK_START}
|
|
662
|
+
if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
|
|
663
|
+
case "$1" in amend|rebase) prim_rewrite_source="$1" ;; *) prim_rewrite_source= ;; esac
|
|
664
|
+
if [ -n "$prim_rewrite_source" ]; then
|
|
665
|
+
prim_rewrite_pairs_file=$(mktemp "\${TMPDIR:-/tmp}/prim-post-rewrite-pairs.XXXXXXXX" 2>/dev/null) || prim_rewrite_pairs_file=
|
|
666
|
+
if [ -n "$prim_rewrite_pairs_file" ] && ! chmod 600 "$prim_rewrite_pairs_file" 2>/dev/null; then
|
|
667
|
+
rm -f "$prim_rewrite_pairs_file"
|
|
668
|
+
prim_rewrite_pairs_file=
|
|
669
|
+
fi
|
|
670
|
+
if [ -n "$prim_rewrite_pairs_file" ]; then
|
|
671
|
+
if cat > "$prim_rewrite_pairs_file"; then
|
|
672
|
+
exec < "$prim_rewrite_pairs_file"
|
|
673
|
+
prim_rewrite_branch=$(git symbolic-ref --quiet --short HEAD 2>/dev/null) || prim_rewrite_branch=
|
|
674
|
+
prim_cache_dir="${GIT_HOOK_CACHE_SHELL_DIR}"
|
|
675
|
+
prim_post_rewrite_ran=0
|
|
676
|
+
if [ "\${PRIM_BIN_CACHE:-1}" != "0" ] && [ -f "$prim_cache_dir/prim-post-rewrite" ] && [ -f "$prim_cache_dir/node" ] && [ -n "$(find "$prim_cache_dir/prim-post-rewrite" -mmin "-\${PRIM_BIN_CACHE_TTL_MIN:-${GIT_HOOK_CACHE_TTL_MINUTES}}" 2>/dev/null)" ]; then
|
|
677
|
+
prim_node=$(cat "$prim_cache_dir/node")
|
|
678
|
+
prim_entry=$(cat "$prim_cache_dir/prim-post-rewrite")
|
|
679
|
+
if [ -x "$prim_node" ] && [ -f "$prim_entry" ]; then
|
|
680
|
+
( trap '' HUP; trap 'if [ -n "$prim_rewrite_pairs_file" ]; then rm -f "$prim_rewrite_pairs_file"; fi' 0; export PRIM_REWRITE_SOURCE="$prim_rewrite_source" PRIM_REWRITE_BRANCH="$prim_rewrite_branch" PRIM_REWRITE_PAIRS_FILE="$prim_rewrite_pairs_file"; "$prim_node" "$prim_entry" ) </dev/null >/dev/null 2>&1 &
|
|
681
|
+
prim_post_rewrite_ran=1
|
|
682
|
+
fi
|
|
683
|
+
fi
|
|
684
|
+
if [ "$prim_post_rewrite_ran" -eq 0 ] && command -v npx >/dev/null 2>&1; then
|
|
685
|
+
( trap '' HUP; trap 'if [ -n "$prim_rewrite_pairs_file" ]; then rm -f "$prim_rewrite_pairs_file"; fi' 0; export PRIM_REWRITE_SOURCE="$prim_rewrite_source" PRIM_REWRITE_BRANCH="$prim_rewrite_branch" PRIM_REWRITE_PAIRS_FILE="$prim_rewrite_pairs_file"; ${pinnedNpxCommand("prim-post-rewrite")} ) </dev/null >/dev/null 2>&1 &
|
|
686
|
+
prim_post_rewrite_ran=1
|
|
687
|
+
fi
|
|
688
|
+
if [ "$prim_post_rewrite_ran" -eq 0 ]; then
|
|
689
|
+
rm -f "$prim_rewrite_pairs_file"
|
|
690
|
+
fi
|
|
691
|
+
unset prim_cache_dir prim_post_rewrite_ran prim_node prim_entry prim_rewrite_branch
|
|
692
|
+
else
|
|
693
|
+
rm -f "$prim_rewrite_pairs_file"
|
|
694
|
+
fi
|
|
695
|
+
unset prim_rewrite_pairs_file
|
|
696
|
+
fi
|
|
697
|
+
fi
|
|
698
|
+
unset prim_rewrite_source
|
|
699
|
+
fi
|
|
700
|
+
${PRIM_POST_REWRITE_BLOCK_END}`;
|
|
701
|
+
}
|
|
702
|
+
var POST_COMMIT_MANAGED_HOOK = {
|
|
703
|
+
hookName: "post-commit",
|
|
704
|
+
blockStart: PRIM_POST_COMMIT_BLOCK_START,
|
|
705
|
+
blockEnd: PRIM_POST_COMMIT_BLOCK_END,
|
|
706
|
+
createdMark: PRIM_CREATED_MARK,
|
|
707
|
+
block: postCommitHookBlock
|
|
708
|
+
};
|
|
709
|
+
var POST_REWRITE_MANAGED_HOOK = {
|
|
710
|
+
hookName: "post-rewrite",
|
|
711
|
+
blockStart: PRIM_POST_REWRITE_BLOCK_START,
|
|
712
|
+
blockEnd: PRIM_POST_REWRITE_BLOCK_END,
|
|
713
|
+
createdMark: PRIM_POST_REWRITE_CREATED_MARK,
|
|
714
|
+
block: postRewriteHookBlock
|
|
715
|
+
};
|
|
716
|
+
function legacyFloatingInvocation() {
|
|
717
|
+
return `if command -v prim-post-commit >/dev/null 2>&1; then
|
|
718
|
+
prim-post-commit || true
|
|
719
|
+
elif [ -f "./node_modules/.bin/prim-post-commit" ]; then
|
|
720
|
+
./node_modules/.bin/prim-post-commit || true
|
|
721
|
+
else
|
|
722
|
+
npx --yes -p @primitive.ai/prim prim-post-commit 2>/dev/null || true
|
|
723
|
+
fi`;
|
|
724
|
+
}
|
|
725
|
+
function legacyGlobalGate() {
|
|
726
|
+
return Buffer.from(`if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
|
|
727
|
+
${legacyFloatingInvocation()}
|
|
728
|
+
fi
|
|
729
|
+
`);
|
|
730
|
+
}
|
|
731
|
+
var PINNED_INVOCATION_RE = /^\{ if \[ -x (?<node>'.*') \] && \[ -f (?<entry>'.*') \]; then \k<node> \k<entry>; else npx --yes -p @primitive\.ai\/prim@(?<version>[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?) prim-post-commit; fi; \} \|\| true$/u;
|
|
732
|
+
var SHELL_QUOTED_RE = /^'(?:[^']|'"'"')*'$/u;
|
|
733
|
+
function decodePinnedPath(token) {
|
|
734
|
+
if (!token || !SHELL_QUOTED_RE.test(token)) return void 0;
|
|
735
|
+
const value = token.slice(1, -1).replaceAll(`'"'"'`, "'");
|
|
736
|
+
if (!isAbsolute2(value) || value.includes("\0") || value.includes("\n") || value.includes("\r")) {
|
|
737
|
+
return void 0;
|
|
738
|
+
}
|
|
739
|
+
return value;
|
|
740
|
+
}
|
|
741
|
+
function isPinnedInvocation(value) {
|
|
742
|
+
const match = PINNED_INVOCATION_RE.exec(value);
|
|
743
|
+
const node = decodePinnedPath(match?.groups?.node);
|
|
744
|
+
const entry = decodePinnedPath(match?.groups?.entry);
|
|
745
|
+
return Boolean(node && entry?.endsWith("/dist/hooks/post-commit.js"));
|
|
746
|
+
}
|
|
747
|
+
function legacyProjectPrefixLength(existing) {
|
|
748
|
+
const prefix = Buffer.from(`#!/bin/sh
|
|
749
|
+
${LEGACY_PRIM_OWNED_HEADER}
|
|
750
|
+
|
|
751
|
+
`);
|
|
752
|
+
if (!existing.subarray(0, prefix.length).equals(prefix)) return void 0;
|
|
753
|
+
const floating = Buffer.from(`${legacyFloatingInvocation()}
|
|
754
|
+
`);
|
|
755
|
+
if (existing.subarray(prefix.length, prefix.length + floating.length).equals(floating)) {
|
|
756
|
+
return prefix.length + floating.length;
|
|
757
|
+
}
|
|
758
|
+
const invocationEnd = existing.indexOf(10, prefix.length);
|
|
759
|
+
if (invocationEnd === -1) return void 0;
|
|
760
|
+
const invocation = existing.subarray(prefix.length, invocationEnd).toString("utf8");
|
|
761
|
+
return isPinnedInvocation(invocation) ? invocationEnd + 1 : void 0;
|
|
762
|
+
}
|
|
763
|
+
function isKnownLegacyGlobalGate(value) {
|
|
764
|
+
if (value.equals(legacyGlobalGate())) return true;
|
|
765
|
+
const prefix = Buffer.from(
|
|
766
|
+
'if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then\n'
|
|
767
|
+
);
|
|
768
|
+
const suffix = Buffer.from("\nfi\n");
|
|
769
|
+
return value.subarray(0, prefix.length).equals(prefix) && value.subarray(-suffix.length).equals(suffix) && isPinnedInvocation(value.subarray(prefix.length, -suffix.length).toString("utf8"));
|
|
770
|
+
}
|
|
771
|
+
function createdScaffold(spec) {
|
|
772
|
+
return Buffer.from(`#!/bin/sh
|
|
773
|
+
${spec.block()}
|
|
774
|
+
# ${spec.createdMark}
|
|
775
|
+
`);
|
|
776
|
+
}
|
|
777
|
+
function safeGitPath(root, value) {
|
|
778
|
+
if (value.length === 0 || value !== value.trim() || value.includes("\0") || value.includes("\n") || value.includes("\r")) {
|
|
779
|
+
throw new Error("Git returned an unsafe hooks path");
|
|
780
|
+
}
|
|
781
|
+
return isAbsolute2(value) ? resolve3(value) : resolve3(root, value);
|
|
782
|
+
}
|
|
783
|
+
function resolveEffectiveManagedHook(spec, cwd = process.cwd()) {
|
|
784
|
+
const gitRoot = gitToplevel(cwd);
|
|
785
|
+
if (!gitRoot) throw new Error("not a git repository");
|
|
786
|
+
const value = execFileSync("git", ["rev-parse", "--git-path", "hooks"], {
|
|
787
|
+
cwd: gitRoot,
|
|
788
|
+
encoding: "utf8",
|
|
789
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
790
|
+
timeout: GIT_TIMEOUT_MS
|
|
791
|
+
}).replace(/\r?\n$/u, "");
|
|
792
|
+
const hooksDir = safeGitPath(gitRoot, value);
|
|
793
|
+
if (basename(hooksDir) === "_" && basename(dirname3(hooksDir)) === ".husky") {
|
|
794
|
+
return {
|
|
795
|
+
gitRoot,
|
|
796
|
+
hooksDir,
|
|
797
|
+
hookPath: resolve3(dirname3(hooksDir), spec.hookName),
|
|
798
|
+
kind: "husky_v9",
|
|
799
|
+
dispatcherPath: resolve3(hooksDir, spec.hookName)
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
return {
|
|
803
|
+
gitRoot,
|
|
804
|
+
hooksDir,
|
|
805
|
+
hookPath: resolve3(hooksDir, spec.hookName),
|
|
806
|
+
kind: "direct"
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
function resolveEffectivePostCommitHook(cwd = process.cwd()) {
|
|
810
|
+
return resolveEffectiveManagedHook(POST_COMMIT_MANAGED_HOOK, cwd);
|
|
811
|
+
}
|
|
812
|
+
function resolveEffectivePostRewriteHook(cwd = process.cwd()) {
|
|
813
|
+
return resolveEffectiveManagedHook(POST_REWRITE_MANAGED_HOOK, cwd);
|
|
814
|
+
}
|
|
815
|
+
function assertSafeFile(path, spec) {
|
|
816
|
+
if (!existsSync3(path)) return void 0;
|
|
817
|
+
const stat = lstatSync3(path);
|
|
818
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_HOOK_BYTES) {
|
|
819
|
+
throw new Error(`unsafe ${spec.hookName} hook target: ${path}`);
|
|
820
|
+
}
|
|
821
|
+
return stat;
|
|
822
|
+
}
|
|
823
|
+
function assertUsableHuskyDispatcher(target, spec) {
|
|
824
|
+
if (!target.dispatcherPath) return;
|
|
825
|
+
const stat = assertSafeFile(target.dispatcherPath, spec);
|
|
826
|
+
if (!stat || (stat.mode & 64) === 0) {
|
|
827
|
+
throw new Error(
|
|
828
|
+
`Husky ${spec.hookName} dispatcher is missing or not executable: ${target.dispatcherPath}`
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
const dispatcher = decodeHookText(readHookFile(target.dispatcherPath), spec);
|
|
832
|
+
const directDispatcher = HUSKY_DIRECT_DISPATCHER.replaceAll("post-commit", spec.hookName);
|
|
833
|
+
if (dispatcher === directDispatcher) return;
|
|
834
|
+
const normalizedDispatcher = dispatcher.endsWith("\n") ? dispatcher.slice(0, -1) : dispatcher;
|
|
835
|
+
if (!HUSKY_V9_DISPATCHERS.has(normalizedDispatcher)) {
|
|
836
|
+
throw new Error(`unrecognized Husky ${spec.hookName} dispatcher: ${target.dispatcherPath}`);
|
|
837
|
+
}
|
|
838
|
+
const huskyRuntime = resolve3(dirname3(target.dispatcherPath), "h");
|
|
839
|
+
const runtimeStat = assertSafeFile(huskyRuntime, spec);
|
|
840
|
+
const runtime = runtimeStat ? readHookFile(huskyRuntime) : void 0;
|
|
841
|
+
const runtimeHash = runtime ? createHash2("sha256").update(runtime).digest("hex") : void 0;
|
|
842
|
+
if (!runtimeHash || !HUSKY_V9_RUNTIME_HASHES.has(runtimeHash)) {
|
|
843
|
+
throw new Error(`unrecognized Husky v9 hook runtime: ${huskyRuntime}`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
function readHookFile(path) {
|
|
847
|
+
const value = readFileSync3(path);
|
|
848
|
+
return Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
849
|
+
}
|
|
850
|
+
function decodeHookText(content, spec) {
|
|
851
|
+
if (content.includes(0)) throw new Error(`binary ${spec.hookName} hook`);
|
|
852
|
+
try {
|
|
853
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(content);
|
|
854
|
+
} catch {
|
|
855
|
+
throw new Error(`binary ${spec.hookName} hook`);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
function supportedShebangEnd(content, spec) {
|
|
859
|
+
const text = decodeHookText(content, spec);
|
|
860
|
+
const newline = text.indexOf("\n");
|
|
861
|
+
const shebang = newline === -1 ? text : text.slice(0, newline);
|
|
862
|
+
if (!SUPPORTED_SHEBANGS.has(shebang) || newline === -1) {
|
|
863
|
+
throw new Error(`unsupported ${spec.hookName} hook interpreter`);
|
|
864
|
+
}
|
|
865
|
+
return Buffer.byteLength(text.slice(0, newline + 1), "utf8");
|
|
866
|
+
}
|
|
867
|
+
function shellInsertionPoint(content, allowShebangless, spec) {
|
|
868
|
+
const text = decodeHookText(content, spec);
|
|
869
|
+
if (text.startsWith("#!")) return supportedShebangEnd(content, spec);
|
|
870
|
+
if (allowShebangless) return 0;
|
|
871
|
+
throw new Error(`unsupported ${spec.hookName} hook interpreter`);
|
|
872
|
+
}
|
|
873
|
+
function allIndexes(haystack, needle) {
|
|
874
|
+
const indexes = [];
|
|
875
|
+
let offset = 0;
|
|
876
|
+
while (offset <= haystack.length - needle.length) {
|
|
877
|
+
const index = haystack.indexOf(needle, offset);
|
|
878
|
+
if (index === -1) break;
|
|
879
|
+
indexes.push(index);
|
|
880
|
+
offset = index + needle.length;
|
|
881
|
+
}
|
|
882
|
+
return indexes;
|
|
883
|
+
}
|
|
884
|
+
function blockRange(content, spec) {
|
|
885
|
+
decodeHookText(content, spec);
|
|
886
|
+
const startMarker = Buffer.from(spec.blockStart);
|
|
887
|
+
const endMarker = Buffer.from(spec.blockEnd);
|
|
888
|
+
const starts = allIndexes(content, startMarker);
|
|
889
|
+
const ends = allIndexes(content, endMarker);
|
|
890
|
+
if (starts.length === 0 && ends.length === 0) return { kind: "absent" };
|
|
891
|
+
if (starts.length !== 1 || ends.length !== 1 || ends[0] < starts[0]) {
|
|
892
|
+
throw new Error(`malformed Prim ${spec.hookName} markers`);
|
|
893
|
+
}
|
|
894
|
+
const start = starts[0];
|
|
895
|
+
const end = ends[0] + endMarker.length;
|
|
896
|
+
const current = Buffer.from(spec.block());
|
|
897
|
+
return content.subarray(start, end).equals(current) ? { kind: "current", start, end } : { kind: "stale", start, end };
|
|
898
|
+
}
|
|
899
|
+
function mergedContent(existing, allowShebangless, spec) {
|
|
900
|
+
const block = Buffer.from(spec.block());
|
|
901
|
+
if (!existing) {
|
|
902
|
+
return createdScaffold(spec);
|
|
903
|
+
}
|
|
904
|
+
const range = blockRange(existing, spec);
|
|
905
|
+
if (spec.hookName === POST_COMMIT_MANAGED_HOOK.hookName && range.kind === "absent" && existing.toString("utf8").startsWith(`#!/bin/sh
|
|
906
|
+
${LEGACY_GLOBAL_OWNED_HEADER}
|
|
907
|
+
`)) {
|
|
908
|
+
const gateAt = existing.indexOf(Buffer.from('if [ "$(git config --get prim.active'));
|
|
909
|
+
const chainAt = existing.indexOf(Buffer.from("common_dir=$(git rev-parse --git-common-dir"));
|
|
910
|
+
if (gateAt === -1 || chainAt <= gateAt) {
|
|
911
|
+
throw new Error("malformed legacy Prim global post-commit hook");
|
|
912
|
+
}
|
|
913
|
+
if (!isKnownLegacyGlobalGate(existing.subarray(gateAt, chainAt))) {
|
|
914
|
+
throw new Error("unrecognized legacy Prim global post-commit invocation");
|
|
915
|
+
}
|
|
916
|
+
const withoutLegacyGate = Buffer.concat([
|
|
917
|
+
existing.subarray(0, gateAt),
|
|
918
|
+
existing.subarray(chainAt)
|
|
919
|
+
]);
|
|
920
|
+
const shebangEnd2 = shellInsertionPoint(withoutLegacyGate, allowShebangless, spec);
|
|
921
|
+
return Buffer.concat([
|
|
922
|
+
withoutLegacyGate.subarray(0, shebangEnd2),
|
|
923
|
+
block,
|
|
924
|
+
Buffer.from("\n"),
|
|
925
|
+
withoutLegacyGate.subarray(shebangEnd2)
|
|
926
|
+
]);
|
|
927
|
+
}
|
|
928
|
+
if (spec.hookName === POST_COMMIT_MANAGED_HOOK.hookName && range.kind === "absent" && existing.toString("utf8").startsWith(`#!/bin/sh
|
|
929
|
+
${LEGACY_PRIM_OWNED_HEADER}
|
|
930
|
+
`)) {
|
|
931
|
+
const prefixLength = legacyProjectPrefixLength(existing);
|
|
932
|
+
if (prefixLength === void 0) {
|
|
933
|
+
throw new Error("unrecognized legacy Prim post-commit invocation");
|
|
934
|
+
}
|
|
935
|
+
return Buffer.concat([createdScaffold(spec), existing.subarray(prefixLength)]);
|
|
936
|
+
}
|
|
937
|
+
if (range.kind !== "absent") {
|
|
938
|
+
const oldCreatedPrefixes = [
|
|
939
|
+
Buffer.from(`#!/bin/sh
|
|
940
|
+
# ${spec.createdMark}
|
|
941
|
+
|
|
942
|
+
`),
|
|
943
|
+
...spec.hookName === POST_COMMIT_MANAGED_HOOK.hookName ? [Buffer.from(`#!/bin/sh
|
|
944
|
+
# ${LEGACY_PRIM_CREATED_MARK}
|
|
945
|
+
|
|
946
|
+
`)] : []
|
|
947
|
+
];
|
|
948
|
+
const oldCreated = oldCreatedPrefixes.some(
|
|
949
|
+
(prefix) => range.start === prefix.length && existing.subarray(0, prefix.length).equals(prefix)
|
|
950
|
+
);
|
|
951
|
+
if (oldCreated) {
|
|
952
|
+
const suffix = existing.subarray(range.end);
|
|
953
|
+
const tail = suffix.at(0) === 10 ? suffix.subarray(1) : suffix;
|
|
954
|
+
return Buffer.concat([createdScaffold(spec), tail]);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
const shebangEnd = shellInsertionPoint(existing, allowShebangless, spec);
|
|
958
|
+
if (range.kind === "current" && range.start === shebangEnd) return existing;
|
|
959
|
+
let withoutBlock = existing;
|
|
960
|
+
if (range.kind !== "absent") {
|
|
961
|
+
const suffix = existing.subarray(range.end);
|
|
962
|
+
withoutBlock = Buffer.concat([
|
|
963
|
+
existing.subarray(0, range.start),
|
|
964
|
+
range.start === shebangEnd && suffix.at(0) === 10 ? suffix.subarray(1) : suffix
|
|
965
|
+
]);
|
|
966
|
+
}
|
|
967
|
+
return Buffer.concat([
|
|
968
|
+
withoutBlock.subarray(0, shebangEnd),
|
|
969
|
+
block,
|
|
970
|
+
Buffer.from("\n"),
|
|
971
|
+
withoutBlock.subarray(shebangEnd)
|
|
972
|
+
]);
|
|
973
|
+
}
|
|
974
|
+
function errorCode(error) {
|
|
975
|
+
return typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
976
|
+
}
|
|
977
|
+
function assertTargetUnchanged(path, expected, spec, stat) {
|
|
978
|
+
let current;
|
|
979
|
+
try {
|
|
980
|
+
current = lstatSync3(path);
|
|
981
|
+
} catch (error) {
|
|
982
|
+
if (expected === void 0 && errorCode(error) === "ENOENT") return;
|
|
983
|
+
throw new Error(`${spec.hookName} hook changed concurrently: ${path}`);
|
|
984
|
+
}
|
|
985
|
+
if (expected === void 0) {
|
|
986
|
+
throw new Error(`${spec.hookName} hook appeared concurrently: ${path}`);
|
|
987
|
+
}
|
|
988
|
+
if (!current.isFile() || current.isSymbolicLink() || current.size > MAX_HOOK_BYTES || current.dev !== stat?.dev || current.ino !== stat?.ino || (current.mode & 4095) !== ((stat?.mode ?? 0) & 4095) || !readHookFile(path).equals(expected)) {
|
|
989
|
+
throw new Error(`${spec.hookName} hook changed concurrently: ${path}`);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
function rewriteHookAtomically(path, next, expected, spec, stat) {
|
|
993
|
+
const parent = dirname3(path);
|
|
994
|
+
mkdirSync3(parent, { recursive: true });
|
|
995
|
+
const temporary = resolve3(
|
|
996
|
+
parent,
|
|
997
|
+
`.${basename(path)}.prim-${String(process.pid)}-${randomUUID()}.tmp`
|
|
998
|
+
);
|
|
999
|
+
let fd;
|
|
1000
|
+
try {
|
|
1001
|
+
fd = openSync3(
|
|
1002
|
+
temporary,
|
|
1003
|
+
constants3.O_CREAT | constants3.O_EXCL | constants3.O_WRONLY | constants3.O_NOFOLLOW,
|
|
1004
|
+
stat?.mode ?? 493
|
|
1005
|
+
);
|
|
1006
|
+
writeFileSync(fd, next);
|
|
1007
|
+
fchmodSync(fd, (stat?.mode ?? 493) & 4095);
|
|
1008
|
+
fsyncSync(fd);
|
|
1009
|
+
closeSync3(fd);
|
|
1010
|
+
fd = void 0;
|
|
1011
|
+
assertTargetUnchanged(path, expected, spec, stat);
|
|
1012
|
+
renameSync(temporary, path);
|
|
1013
|
+
} finally {
|
|
1014
|
+
if (fd !== void 0) {
|
|
1015
|
+
try {
|
|
1016
|
+
closeSync3(fd);
|
|
1017
|
+
} catch {
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
try {
|
|
1021
|
+
unlinkSync(temporary);
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
if (errorCode(error) !== "ENOENT") {
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
function unlinkHookUnchanged(path, expected, stat, spec) {
|
|
1029
|
+
assertTargetUnchanged(path, expected, spec, stat);
|
|
1030
|
+
unlinkSync(path);
|
|
1031
|
+
}
|
|
1032
|
+
function ensureEffectivePostCommitHook(cwd = process.cwd()) {
|
|
1033
|
+
const target = resolveEffectivePostCommitHook(cwd);
|
|
1034
|
+
return ensureTarget(target, POST_COMMIT_MANAGED_HOOK);
|
|
1035
|
+
}
|
|
1036
|
+
function ensureEffectivePostRewriteHook(cwd = process.cwd()) {
|
|
1037
|
+
const target = resolveEffectivePostRewriteHook(cwd);
|
|
1038
|
+
return ensureTarget(target, POST_REWRITE_MANAGED_HOOK);
|
|
1039
|
+
}
|
|
1040
|
+
function ensureTarget(target, spec, initialContent) {
|
|
1041
|
+
assertUsableHuskyDispatcher(target, spec);
|
|
1042
|
+
const stat = assertSafeFile(target.hookPath, spec);
|
|
1043
|
+
if (stat && target.kind === "direct" && (stat.mode & 64) === 0) {
|
|
1044
|
+
throw new Error(`existing ${spec.hookName} hook is not executable: ${target.hookPath}`);
|
|
1045
|
+
}
|
|
1046
|
+
const existing = stat ? readHookFile(target.hookPath) : void 0;
|
|
1047
|
+
const next = !existing && initialContent !== void 0 ? Buffer.from(initialContent) : mergedContent(existing, target.kind === "husky_v9", spec);
|
|
1048
|
+
if (!existing && initialContent !== void 0) {
|
|
1049
|
+
const shebangEnd = shellInsertionPoint(next, target.kind === "husky_v9", spec);
|
|
1050
|
+
const range = blockRange(next, spec);
|
|
1051
|
+
if (range.kind !== "current" || range.start !== shebangEnd) {
|
|
1052
|
+
throw new Error(`invalid initial Prim ${spec.hookName} scaffold`);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
const contentChanged = !existing || !next.equals(existing);
|
|
1056
|
+
if (contentChanged) {
|
|
1057
|
+
rewriteHookAtomically(target.hookPath, next, existing, spec, stat);
|
|
1058
|
+
}
|
|
1059
|
+
return { path: target.hookPath, changed: contentChanged || !stat, kind: target.kind };
|
|
1060
|
+
}
|
|
1061
|
+
function ensurePostCommitHookAtPath(hookPath, initialContent) {
|
|
1062
|
+
const absolute = resolve3(hookPath);
|
|
1063
|
+
const result = ensureTarget(
|
|
1064
|
+
{
|
|
1065
|
+
gitRoot: dirname3(dirname3(absolute)),
|
|
1066
|
+
hooksDir: dirname3(absolute),
|
|
1067
|
+
hookPath: absolute,
|
|
1068
|
+
kind: "direct"
|
|
1069
|
+
},
|
|
1070
|
+
POST_COMMIT_MANAGED_HOOK,
|
|
1071
|
+
initialContent
|
|
1072
|
+
);
|
|
1073
|
+
return { ...result, kind: "direct" };
|
|
1074
|
+
}
|
|
1075
|
+
function ensurePostRewriteHookAtPath(hookPath, initialContent) {
|
|
1076
|
+
const absolute = resolve3(hookPath);
|
|
1077
|
+
const result = ensureTarget(
|
|
1078
|
+
{
|
|
1079
|
+
gitRoot: dirname3(dirname3(absolute)),
|
|
1080
|
+
hooksDir: dirname3(absolute),
|
|
1081
|
+
hookPath: absolute,
|
|
1082
|
+
kind: "direct"
|
|
1083
|
+
},
|
|
1084
|
+
POST_REWRITE_MANAGED_HOOK,
|
|
1085
|
+
initialContent
|
|
1086
|
+
);
|
|
1087
|
+
return { ...result, kind: "direct" };
|
|
1088
|
+
}
|
|
1089
|
+
function inspectEffectivePostCommitHook(cwd = process.cwd()) {
|
|
1090
|
+
return inspectEffectiveManagedHook(POST_COMMIT_MANAGED_HOOK, cwd);
|
|
1091
|
+
}
|
|
1092
|
+
function inspectEffectivePostRewriteHook(cwd = process.cwd()) {
|
|
1093
|
+
return inspectEffectiveManagedHook(POST_REWRITE_MANAGED_HOOK, cwd);
|
|
1094
|
+
}
|
|
1095
|
+
function inspectEffectiveManagedHook(spec, cwd) {
|
|
1096
|
+
const target = resolveEffectiveManagedHook(spec, cwd);
|
|
1097
|
+
try {
|
|
1098
|
+
assertUsableHuskyDispatcher(target, spec);
|
|
1099
|
+
} catch (error) {
|
|
1100
|
+
const missing = error instanceof Error && (error.message.includes("missing or not executable") || error.message.includes("unsafe"));
|
|
1101
|
+
return {
|
|
1102
|
+
...target,
|
|
1103
|
+
covered: false,
|
|
1104
|
+
executable: false,
|
|
1105
|
+
current: false,
|
|
1106
|
+
reason: missing ? "husky_dispatcher_missing" : "husky_dispatcher_invalid"
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
let stat;
|
|
1110
|
+
try {
|
|
1111
|
+
stat = assertSafeFile(target.hookPath, spec);
|
|
1112
|
+
} catch {
|
|
1113
|
+
return {
|
|
1114
|
+
...target,
|
|
1115
|
+
covered: false,
|
|
1116
|
+
executable: false,
|
|
1117
|
+
current: false,
|
|
1118
|
+
reason: "unsafe_target"
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
if (!stat) {
|
|
1122
|
+
return {
|
|
1123
|
+
...target,
|
|
1124
|
+
covered: false,
|
|
1125
|
+
executable: false,
|
|
1126
|
+
current: false,
|
|
1127
|
+
reason: "missing"
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
const executable = target.kind === "husky_v9" || (stat.mode & 64) !== 0;
|
|
1131
|
+
try {
|
|
1132
|
+
const content = readHookFile(target.hookPath);
|
|
1133
|
+
const shebangEnd = shellInsertionPoint(content, target.kind === "husky_v9", spec);
|
|
1134
|
+
const range = blockRange(content, spec);
|
|
1135
|
+
const positioned = range.kind !== "absent" && range.start === shebangEnd;
|
|
1136
|
+
const current = range.kind === "current" && positioned;
|
|
1137
|
+
return {
|
|
1138
|
+
...target,
|
|
1139
|
+
covered: current && executable,
|
|
1140
|
+
executable,
|
|
1141
|
+
current,
|
|
1142
|
+
reason: range.kind === "current" && !positioned ? "unreachable_block" : !current ? "stale_block" : executable ? void 0 : "not_executable"
|
|
1143
|
+
};
|
|
1144
|
+
} catch (error) {
|
|
1145
|
+
const message = error instanceof Error ? error.message : "";
|
|
1146
|
+
const reason = message.includes("binary") ? "binary" : message.includes("unsupported") ? "unsupported_interpreter" : "malformed_markers";
|
|
1147
|
+
return {
|
|
1148
|
+
...target,
|
|
1149
|
+
covered: false,
|
|
1150
|
+
executable,
|
|
1151
|
+
current: false,
|
|
1152
|
+
reason
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
function uninstallEffectivePostCommitHook(cwd = process.cwd()) {
|
|
1157
|
+
const target = resolveEffectivePostCommitHook(cwd);
|
|
1158
|
+
return uninstallTarget(target, POST_COMMIT_MANAGED_HOOK);
|
|
1159
|
+
}
|
|
1160
|
+
function uninstallEffectivePostRewriteHook(cwd = process.cwd()) {
|
|
1161
|
+
const target = resolveEffectivePostRewriteHook(cwd);
|
|
1162
|
+
return uninstallTarget(target, POST_REWRITE_MANAGED_HOOK);
|
|
1163
|
+
}
|
|
1164
|
+
function projectHooksPathIsConfigured(gitRoot) {
|
|
1165
|
+
for (const scope of ["--worktree", "--local"]) {
|
|
1166
|
+
try {
|
|
1167
|
+
const value = execFileSync("git", ["config", scope, "--get", "core.hooksPath"], {
|
|
1168
|
+
cwd: gitRoot,
|
|
1169
|
+
encoding: "utf8",
|
|
1170
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1171
|
+
timeout: GIT_TIMEOUT_MS
|
|
1172
|
+
}).trim();
|
|
1173
|
+
if (value.length > 0) return true;
|
|
1174
|
+
} catch {
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
return false;
|
|
1178
|
+
}
|
|
1179
|
+
function uninstallProjectPostCommitHook(cwd = process.cwd()) {
|
|
1180
|
+
const gitRoot = gitToplevel(cwd);
|
|
1181
|
+
if (!gitRoot) throw new Error("not a git repository");
|
|
1182
|
+
if (projectHooksPathIsConfigured(gitRoot)) {
|
|
1183
|
+
return uninstallEffectivePostCommitHook(gitRoot);
|
|
1184
|
+
}
|
|
1185
|
+
const commonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
|
|
1186
|
+
cwd: gitRoot,
|
|
1187
|
+
encoding: "utf8",
|
|
1188
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1189
|
+
timeout: GIT_TIMEOUT_MS
|
|
1190
|
+
}).replace(/\r?\n$/u, "");
|
|
1191
|
+
return uninstallPostCommitHookAtPath(
|
|
1192
|
+
resolve3(safeGitPath(gitRoot, commonDir), "hooks", "post-commit")
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
function uninstallProjectPostRewriteHook(cwd = process.cwd()) {
|
|
1196
|
+
const gitRoot = gitToplevel(cwd);
|
|
1197
|
+
if (!gitRoot) throw new Error("not a git repository");
|
|
1198
|
+
if (projectHooksPathIsConfigured(gitRoot)) {
|
|
1199
|
+
return uninstallEffectivePostRewriteHook(gitRoot);
|
|
1200
|
+
}
|
|
1201
|
+
const commonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
|
|
1202
|
+
cwd: gitRoot,
|
|
1203
|
+
encoding: "utf8",
|
|
1204
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1205
|
+
timeout: GIT_TIMEOUT_MS
|
|
1206
|
+
}).replace(/\r?\n$/u, "");
|
|
1207
|
+
return uninstallPostRewriteHookAtPath(
|
|
1208
|
+
resolve3(safeGitPath(gitRoot, commonDir), "hooks", POST_REWRITE_MANAGED_HOOK.hookName)
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
function uninstallTarget(target, spec) {
|
|
1212
|
+
const stat = assertSafeFile(target.hookPath, spec);
|
|
1213
|
+
if (!stat) return { path: target.hookPath, changed: false, removedFile: false };
|
|
1214
|
+
const existing = readHookFile(target.hookPath);
|
|
1215
|
+
const range = blockRange(existing, spec);
|
|
1216
|
+
if (range.kind === "absent") {
|
|
1217
|
+
if (spec.hookName === POST_COMMIT_MANAGED_HOOK.hookName && existing.toString("utf8").startsWith(`#!/bin/sh
|
|
1218
|
+
${LEGACY_PRIM_OWNED_HEADER}
|
|
1219
|
+
`)) {
|
|
1220
|
+
const prefixLength = legacyProjectPrefixLength(existing);
|
|
1221
|
+
if (prefixLength === void 0) {
|
|
1222
|
+
throw new Error("unrecognized legacy Prim post-commit invocation");
|
|
1223
|
+
}
|
|
1224
|
+
const tail = existing.subarray(prefixLength);
|
|
1225
|
+
if (tail.length === 0) {
|
|
1226
|
+
unlinkHookUnchanged(target.hookPath, existing, stat, spec);
|
|
1227
|
+
return { path: target.hookPath, changed: true, removedFile: true };
|
|
1228
|
+
}
|
|
1229
|
+
rewriteHookAtomically(
|
|
1230
|
+
target.hookPath,
|
|
1231
|
+
Buffer.concat([Buffer.from("#!/bin/sh\n"), tail]),
|
|
1232
|
+
existing,
|
|
1233
|
+
spec,
|
|
1234
|
+
stat
|
|
1235
|
+
);
|
|
1236
|
+
return { path: target.hookPath, changed: true, removedFile: false };
|
|
1237
|
+
}
|
|
1238
|
+
return { path: target.hookPath, changed: false, removedFile: false };
|
|
1239
|
+
}
|
|
1240
|
+
const shebangEnd = shellInsertionPoint(existing, target.kind === "husky_v9", spec);
|
|
1241
|
+
const suffix = existing.subarray(range.end);
|
|
1242
|
+
const createdSuffix = Buffer.from(`
|
|
1243
|
+
# ${spec.createdMark}
|
|
1244
|
+
`);
|
|
1245
|
+
if (range.start === shebangEnd && suffix.subarray(0, createdSuffix.length).equals(createdSuffix)) {
|
|
1246
|
+
const tail = suffix.subarray(createdSuffix.length);
|
|
1247
|
+
if (tail.length === 0) {
|
|
1248
|
+
unlinkHookUnchanged(target.hookPath, existing, stat, spec);
|
|
1249
|
+
return { path: target.hookPath, changed: true, removedFile: true };
|
|
1250
|
+
}
|
|
1251
|
+
rewriteHookAtomically(
|
|
1252
|
+
target.hookPath,
|
|
1253
|
+
Buffer.concat([existing.subarray(0, shebangEnd), tail]),
|
|
1254
|
+
existing,
|
|
1255
|
+
spec,
|
|
1256
|
+
stat
|
|
1257
|
+
);
|
|
1258
|
+
return { path: target.hookPath, changed: true, removedFile: false };
|
|
1259
|
+
}
|
|
1260
|
+
const createdPrefixes = [
|
|
1261
|
+
Buffer.from(`#!/bin/sh
|
|
1262
|
+
# ${spec.createdMark}
|
|
1263
|
+
|
|
1264
|
+
`),
|
|
1265
|
+
...spec.hookName === POST_COMMIT_MANAGED_HOOK.hookName ? [Buffer.from(`#!/bin/sh
|
|
1266
|
+
# ${LEGACY_PRIM_CREATED_MARK}
|
|
1267
|
+
|
|
1268
|
+
`)] : []
|
|
1269
|
+
];
|
|
1270
|
+
const createdPrefix = createdPrefixes.find(
|
|
1271
|
+
(prefix) => range.start === prefix.length && existing.subarray(0, prefix.length).equals(prefix)
|
|
1272
|
+
);
|
|
1273
|
+
if (createdPrefix) {
|
|
1274
|
+
const tail = suffix.at(0) === 10 ? suffix.subarray(1) : suffix;
|
|
1275
|
+
if (tail.length === 0) {
|
|
1276
|
+
unlinkHookUnchanged(target.hookPath, existing, stat, spec);
|
|
1277
|
+
return { path: target.hookPath, changed: true, removedFile: true };
|
|
1278
|
+
}
|
|
1279
|
+
rewriteHookAtomically(
|
|
1280
|
+
target.hookPath,
|
|
1281
|
+
Buffer.concat([Buffer.from("#!/bin/sh\n"), tail]),
|
|
1282
|
+
existing,
|
|
1283
|
+
spec,
|
|
1284
|
+
stat
|
|
1285
|
+
);
|
|
1286
|
+
return { path: target.hookPath, changed: true, removedFile: false };
|
|
1287
|
+
}
|
|
1288
|
+
const next = Buffer.concat([
|
|
1289
|
+
existing.subarray(0, range.start),
|
|
1290
|
+
range.start === shebangEnd && suffix.at(0) === 10 ? suffix.subarray(1) : suffix
|
|
1291
|
+
]);
|
|
1292
|
+
rewriteHookAtomically(target.hookPath, next, existing, spec, stat);
|
|
1293
|
+
return { path: target.hookPath, changed: true, removedFile: false };
|
|
1294
|
+
}
|
|
1295
|
+
function uninstallPostCommitHookAtPath(hookPath) {
|
|
1296
|
+
const absolute = resolve3(hookPath);
|
|
1297
|
+
return uninstallTarget(
|
|
1298
|
+
{
|
|
1299
|
+
gitRoot: dirname3(dirname3(absolute)),
|
|
1300
|
+
hooksDir: dirname3(absolute),
|
|
1301
|
+
hookPath: absolute,
|
|
1302
|
+
kind: "direct"
|
|
1303
|
+
},
|
|
1304
|
+
POST_COMMIT_MANAGED_HOOK
|
|
1305
|
+
);
|
|
1306
|
+
}
|
|
1307
|
+
function uninstallPostRewriteHookAtPath(hookPath) {
|
|
1308
|
+
const absolute = resolve3(hookPath);
|
|
1309
|
+
return uninstallTarget(
|
|
1310
|
+
{
|
|
1311
|
+
gitRoot: dirname3(dirname3(absolute)),
|
|
1312
|
+
hooksDir: dirname3(absolute),
|
|
1313
|
+
hookPath: absolute,
|
|
1314
|
+
kind: "direct"
|
|
1315
|
+
},
|
|
1316
|
+
POST_REWRITE_MANAGED_HOOK
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// src/lib/repository-binding.ts
|
|
1321
|
+
var InvalidRepositoryBindingResponseError = class extends Error {
|
|
1322
|
+
};
|
|
1323
|
+
async function resolveRepositoryBindingWithClient(root, client, options) {
|
|
1324
|
+
const repositoryFullName = githubRepositoryFullName(root);
|
|
1325
|
+
if (!repositoryFullName) {
|
|
1326
|
+
throw new Error("origin must be a GitHub HTTPS/SSH remote in owner/name form");
|
|
1327
|
+
}
|
|
1328
|
+
let response;
|
|
1329
|
+
try {
|
|
1330
|
+
response = await client.post(
|
|
1331
|
+
"/api/cli/repositories/bind",
|
|
1332
|
+
{ repositoryFullName },
|
|
1333
|
+
options
|
|
1334
|
+
);
|
|
1335
|
+
} catch (error) {
|
|
1336
|
+
if (error instanceof HttpError && error.status === 404) {
|
|
1337
|
+
return { status: "unbound", repositoryFullName };
|
|
1338
|
+
}
|
|
1339
|
+
throw error;
|
|
1340
|
+
}
|
|
1341
|
+
if (!isValidRepoSyncId(response.repoSyncId)) {
|
|
1342
|
+
throw new InvalidRepositoryBindingResponseError(
|
|
1343
|
+
"server returned an invalid repository binding"
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
return { status: "connected", repoSyncId: response.repoSyncId, repositoryFullName };
|
|
1347
|
+
}
|
|
1348
|
+
async function resolveRepositoryBinding(root, options) {
|
|
1349
|
+
return resolveRepositoryBindingWithClient(root, getClient(), options);
|
|
1350
|
+
}
|
|
1351
|
+
function persistRepositoryBinding(root, binding) {
|
|
1352
|
+
if (binding.status === "connected") {
|
|
1353
|
+
setRepoSyncId(root, binding.repoSyncId, binding.repositoryFullName);
|
|
1354
|
+
setRepositoryBindingState(root, "connected");
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
setRepositoryBindingState(root, "unbound");
|
|
1358
|
+
}
|
|
1359
|
+
async function bindRepositoryWithClient(root, client, options) {
|
|
1360
|
+
let binding;
|
|
1361
|
+
try {
|
|
1362
|
+
binding = await resolveRepositoryBindingWithClient(root, client, options);
|
|
1363
|
+
} catch (error) {
|
|
1364
|
+
if (error instanceof InvalidRepositoryBindingResponseError) {
|
|
1365
|
+
try {
|
|
1366
|
+
setRepositoryBindingState(root, "invalid");
|
|
1367
|
+
} catch {
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
throw error;
|
|
1371
|
+
}
|
|
1372
|
+
persistRepositoryBinding(root, binding);
|
|
1373
|
+
return binding;
|
|
1374
|
+
}
|
|
1375
|
+
async function bindRepository(root, options) {
|
|
1376
|
+
return bindRepositoryWithClient(root, getClient(), options);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
export {
|
|
1380
|
+
warmBinCache,
|
|
1381
|
+
postCommitHookBlock,
|
|
1382
|
+
postRewriteHookBlock,
|
|
1383
|
+
ensureEffectivePostCommitHook,
|
|
1384
|
+
ensureEffectivePostRewriteHook,
|
|
1385
|
+
ensurePostCommitHookAtPath,
|
|
1386
|
+
ensurePostRewriteHookAtPath,
|
|
1387
|
+
inspectEffectivePostCommitHook,
|
|
1388
|
+
inspectEffectivePostRewriteHook,
|
|
1389
|
+
uninstallProjectPostCommitHook,
|
|
1390
|
+
uninstallProjectPostRewriteHook,
|
|
1391
|
+
uninstallPostCommitHookAtPath,
|
|
1392
|
+
uninstallPostRewriteHookAtPath,
|
|
1393
|
+
resolveRepositoryBinding,
|
|
1394
|
+
bindRepositoryWithClient,
|
|
1395
|
+
bindRepository,
|
|
1396
|
+
printJson,
|
|
1397
|
+
refreshClaudePlugins,
|
|
1398
|
+
hasUsableCodexGuidance,
|
|
1399
|
+
registerSkillCommands
|
|
1400
|
+
};
|