@primitive.ai/prim 0.1.0-alpha.93 → 0.1.0-alpha.96
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 +57 -12
- package/SKILL.md +5 -2
- package/dist/{chunk-UKUAAAXE.js → chunk-27ADT6FQ.js} +2 -2
- package/dist/{chunk-OTHKG76J.js → chunk-35FMRWJH.js} +1 -1
- package/dist/{chunk-DXJQR4QI.js → chunk-4GHANPWQ.js} +2 -2
- package/dist/{chunk-DCJN6LZH.js → chunk-7KABYYWG.js} +747 -430
- package/dist/{chunk-LFFLHSQO.js → chunk-CU7RW7XU.js} +63 -6
- package/dist/{chunk-H4UNYFUC.js → chunk-DHOQGHKQ.js} +184 -1
- package/dist/{chunk-2GP5UFV7.js → chunk-DWRSEC34.js} +5 -3
- package/dist/{chunk-36TWVNBE.js → chunk-FVDBULGZ.js} +9 -11
- package/dist/{chunk-G2VMA2QO.js → chunk-LY6NJ7KL.js} +6 -4
- package/dist/{chunk-ZB4F75SZ.js → chunk-MXVCP33E.js} +5 -1
- package/dist/{chunk-TYAHC25F.js → chunk-QCBB5HFX.js} +1 -1
- package/dist/{chunk-C3VAA5CG.js → chunk-RK4ZS3HG.js} +2 -2
- package/dist/{chunk-ZU563PQI.js → chunk-ROLYLXJS.js} +2 -2
- package/dist/{chunk-OH3EZXYX.js → chunk-UO2U6IKF.js} +1 -1
- package/dist/{chunk-K7D2EYBS.js → chunk-URLLSALP.js} +2 -2
- package/dist/chunk-XEDJ5N3M.js +177 -0
- package/dist/{chunk-X64HTHUF.js → chunk-Y5W4TZM7.js} +202 -10
- package/dist/{chunk-ICM2XCXS.js → chunk-YYE2NLWX.js} +2 -2
- package/dist/{chunk-DSWT6BCC.js → chunk-Z6NPNLOT.js} +825 -665
- package/dist/daemon/server.js +836 -540
- package/dist/hook-runtime/daemon/server.js +836 -540
- package/dist/hook-runtime/hooks/post-commit.js +978 -566
- package/dist/hook-runtime/hooks/post-rewrite.js +836 -540
- package/dist/hook-runtime/hooks/post-tool-use.js +1215 -796
- package/dist/hook-runtime/hooks/pre-tool-use.js +2100 -1741
- package/dist/hook-runtime/hooks/prim-hook.js +1312 -861
- package/dist/hook-runtime/hooks/session-end.js +232 -134
- package/dist/hook-runtime/hooks/session-start.js +1799 -935
- package/dist/hook-runtime/index.js +3628 -2461
- package/dist/hooks/post-commit.js +14 -10
- package/dist/hooks/post-rewrite.js +10 -10
- package/dist/hooks/post-tool-use.js +20 -20
- package/dist/hooks/pre-commit.js +4 -4
- package/dist/hooks/pre-tool-use.js +29 -17
- package/dist/hooks/prim-hook.js +22 -18
- package/dist/hooks/session-end.js +11 -5
- package/dist/hooks/session-start.js +46 -28
- package/dist/index.js +623 -390
- package/package.json +1 -1
|
@@ -1,62 +1,35 @@
|
|
|
1
1
|
import {
|
|
2
2
|
unattendedEnv
|
|
3
3
|
} from "./chunk-OYHYCK4K.js";
|
|
4
|
-
import {
|
|
5
|
-
isValidRepoSyncId,
|
|
6
|
-
setRepoSyncId,
|
|
7
|
-
setRepositoryBindingState
|
|
8
|
-
} from "./chunk-DSWT6BCC.js";
|
|
9
4
|
import {
|
|
10
5
|
HttpError,
|
|
11
6
|
getClient
|
|
12
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-LY6NJ7KL.js";
|
|
13
8
|
import {
|
|
9
|
+
GIT_HOOK_CONTRACT_VERSION,
|
|
14
10
|
binFile,
|
|
11
|
+
blockMarkers,
|
|
12
|
+
canonicalHookBlock,
|
|
15
13
|
compareSemver,
|
|
16
14
|
cursorConfigDirectory,
|
|
15
|
+
hookBlockContractVersion,
|
|
16
|
+
inspectGitHookEntrypoint,
|
|
17
|
+
managedHookBlock,
|
|
18
|
+
pinnedHookCommand,
|
|
17
19
|
pinnedNpxCommand
|
|
18
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-Y5W4TZM7.js";
|
|
19
21
|
import {
|
|
20
22
|
gitToplevel,
|
|
21
|
-
githubRepositoryFullName
|
|
22
|
-
|
|
23
|
+
githubRepositoryFullName,
|
|
24
|
+
isValidRepoSyncId,
|
|
25
|
+
setRepoSyncId,
|
|
26
|
+
setRepositoryBindingState
|
|
27
|
+
} from "./chunk-DHOQGHKQ.js";
|
|
23
28
|
import {
|
|
24
29
|
atomicWriteFile,
|
|
30
|
+
primConfigDirectory,
|
|
25
31
|
withFileLock
|
|
26
|
-
} from "./chunk-
|
|
27
|
-
|
|
28
|
-
// src/lib/bin-cache.ts
|
|
29
|
-
import { chmodSync, mkdirSync } from "fs";
|
|
30
|
-
import { homedir } from "os";
|
|
31
|
-
import { join } from "path";
|
|
32
|
-
var GIT_HOOK_CACHE_SHELL_DIR = "${XDG_CACHE_HOME:-$HOME/.cache}/prim/bin";
|
|
33
|
-
var GIT_HOOK_CACHE_TTL_MINUTES = 1440;
|
|
34
|
-
var GIT_HOOK_CACHED_BINS = ["prim-post-commit", "prim-post-rewrite"];
|
|
35
|
-
function binCacheDir() {
|
|
36
|
-
const base = process.env.XDG_CACHE_HOME || join(homedir(), ".cache");
|
|
37
|
-
return join(base, "prim", "bin");
|
|
38
|
-
}
|
|
39
|
-
function writeAtomic(path, content) {
|
|
40
|
-
atomicWriteFile(path, content, { mode: 384 });
|
|
41
|
-
}
|
|
42
|
-
function warmBinCache() {
|
|
43
|
-
try {
|
|
44
|
-
if (process.env.PRIM_BIN_CACHE === "0") {
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
const dir = binCacheDir();
|
|
48
|
-
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
49
|
-
chmodSync(dir, 448);
|
|
50
|
-
writeAtomic(join(dir, "node"), process.execPath);
|
|
51
|
-
for (const bin of GIT_HOOK_CACHED_BINS) {
|
|
52
|
-
const file = binFile(bin);
|
|
53
|
-
if (file) {
|
|
54
|
-
writeAtomic(join(dir, bin), file);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
} catch {
|
|
58
|
-
}
|
|
59
|
-
}
|
|
32
|
+
} from "./chunk-DWRSEC34.js";
|
|
60
33
|
|
|
61
34
|
// src/commands/skill.ts
|
|
62
35
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -73,8 +46,8 @@ import {
|
|
|
73
46
|
rmdirSync as rmdirSync2,
|
|
74
47
|
unlinkSync
|
|
75
48
|
} from "fs";
|
|
76
|
-
import { homedir as
|
|
77
|
-
import { dirname as dirname2, join as
|
|
49
|
+
import { homedir as homedir2 } from "os";
|
|
50
|
+
import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
78
51
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
79
52
|
import { createPatch } from "diff";
|
|
80
53
|
import { parse as parseYaml2 } from "yaml";
|
|
@@ -92,7 +65,7 @@ import {
|
|
|
92
65
|
existsSync,
|
|
93
66
|
fstatSync,
|
|
94
67
|
lstatSync,
|
|
95
|
-
mkdirSync
|
|
68
|
+
mkdirSync,
|
|
96
69
|
openSync,
|
|
97
70
|
readFileSync,
|
|
98
71
|
readSync,
|
|
@@ -101,8 +74,8 @@ import {
|
|
|
101
74
|
rmSync,
|
|
102
75
|
rmdirSync
|
|
103
76
|
} from "fs";
|
|
104
|
-
import { homedir
|
|
105
|
-
import { dirname, isAbsolute, join
|
|
77
|
+
import { homedir } from "os";
|
|
78
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
|
|
106
79
|
import { fileURLToPath } from "url";
|
|
107
80
|
import { parse as parseYaml } from "yaml";
|
|
108
81
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -112,8 +85,8 @@ var MAX_SKILL_BYTES = 1024 * 1024;
|
|
|
112
85
|
var REFRESH_LOCK_TIMEOUT_MS = 150;
|
|
113
86
|
var REFRESH_LOCK_POLL_MS = 10;
|
|
114
87
|
function pluginDirFor(cwd, scope) {
|
|
115
|
-
const base = scope === "user" ?
|
|
116
|
-
return
|
|
88
|
+
const base = scope === "user" ? join(homedir(), ".claude") : join(gitToplevel(cwd) ?? cwd, ".claude");
|
|
89
|
+
return join(base, "skills", "prim");
|
|
117
90
|
}
|
|
118
91
|
function resolvePluginDir(cwd, scope) {
|
|
119
92
|
if (scope && scope !== "user" && scope !== "project") {
|
|
@@ -145,8 +118,8 @@ function removeDirIfEmpty(dir) {
|
|
|
145
118
|
}
|
|
146
119
|
function pluginPaths(dir) {
|
|
147
120
|
return {
|
|
148
|
-
manifestPath:
|
|
149
|
-
skillPath:
|
|
121
|
+
manifestPath: join(dir, ".claude-plugin", "plugin.json"),
|
|
122
|
+
skillPath: join(dir, "SKILL.md")
|
|
150
123
|
};
|
|
151
124
|
}
|
|
152
125
|
function pathIsInside(root, path) {
|
|
@@ -211,9 +184,9 @@ function inspectRecognizedInstallation(paths, projectRoot) {
|
|
|
211
184
|
};
|
|
212
185
|
}
|
|
213
186
|
function refreshLockDir(physicalPluginDir) {
|
|
214
|
-
const cacheRoot = process.env.XDG_CACHE_HOME ||
|
|
187
|
+
const cacheRoot = process.env.XDG_CACHE_HOME || join(homedir(), ".cache");
|
|
215
188
|
const identity = createHash("sha256").update(physicalPluginDir).digest("hex");
|
|
216
|
-
return
|
|
189
|
+
return join(cacheRoot, "prim", "skill-refresh-locks", `${identity}.lock`);
|
|
217
190
|
}
|
|
218
191
|
async function refreshClaudePlugins(cwd, options = {}) {
|
|
219
192
|
const result = { installed: 0, refreshed: 0 };
|
|
@@ -221,7 +194,7 @@ async function refreshClaudePlugins(cwd, options = {}) {
|
|
|
221
194
|
if (options.includeProject !== false) {
|
|
222
195
|
const projectRoot = options.projectRoot ?? gitToplevel(cwd);
|
|
223
196
|
if (projectRoot !== null && isAbsolute(projectRoot)) {
|
|
224
|
-
candidates.push({ dir:
|
|
197
|
+
candidates.push({ dir: join(projectRoot, ".claude", "skills", "prim"), projectRoot });
|
|
225
198
|
}
|
|
226
199
|
}
|
|
227
200
|
candidates.push({ dir: pluginDirFor(cwd, "user") });
|
|
@@ -297,7 +270,7 @@ function installClaudePlugin(cwd, opts) {
|
|
|
297
270
|
console.log(`Would write plugin to ${dir} (.claude-plugin/plugin.json + SKILL.md)`);
|
|
298
271
|
return 0;
|
|
299
272
|
}
|
|
300
|
-
|
|
273
|
+
mkdirSync(join(dir, ".claude-plugin"), { recursive: true });
|
|
301
274
|
atomicWrite(manifestPath, manifest);
|
|
302
275
|
atomicWrite(skillPath, skill);
|
|
303
276
|
console.log(`Installed prim skill plugin at ${dir}`);
|
|
@@ -317,7 +290,7 @@ function uninstallClaudePlugin(cwd, opts) {
|
|
|
317
290
|
}
|
|
318
291
|
rmSync(manifestPath, { force: true });
|
|
319
292
|
rmSync(skillPath, { force: true });
|
|
320
|
-
removeDirIfEmpty(
|
|
293
|
+
removeDirIfEmpty(join(dir, ".claude-plugin"));
|
|
321
294
|
removeDirIfEmpty(dir);
|
|
322
295
|
console.log(`Removed prim skill plugin from ${dir}`);
|
|
323
296
|
return 0;
|
|
@@ -372,10 +345,10 @@ function readCodexGuidance(path) {
|
|
|
372
345
|
}
|
|
373
346
|
}
|
|
374
347
|
function effectiveCodexGuidance(scope) {
|
|
375
|
-
const override = readCodexGuidance(
|
|
348
|
+
const override = readCodexGuidance(join2(scope, "AGENTS.override.md"));
|
|
376
349
|
if (override === null) return null;
|
|
377
350
|
if (override !== void 0 && override.toString("utf8").trim().length > 0) return override;
|
|
378
|
-
const base = readCodexGuidance(
|
|
351
|
+
const base = readCodexGuidance(join2(scope, "AGENTS.md"));
|
|
379
352
|
return base ?? null;
|
|
380
353
|
}
|
|
381
354
|
function hasUsablePrimBlock(content, visibleBytes) {
|
|
@@ -398,7 +371,7 @@ function hasUsablePrimBlock(content, visibleBytes) {
|
|
|
398
371
|
}
|
|
399
372
|
function hasUsableCodexGuidance(projectRoot) {
|
|
400
373
|
try {
|
|
401
|
-
const codexHome = process.env.CODEX_HOME?.trim() ? resolve2(process.env.CODEX_HOME) :
|
|
374
|
+
const codexHome = process.env.CODEX_HOME?.trim() ? resolve2(process.env.CODEX_HOME) : join2(homedir2(), ".codex");
|
|
402
375
|
const scopes = [
|
|
403
376
|
{ path: codexHome, visibleBytes: CODEX_GUIDANCE_LIMIT_BYTES },
|
|
404
377
|
{ path: projectRoot, visibleBytes: CODEX_DEFAULT_PROJECT_GUIDANCE_BYTES }
|
|
@@ -432,14 +405,14 @@ function sha256(content) {
|
|
|
432
405
|
}
|
|
433
406
|
function cursorSkillDirectory(cwd, scope) {
|
|
434
407
|
const base = scope === "user" ? cursorConfigDirectory() : gitToplevel(cwd) ?? cwd;
|
|
435
|
-
return
|
|
408
|
+
return join2(base, scope === "user" ? "skills" : ".cursor/skills", "prim");
|
|
436
409
|
}
|
|
437
410
|
function cursorSkillPaths(cwd, scope) {
|
|
438
411
|
const directory = cursorSkillDirectory(cwd, scope);
|
|
439
412
|
return {
|
|
440
413
|
directory,
|
|
441
|
-
skill:
|
|
442
|
-
manifest:
|
|
414
|
+
skill: join2(directory, "SKILL.md"),
|
|
415
|
+
manifest: join2(directory, CURSOR_SKILL_MANIFEST)
|
|
443
416
|
};
|
|
444
417
|
}
|
|
445
418
|
function readCursorSkillManifest(path) {
|
|
@@ -527,10 +500,10 @@ function hasUsableCursorSkill(projectRoot) {
|
|
|
527
500
|
return cursorSkillStatus(projectRoot, "project").installed || cursorSkillStatus(projectRoot, "user").installed;
|
|
528
501
|
}
|
|
529
502
|
function userTargetFor(agent) {
|
|
530
|
-
if (agent === "claude") return
|
|
531
|
-
if (agent === "codex") return
|
|
503
|
+
if (agent === "claude") return join2(homedir2(), ".claude", "CLAUDE.md");
|
|
504
|
+
if (agent === "codex") return join2(homedir2(), ".codex", "AGENTS.md");
|
|
532
505
|
if (agent === "hermes") {
|
|
533
|
-
return
|
|
506
|
+
return join2(process.env.HERMES_HOME ?? join2(homedir2(), ".hermes"), ".hermes.md");
|
|
534
507
|
}
|
|
535
508
|
return null;
|
|
536
509
|
}
|
|
@@ -560,8 +533,8 @@ function applyBlock(existing, block, eol) {
|
|
|
560
533
|
return existing.slice(0, b) + block + existing.slice(e + SKILL_END.length);
|
|
561
534
|
}
|
|
562
535
|
if (existing.length === 0) return `${block}${eol}`;
|
|
563
|
-
const
|
|
564
|
-
return `${existing}${
|
|
536
|
+
const sep3 = existing.endsWith(eol) ? "" : eol;
|
|
537
|
+
return `${existing}${sep3}${block}${eol}`;
|
|
565
538
|
}
|
|
566
539
|
function removeBlock(existing) {
|
|
567
540
|
const b = existing.indexOf(SKILL_BEGIN);
|
|
@@ -701,60 +674,61 @@ function registerSkillCommands(program) {
|
|
|
701
674
|
});
|
|
702
675
|
}
|
|
703
676
|
|
|
704
|
-
// src/lib/
|
|
705
|
-
import {
|
|
706
|
-
import {
|
|
707
|
-
import {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
var
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
"
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
677
|
+
// src/lib/bin-cache.ts
|
|
678
|
+
import { chmodSync, mkdirSync as mkdirSync2 } from "fs";
|
|
679
|
+
import { homedir as homedir3 } from "os";
|
|
680
|
+
import { join as join3 } from "path";
|
|
681
|
+
var GIT_HOOK_CACHE_SHELL_DIR = "${XDG_CACHE_HOME:-$HOME/.cache}/prim/bin";
|
|
682
|
+
var GIT_HOOK_CACHE_TTL_MINUTES = 1440;
|
|
683
|
+
var GIT_HOOK_CACHED_BINS = ["prim-post-commit", "prim-post-rewrite"];
|
|
684
|
+
function binCacheDir() {
|
|
685
|
+
const base = process.env.XDG_CACHE_HOME || join3(homedir3(), ".cache");
|
|
686
|
+
return join3(base, "prim", "bin");
|
|
687
|
+
}
|
|
688
|
+
function writeAtomic(path, content) {
|
|
689
|
+
atomicWriteFile(path, content, { mode: 384 });
|
|
690
|
+
}
|
|
691
|
+
function warmBinCache() {
|
|
692
|
+
try {
|
|
693
|
+
if (process.env.PRIM_BIN_CACHE === "0") {
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
const dir = binCacheDir();
|
|
697
|
+
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
698
|
+
chmodSync(dir, 448);
|
|
699
|
+
writeAtomic(join3(dir, "node"), process.execPath);
|
|
700
|
+
for (const bin of GIT_HOOK_CACHED_BINS) {
|
|
701
|
+
const file = binFile(bin);
|
|
702
|
+
if (file) {
|
|
703
|
+
writeAtomic(join3(dir, bin), file);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
} catch {
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/lib/git-hook-legacy.ts
|
|
711
|
+
import { isAbsolute as isAbsolute2 } from "path";
|
|
712
|
+
var PRIM_MANAGED_MARK = "prim-managed-hook";
|
|
713
|
+
var PRE_COMMIT = { hookName: "pre-commit", binName: "prim-pre-commit" };
|
|
714
|
+
var POST_COMMIT = { hookName: "post-commit", binName: "prim-post-commit" };
|
|
715
|
+
var POST_REWRITE = { hookName: "post-rewrite", binName: "prim-post-rewrite" };
|
|
716
|
+
function blockMarkers2(spec) {
|
|
717
|
+
return {
|
|
718
|
+
start: `# >>> prim ${spec.hookName} hook >>>`,
|
|
719
|
+
end: `# <<< prim ${spec.hookName} hook <<<`
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
function hookShim(binName) {
|
|
723
|
+
return `{ ${pinnedHookCommand(binName)}; } || true`;
|
|
724
|
+
}
|
|
725
|
+
function gatedShim(binName) {
|
|
726
|
+
return `if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
|
|
727
|
+
${hookShim(binName)}
|
|
728
|
+
fi`;
|
|
729
|
+
}
|
|
730
|
+
function legacyPostCommitBlock() {
|
|
731
|
+
return `# >>> prim post-commit hook >>>
|
|
758
732
|
if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
|
|
759
733
|
prim_commit_sha=$(git rev-parse --verify HEAD 2>/dev/null) || prim_commit_sha=
|
|
760
734
|
case "$prim_commit_sha" in *[!0-9a-f]*|"") prim_commit_sha= ;; esac
|
|
@@ -783,13 +757,10 @@ if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
|
|
|
783
757
|
fi
|
|
784
758
|
unset prim_commit_sha
|
|
785
759
|
fi
|
|
786
|
-
|
|
760
|
+
# <<< prim post-commit hook <<<`;
|
|
787
761
|
}
|
|
788
|
-
function
|
|
789
|
-
return
|
|
790
|
-
}
|
|
791
|
-
function postRewriteHookBlock() {
|
|
792
|
-
return `${PRIM_POST_REWRITE_BLOCK_START}
|
|
762
|
+
function legacyPostRewriteBlock() {
|
|
763
|
+
return `# >>> prim post-rewrite hook >>>
|
|
793
764
|
if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
|
|
794
765
|
case "$1" in amend|rebase) prim_rewrite_source="$1" ;; *) prim_rewrite_source= ;; esac
|
|
795
766
|
if [ -n "$prim_rewrite_source" ]; then
|
|
@@ -828,22 +799,227 @@ if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
|
|
|
828
799
|
fi
|
|
829
800
|
unset prim_rewrite_source
|
|
830
801
|
fi
|
|
831
|
-
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
hookName: "
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
802
|
+
# <<< prim post-rewrite hook <<<`;
|
|
803
|
+
}
|
|
804
|
+
function legacyGlobalHookScript(spec) {
|
|
805
|
+
const chainExit = spec.hookName === PRE_COMMIT.hookName ? "|| exit $?" : "|| true";
|
|
806
|
+
const managedBlock = spec.hookName === POST_COMMIT.hookName ? legacyPostCommitBlock() : spec.hookName === POST_REWRITE.hookName ? legacyPostRewriteBlock() : void 0;
|
|
807
|
+
const invocation = managedBlock ?? gatedShim(spec.binName);
|
|
808
|
+
const managedRepoGuard = managedBlock ? ` && ! grep -Fq '${blockMarkers2(spec).start.slice(0, -3)}'">>>" "$repo_hook" 2>/dev/null` : "";
|
|
809
|
+
const beforeComments = managedBlock ? `${invocation}
|
|
810
|
+
` : "";
|
|
811
|
+
const afterComments = managedBlock ? "" : `${invocation}
|
|
812
|
+
`;
|
|
813
|
+
return `#!/bin/sh
|
|
814
|
+
${beforeComments}# prim global ${spec.hookName} hook (core.hooksPath) \u2014 managed by prim; do not edit.
|
|
815
|
+
# Install/uninstall: prim hooks install|uninstall --scope user
|
|
816
|
+
# Runs prim only where activated \u2014 'prim enable' (this repo) or
|
|
817
|
+
# 'git config --global prim.active true' (every repo). Chains to the repo's own
|
|
818
|
+
# hook regardless, so inactive repos are unaffected.
|
|
819
|
+
${afterComments}common_dir=$(git rev-parse --git-common-dir 2>/dev/null) || exit 0
|
|
820
|
+
repo_hook="$common_dir/hooks/${spec.hookName}"
|
|
821
|
+
if [ -x "$repo_hook" ] && ! grep -q '${PRIM_MANAGED_MARK}' "$repo_hook" 2>/dev/null${managedRepoGuard}; then
|
|
822
|
+
"$repo_hook" "$@" ${chainExit}
|
|
823
|
+
fi
|
|
824
|
+
exit 0
|
|
825
|
+
`;
|
|
826
|
+
}
|
|
827
|
+
var PRE_COMMIT_GATE = 'if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then';
|
|
828
|
+
function preCommitLadder(fallback, failSoft) {
|
|
829
|
+
const soft = failSoft ? " || true" : "";
|
|
830
|
+
return `if command -v prim-pre-commit >/dev/null 2>&1; then
|
|
831
|
+
prim-pre-commit${soft}
|
|
832
|
+
elif [ -f "./node_modules/.bin/prim-pre-commit" ]; then
|
|
833
|
+
./node_modules/.bin/prim-pre-commit${soft}
|
|
834
|
+
else
|
|
835
|
+
${fallback} 2>/dev/null || true
|
|
836
|
+
fi`;
|
|
837
|
+
}
|
|
838
|
+
var LADDER_ALPHA_2 = preCommitLadder("npx --yes @primitive.ai/prim pre-commit-hook", false);
|
|
839
|
+
var LADDER_ALPHA_9 = preCommitLadder("npx --yes -p @primitive.ai/prim prim-pre-commit", false);
|
|
840
|
+
var LADDER_ALPHA_35 = preCommitLadder("npx --yes -p @primitive.ai/prim prim-pre-commit", true);
|
|
841
|
+
var PINNED_NPX_PRE_COMMIT = String.raw`npx --yes(?: --ignore-scripts)? -p @primitive\.ai/prim@[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)? prim-pre-commit`;
|
|
842
|
+
var PINNED_PRE_COMMIT_RE = new RegExp(
|
|
843
|
+
String.raw`^\{ (?:${PINNED_NPX_PRE_COMMIT}|if \[ -x (?<node>'.*') \] && \[ -f (?<entry>'.*') \]; then \k<node> \k<entry>; else ${PINNED_NPX_PRE_COMMIT}; fi); \} \|\| true$`,
|
|
844
|
+
"u"
|
|
845
|
+
);
|
|
846
|
+
var SHELL_QUOTED_RE = /^'(?:[^']|'"'"')*'$/u;
|
|
847
|
+
function decodePinnedPath(token) {
|
|
848
|
+
if (!token || !SHELL_QUOTED_RE.test(token)) return void 0;
|
|
849
|
+
const value = token.slice(1, -1).replaceAll(`'"'"'`, "'");
|
|
850
|
+
if (!isAbsolute2(value) || value.includes("\0") || value.includes("\n") || value.includes("\r")) {
|
|
851
|
+
return void 0;
|
|
852
|
+
}
|
|
853
|
+
return value;
|
|
854
|
+
}
|
|
855
|
+
function isPinnedPreCommitInvocation(line) {
|
|
856
|
+
const match = PINNED_PRE_COMMIT_RE.exec(line);
|
|
857
|
+
if (!match) return false;
|
|
858
|
+
if (match.groups?.node === void 0) return true;
|
|
859
|
+
return Boolean(
|
|
860
|
+
decodePinnedPath(match.groups.node) && decodePinnedPath(match.groups.entry)?.endsWith("/dist/hooks/pre-commit.js")
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
function isLegacyPreCommitBody(body) {
|
|
864
|
+
if (body === LADDER_ALPHA_2 || body === LADDER_ALPHA_9 || body === LADDER_ALPHA_35) return true;
|
|
865
|
+
if (isPinnedPreCommitInvocation(body)) return true;
|
|
866
|
+
const prefix = `${PRE_COMMIT_GATE}
|
|
867
|
+
`;
|
|
868
|
+
if (!body.startsWith(prefix) || !body.endsWith("\nfi")) return false;
|
|
869
|
+
const inner = body.slice(prefix.length, -"\nfi".length);
|
|
870
|
+
return inner === LADDER_ALPHA_35 || isPinnedPreCommitInvocation(inner);
|
|
871
|
+
}
|
|
872
|
+
function isLegacyOwnedPreCommitScript(content) {
|
|
873
|
+
const original = (ladder) => `#!/bin/sh
|
|
874
|
+
# prim pre-commit hook \u2014 auto-syncs affected specs on commit
|
|
875
|
+
# Installed by: prim hooks install
|
|
876
|
+
|
|
877
|
+
# Find the nearest node_modules/.bin with prim, or use npx
|
|
878
|
+
${ladder}
|
|
879
|
+
`;
|
|
880
|
+
if (content === original(LADDER_ALPHA_2) || content === original(LADDER_ALPHA_9)) return true;
|
|
881
|
+
if (content === `#!/bin/sh
|
|
882
|
+
# prim pre-commit hook \u2014 installed by: prim hooks install
|
|
883
|
+
|
|
884
|
+
${LADDER_ALPHA_9}
|
|
885
|
+
`) {
|
|
886
|
+
return true;
|
|
887
|
+
}
|
|
888
|
+
const prefix = `#!/bin/sh
|
|
889
|
+
# prim pre-commit hook \u2014 installed by: prim hooks install (${PRIM_MANAGED_MARK})
|
|
890
|
+
|
|
891
|
+
`;
|
|
892
|
+
if (!content.startsWith(prefix) || !content.endsWith("\n")) return false;
|
|
893
|
+
const body = content.slice(prefix.length, -1);
|
|
894
|
+
return body === LADDER_ALPHA_35 || isPinnedPreCommitInvocation(body);
|
|
895
|
+
}
|
|
896
|
+
var PINNED_PACKAGE_VERSION_RE = /@primitive\.ai\/prim@[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?/gu;
|
|
897
|
+
function normalizeOwnedGlobalHook(content, hookName) {
|
|
898
|
+
if (hookName === PRE_COMMIT.hookName) {
|
|
899
|
+
const lines = content.split("\n");
|
|
900
|
+
const invocationIndexes = lines.flatMap(
|
|
901
|
+
(line, index) => line.startsWith("{ ") && line.endsWith("; } || true") ? [index] : []
|
|
902
|
+
);
|
|
903
|
+
if (invocationIndexes.length !== 1) return null;
|
|
904
|
+
const invocationIndex = invocationIndexes[0];
|
|
905
|
+
if (!isPinnedPreCommitInvocation(lines[invocationIndex])) return null;
|
|
906
|
+
lines[invocationIndex] = "{ <recognized Prim pre-commit invocation>; } || true";
|
|
907
|
+
return lines.join("\n");
|
|
908
|
+
}
|
|
909
|
+
return content.replace(PINNED_PACKAGE_VERSION_RE, "@primitive.ai/prim@<version>");
|
|
910
|
+
}
|
|
911
|
+
function isLegacyOwnedGlobalHook(content, hookName) {
|
|
912
|
+
const spec = [PRE_COMMIT, POST_COMMIT, POST_REWRITE].find((s) => s.hookName === hookName);
|
|
913
|
+
if (!spec) return false;
|
|
914
|
+
const expected = legacyGlobalHookScript(spec);
|
|
915
|
+
if (content === expected) return true;
|
|
916
|
+
if (spec === PRE_COMMIT && content === expected.replace(gatedShim(spec.binName), `${PRE_COMMIT_GATE}
|
|
917
|
+
${LADDER_ALPHA_35}
|
|
918
|
+
fi`)) {
|
|
919
|
+
return true;
|
|
920
|
+
}
|
|
921
|
+
const normalized = normalizeOwnedGlobalHook(content, hookName);
|
|
922
|
+
return normalized !== null && normalized === normalizeOwnedGlobalHook(expected, hookName);
|
|
923
|
+
}
|
|
924
|
+
function legacyInlineHookBlock(hookName) {
|
|
925
|
+
return hookName === "post-commit" ? legacyPostCommitBlock() : legacyPostRewriteBlock();
|
|
926
|
+
}
|
|
927
|
+
function isRecognizedLegacyBlock(hookName, block) {
|
|
928
|
+
if (hookName === "pre-commit") {
|
|
929
|
+
const { start, end } = blockMarkers2(PRE_COMMIT);
|
|
930
|
+
const prefix = `${start}
|
|
931
|
+
`;
|
|
932
|
+
const suffix = `
|
|
933
|
+
${end}`;
|
|
934
|
+
if (!block.startsWith(prefix) || !block.endsWith(suffix)) return false;
|
|
935
|
+
return isLegacyPreCommitBody(block.slice(prefix.length, -suffix.length));
|
|
936
|
+
}
|
|
937
|
+
const expected = legacyInlineHookBlock(hookName);
|
|
938
|
+
const normalize = (text) => text.replace(PINNED_PACKAGE_VERSION_RE, "@primitive.ai/prim@<version>");
|
|
939
|
+
return normalize(block) === normalize(expected);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// src/lib/git-hooks.ts
|
|
943
|
+
import { execFileSync } from "child_process";
|
|
944
|
+
import { createHash as createHash3, randomUUID } from "crypto";
|
|
945
|
+
import {
|
|
946
|
+
constants as constants3,
|
|
947
|
+
closeSync as closeSync3,
|
|
948
|
+
existsSync as existsSync3,
|
|
949
|
+
fchmodSync,
|
|
950
|
+
fsyncSync,
|
|
951
|
+
lstatSync as lstatSync3,
|
|
952
|
+
mkdirSync as mkdirSync3,
|
|
953
|
+
openSync as openSync3,
|
|
954
|
+
readFileSync as readFileSync3,
|
|
955
|
+
realpathSync as realpathSync2,
|
|
956
|
+
renameSync,
|
|
957
|
+
unlinkSync as unlinkSync2,
|
|
958
|
+
writeFileSync
|
|
959
|
+
} from "fs";
|
|
960
|
+
import { basename, dirname as dirname3, isAbsolute as isAbsolute3, join as join4, relative as relative2, resolve as resolve3, sep as sep2 } from "path";
|
|
961
|
+
var LEGACY_PRIM_CREATED_MARK = "prim-created-hook";
|
|
962
|
+
var LEGACY_PRIM_OWNED_HEADER = "# prim post-commit hook \u2014 installed by: prim hooks install (prim-managed-hook)";
|
|
963
|
+
var LEGACY_GLOBAL_OWNED_HEADER = "# prim global post-commit hook (core.hooksPath) \u2014 managed by prim; do not edit.";
|
|
964
|
+
var MAX_HOOK_BYTES = 1048576;
|
|
965
|
+
var GIT_TIMEOUT_MS = 1e3;
|
|
966
|
+
var SUPPORTED_SHEBANGS = /* @__PURE__ */ new Set([
|
|
967
|
+
"#!/bin/sh",
|
|
968
|
+
"#!/usr/bin/env sh",
|
|
969
|
+
"#!/bin/bash",
|
|
970
|
+
"#!/usr/bin/env bash",
|
|
971
|
+
"#!/bin/zsh",
|
|
972
|
+
"#!/usr/bin/env zsh"
|
|
973
|
+
]);
|
|
974
|
+
var HUSKY_V9_DISPATCHERS = /* @__PURE__ */ new Set([
|
|
975
|
+
'#!/usr/bin/env sh\n. "${0%/*}/h"',
|
|
976
|
+
'#!/usr/bin/env sh\n. "$(dirname "$0")/h"'
|
|
977
|
+
]);
|
|
978
|
+
var HUSKY_DIRECT_DISPATCHER = '#!/bin/sh\nexec sh "$(dirname "$0")/../post-commit"\n';
|
|
979
|
+
var HUSKY_V9_RUNTIME_HASHES = /* @__PURE__ */ new Set([
|
|
980
|
+
"d6aaa6f3f7c11008c525e649161df4a93152f1e55d0887fa1d7b8d622735d380",
|
|
981
|
+
"b02cb91cab7125a9e906be723a1b85297fc435a8307d429ae845546b4ee0ecde",
|
|
982
|
+
"9db5d129c4eb822a773157d98cd8fb8f1b156b85e3193117fefe527ca6d45e91",
|
|
983
|
+
"3b2cb21335e544b4ebce658ca47af4a69d2c77026cc4d3f8b2227c34d062b207",
|
|
984
|
+
"548990e1c11285694096184993d907b258d41235461a7ca2551e3b53ff9d3a38",
|
|
985
|
+
"c029a943acea5d6e2ddbaa271a1fb22c2da55d56b5c6fb59ca804ac9a4018708",
|
|
986
|
+
"ae3413a8fe2b39372de48bdc9691f42055f089c0c7529834b2fb07a131585b6a",
|
|
987
|
+
"70200b200ca709b0622784f93839a5b2872333a917a09afddefd7dc2d8cdc680"
|
|
988
|
+
]);
|
|
989
|
+
function managedHookSpecFor(hookName) {
|
|
990
|
+
const { start, end } = blockMarkers(hookName);
|
|
991
|
+
return {
|
|
992
|
+
hookName,
|
|
993
|
+
blockStart: start,
|
|
994
|
+
blockEnd: end,
|
|
995
|
+
createdMark: `prim-created-${hookName}-hook`
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
var MANAGED_HOOK_SPECS = {
|
|
999
|
+
"pre-commit": managedHookSpecFor("pre-commit"),
|
|
1000
|
+
"post-commit": managedHookSpecFor("post-commit"),
|
|
1001
|
+
"post-rewrite": managedHookSpecFor("post-rewrite")
|
|
846
1002
|
};
|
|
1003
|
+
function managedHookSpec(hookName) {
|
|
1004
|
+
return MANAGED_HOOK_SPECS[hookName];
|
|
1005
|
+
}
|
|
1006
|
+
function gitHooksMode(options = {}) {
|
|
1007
|
+
try {
|
|
1008
|
+
const value = execFileSync(
|
|
1009
|
+
"git",
|
|
1010
|
+
["config", ...options.global ? ["--global"] : [], "--get", "prim.gitHooks"],
|
|
1011
|
+
{
|
|
1012
|
+
cwd: options.cwd,
|
|
1013
|
+
encoding: "utf8",
|
|
1014
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1015
|
+
timeout: GIT_TIMEOUT_MS
|
|
1016
|
+
}
|
|
1017
|
+
).trim();
|
|
1018
|
+
return value === "manual" ? "manual" : "auto";
|
|
1019
|
+
} catch {
|
|
1020
|
+
return "auto";
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
847
1023
|
function legacyFloatingInvocation() {
|
|
848
1024
|
return `if command -v prim-post-commit >/dev/null 2>&1; then
|
|
849
1025
|
prim-post-commit || true
|
|
@@ -860,15 +1036,6 @@ fi
|
|
|
860
1036
|
`);
|
|
861
1037
|
}
|
|
862
1038
|
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;
|
|
863
|
-
var SHELL_QUOTED_RE = /^'(?:[^']|'"'"')*'$/u;
|
|
864
|
-
function decodePinnedPath(token) {
|
|
865
|
-
if (!token || !SHELL_QUOTED_RE.test(token)) return void 0;
|
|
866
|
-
const value = token.slice(1, -1).replaceAll(`'"'"'`, "'");
|
|
867
|
-
if (!isAbsolute2(value) || value.includes("\0") || value.includes("\n") || value.includes("\r")) {
|
|
868
|
-
return void 0;
|
|
869
|
-
}
|
|
870
|
-
return value;
|
|
871
|
-
}
|
|
872
1039
|
function isPinnedInvocation(value) {
|
|
873
1040
|
const match = PINNED_INVOCATION_RE.exec(value);
|
|
874
1041
|
const node = decodePinnedPath(match?.groups?.node);
|
|
@@ -901,48 +1068,149 @@ function isKnownLegacyGlobalGate(value) {
|
|
|
901
1068
|
}
|
|
902
1069
|
function createdScaffold(spec) {
|
|
903
1070
|
return Buffer.from(`#!/bin/sh
|
|
904
|
-
${spec.
|
|
1071
|
+
${managedHookBlock(spec.hookName)}
|
|
905
1072
|
# ${spec.createdMark}
|
|
906
1073
|
`);
|
|
907
1074
|
}
|
|
1075
|
+
function oldCreatedPrefixes(spec) {
|
|
1076
|
+
return [
|
|
1077
|
+
Buffer.from(`#!/bin/sh
|
|
1078
|
+
# ${spec.createdMark}
|
|
1079
|
+
|
|
1080
|
+
`),
|
|
1081
|
+
Buffer.from(`#!/bin/sh
|
|
1082
|
+
# ${LEGACY_PRIM_CREATED_MARK}
|
|
1083
|
+
|
|
1084
|
+
`)
|
|
1085
|
+
];
|
|
1086
|
+
}
|
|
1087
|
+
function isOwnedStandalonePreCommit(content) {
|
|
1088
|
+
return content === "#!/bin/sh\nprim-pre-commit\n" || isLegacyOwnedPreCommitScript(content);
|
|
1089
|
+
}
|
|
1090
|
+
function realpathLoose(path) {
|
|
1091
|
+
const missing = [];
|
|
1092
|
+
let current = resolve3(path);
|
|
1093
|
+
for (; ; ) {
|
|
1094
|
+
try {
|
|
1095
|
+
return join4(realpathSync2.native(current), ...missing);
|
|
1096
|
+
} catch {
|
|
1097
|
+
const parent = dirname3(current);
|
|
1098
|
+
if (parent === current) return resolve3(path);
|
|
1099
|
+
missing.unshift(basename(current));
|
|
1100
|
+
current = parent;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
function isWithin(path, directory) {
|
|
1105
|
+
const rel = relative2(directory, path);
|
|
1106
|
+
return rel === "" || !isAbsolute3(rel) && rel !== ".." && !rel.startsWith(`..${sep2}`);
|
|
1107
|
+
}
|
|
1108
|
+
function primGitHooksDirectory() {
|
|
1109
|
+
try {
|
|
1110
|
+
return join4(primConfigDirectory(), "git-hooks");
|
|
1111
|
+
} catch {
|
|
1112
|
+
return void 0;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
function otherWorktreeRoots(gitRoot) {
|
|
1116
|
+
try {
|
|
1117
|
+
const self = realpathLoose(gitRoot);
|
|
1118
|
+
return execFileSync("git", ["worktree", "list", "--porcelain"], {
|
|
1119
|
+
cwd: gitRoot,
|
|
1120
|
+
encoding: "utf8",
|
|
1121
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1122
|
+
timeout: GIT_TIMEOUT_MS
|
|
1123
|
+
}).split("\n").flatMap((line) => line.startsWith("worktree ") ? [realpathLoose(line.slice(9))] : []).filter((root) => root !== self);
|
|
1124
|
+
} catch {
|
|
1125
|
+
return [];
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
function hookLocation(hookPath, dirs) {
|
|
1129
|
+
const path = realpathLoose(hookPath);
|
|
1130
|
+
if (isWithin(path, realpathLoose(dirs.gitDir)) || isWithin(path, realpathLoose(dirs.commonDir))) {
|
|
1131
|
+
return "repository";
|
|
1132
|
+
}
|
|
1133
|
+
const primDir = primGitHooksDirectory();
|
|
1134
|
+
if (primDir !== void 0 && isWithin(path, realpathLoose(primDir))) return "prim";
|
|
1135
|
+
const root = realpathLoose(dirs.gitRoot);
|
|
1136
|
+
if (!isWithin(path, root)) return "external";
|
|
1137
|
+
const nested = otherWorktreeRoots(dirs.gitRoot).filter((other) => isWithin(other, root));
|
|
1138
|
+
return nested.some((other) => isWithin(path, other)) ? "external" : "worktree";
|
|
1139
|
+
}
|
|
1140
|
+
function hooksPathScope(cwd) {
|
|
1141
|
+
try {
|
|
1142
|
+
const scope = execFileSync("git", ["config", "--show-scope", "--get", "core.hooksPath"], {
|
|
1143
|
+
cwd,
|
|
1144
|
+
encoding: "utf8",
|
|
1145
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1146
|
+
timeout: GIT_TIMEOUT_MS
|
|
1147
|
+
}).split(" ")[0];
|
|
1148
|
+
return scope === "local" || scope === "worktree" || scope === "global" || scope === "system" || scope === "command" ? scope : void 0;
|
|
1149
|
+
} catch {
|
|
1150
|
+
return void 0;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
function externalHookRemedy(hookName, cwd) {
|
|
1154
|
+
const snippet = `place \`prim hooks snippet ${hookName}\` there yourself`;
|
|
1155
|
+
switch (hooksPathScope(cwd)) {
|
|
1156
|
+
case "global":
|
|
1157
|
+
return `your global core.hooksPath runs every repository's hooks: ${snippet}, or have prim add it with \`prim hooks install --scope user --global-hooks-path\` (it changes every repository's hooks: an agent must ask the user first)`;
|
|
1158
|
+
case "system":
|
|
1159
|
+
return `the system core.hooksPath is shared by every user on this machine: ${snippet}, or ask its owner`;
|
|
1160
|
+
case "local":
|
|
1161
|
+
case "worktree":
|
|
1162
|
+
return `this repository's core.hooksPath points outside it: ${snippet}, or point core.hooksPath inside the repository`;
|
|
1163
|
+
default:
|
|
1164
|
+
return snippet;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
908
1167
|
function safeGitPath(root, value) {
|
|
909
1168
|
if (value.length === 0 || value !== value.trim() || value.includes("\0") || value.includes("\n") || value.includes("\r")) {
|
|
910
1169
|
throw new Error("Git returned an unsafe hooks path");
|
|
911
1170
|
}
|
|
912
|
-
return
|
|
1171
|
+
return isAbsolute3(value) ? resolve3(value) : resolve3(root, value);
|
|
913
1172
|
}
|
|
914
|
-
function
|
|
1173
|
+
function resolveEffectiveGitHook(hookName, cwd = process.cwd()) {
|
|
915
1174
|
const gitRoot = gitToplevel(cwd);
|
|
916
1175
|
if (!gitRoot) throw new Error("not a git repository");
|
|
917
|
-
const
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1176
|
+
const lines = execFileSync(
|
|
1177
|
+
"git",
|
|
1178
|
+
["rev-parse", "--git-path", "hooks", "--absolute-git-dir", "--git-common-dir"],
|
|
1179
|
+
{
|
|
1180
|
+
cwd: gitRoot,
|
|
1181
|
+
encoding: "utf8",
|
|
1182
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1183
|
+
timeout: GIT_TIMEOUT_MS
|
|
1184
|
+
}
|
|
1185
|
+
).replace(/\r?\n$/u, "").split(/\r?\n/u);
|
|
1186
|
+
if (lines.length !== 3) throw new Error("Git returned unexpected repository paths");
|
|
1187
|
+
const [hooksValue, gitDirValue, commonDirValue] = lines;
|
|
1188
|
+
const hooksDir = safeGitPath(gitRoot, hooksValue);
|
|
1189
|
+
const dirs = {
|
|
1190
|
+
gitRoot,
|
|
1191
|
+
gitDir: safeGitPath(gitRoot, gitDirValue),
|
|
1192
|
+
commonDir: safeGitPath(gitRoot, commonDirValue)
|
|
1193
|
+
};
|
|
924
1194
|
if (basename(hooksDir) === "_" && basename(dirname3(hooksDir)) === ".husky") {
|
|
1195
|
+
const hookPath2 = resolve3(dirname3(hooksDir), hookName);
|
|
925
1196
|
return {
|
|
926
1197
|
gitRoot,
|
|
927
1198
|
hooksDir,
|
|
928
|
-
hookPath:
|
|
1199
|
+
hookPath: hookPath2,
|
|
929
1200
|
kind: "husky_v9",
|
|
930
|
-
dispatcherPath: resolve3(hooksDir,
|
|
1201
|
+
dispatcherPath: resolve3(hooksDir, hookName),
|
|
1202
|
+
location: hookLocation(hookPath2, dirs)
|
|
931
1203
|
};
|
|
932
1204
|
}
|
|
1205
|
+
const hookPath = resolve3(hooksDir, hookName);
|
|
933
1206
|
return {
|
|
934
1207
|
gitRoot,
|
|
935
1208
|
hooksDir,
|
|
936
|
-
hookPath
|
|
937
|
-
kind: "direct"
|
|
1209
|
+
hookPath,
|
|
1210
|
+
kind: "direct",
|
|
1211
|
+
location: hookLocation(hookPath, dirs)
|
|
938
1212
|
};
|
|
939
1213
|
}
|
|
940
|
-
function resolveEffectivePostCommitHook(cwd = process.cwd()) {
|
|
941
|
-
return resolveEffectiveManagedHook(POST_COMMIT_MANAGED_HOOK, cwd);
|
|
942
|
-
}
|
|
943
|
-
function resolveEffectivePostRewriteHook(cwd = process.cwd()) {
|
|
944
|
-
return resolveEffectiveManagedHook(POST_REWRITE_MANAGED_HOOK, cwd);
|
|
945
|
-
}
|
|
946
1214
|
function assertSafeFile(path, spec) {
|
|
947
1215
|
if (!existsSync3(path)) return void 0;
|
|
948
1216
|
const stat = lstatSync3(path);
|
|
@@ -1024,16 +1292,107 @@ function blockRange(content, spec) {
|
|
|
1024
1292
|
}
|
|
1025
1293
|
const start = starts[0];
|
|
1026
1294
|
const end = ends[0] + endMarker.length;
|
|
1027
|
-
const
|
|
1028
|
-
|
|
1295
|
+
const text = content.subarray(start, end).toString("utf8");
|
|
1296
|
+
if (canonicalHookBlock(text) === canonicalHookBlock(managedHookBlock(spec.hookName))) {
|
|
1297
|
+
return { kind: "current", start, end };
|
|
1298
|
+
}
|
|
1299
|
+
const version = hookBlockContractVersion(text);
|
|
1300
|
+
if (version !== void 0 && version > GIT_HOOK_CONTRACT_VERSION) {
|
|
1301
|
+
return { kind: "newer", start, end };
|
|
1302
|
+
}
|
|
1303
|
+
return { kind: "stale", start, end };
|
|
1304
|
+
}
|
|
1305
|
+
var HUSKY_V8_SOURCE_RE = /^\.[ \t]+"\$\(dirname(?:[ \t]+--)?[ \t]+"\$0"\)\/_\/husky\.sh"[ \t]*$/mu;
|
|
1306
|
+
function blockInsertionPoint(content, allowShebangless, spec, huskyV8) {
|
|
1307
|
+
const shebangEnd = shellInsertionPoint(content, allowShebangless, spec);
|
|
1308
|
+
if (!huskyV8) return shebangEnd;
|
|
1309
|
+
const rest = content.subarray(shebangEnd).toString("utf8");
|
|
1310
|
+
const match = HUSKY_V8_SOURCE_RE.exec(rest);
|
|
1311
|
+
if (!match) return shebangEnd;
|
|
1312
|
+
const lineEnd = match.index + match[0].length;
|
|
1313
|
+
const afterLine = rest[lineEnd] === "\n" ? lineEnd + 1 : lineEnd;
|
|
1314
|
+
return shebangEnd + Buffer.byteLength(rest.slice(0, afterLine), "utf8");
|
|
1315
|
+
}
|
|
1316
|
+
var HUSKY_SPEC_MAJOR_RE = /^\s*[~^]?=?\s*v?([0-9]+)(?:\.(?:[0-9]+|[xX*])){0,2}(?:-[0-9A-Za-z.-]+)?\s*$/u;
|
|
1317
|
+
function declaredHuskyMajor(dir) {
|
|
1318
|
+
for (let current = resolve3(dir); ; ) {
|
|
1319
|
+
const manifest = join4(current, "package.json");
|
|
1320
|
+
if (existsSync3(manifest)) {
|
|
1321
|
+
try {
|
|
1322
|
+
if (lstatSync3(manifest).size > MAX_HOOK_BYTES) return void 0;
|
|
1323
|
+
const parsed = JSON.parse(readFileSync3(manifest, "utf8"));
|
|
1324
|
+
const range = parsed.devDependencies?.husky ?? parsed.dependencies?.husky;
|
|
1325
|
+
const major = typeof range === "string" ? HUSKY_SPEC_MAJOR_RE.exec(range)?.[1] : void 0;
|
|
1326
|
+
return major === void 0 ? void 0 : Number(major);
|
|
1327
|
+
} catch {
|
|
1328
|
+
return void 0;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
const parent = dirname3(current);
|
|
1332
|
+
if (parent === current || existsSync3(join4(current, ".git"))) return void 0;
|
|
1333
|
+
current = parent;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
function isHuskyV8Hook(hookPath) {
|
|
1337
|
+
const runtime = join4(dirname3(hookPath), "_", "husky.sh");
|
|
1338
|
+
try {
|
|
1339
|
+
const stat = lstatSync3(runtime);
|
|
1340
|
+
return stat.isFile() && stat.size <= MAX_HOOK_BYTES && readFileSync3(runtime, "utf8").includes("husky_skip_init");
|
|
1341
|
+
} catch (error) {
|
|
1342
|
+
if (errorCode(error) !== "ENOENT") return false;
|
|
1343
|
+
}
|
|
1344
|
+
const major = declaredHuskyMajor(dirname3(dirname3(hookPath)));
|
|
1345
|
+
return major !== void 0 && major <= 8;
|
|
1346
|
+
}
|
|
1347
|
+
var TERMINAL_LINE_RE = /^(?:exit|return)(?:\s|;|$)/u;
|
|
1348
|
+
var REDIRECT_OPERATOR_RE = /^[0-9]*(?:<<?|>>?|<&|>&|<>|>\|)$/u;
|
|
1349
|
+
var REDIRECT_WORD_RE = /^[0-9]*(?:<|>)/u;
|
|
1350
|
+
function isReplacingExec(line) {
|
|
1351
|
+
if (!/^exec(?:\s|$)/u.test(line)) return false;
|
|
1352
|
+
const words = line.slice(4).trim().split(/\s+/u).filter(Boolean);
|
|
1353
|
+
for (let index = 0; index < words.length; index += 1) {
|
|
1354
|
+
const word = words[index];
|
|
1355
|
+
if (word.startsWith("#")) break;
|
|
1356
|
+
if (REDIRECT_OPERATOR_RE.test(word)) {
|
|
1357
|
+
index += 1;
|
|
1358
|
+
continue;
|
|
1359
|
+
}
|
|
1360
|
+
if (!REDIRECT_WORD_RE.test(word)) return true;
|
|
1361
|
+
}
|
|
1362
|
+
return false;
|
|
1363
|
+
}
|
|
1364
|
+
function terminatesBefore(content, from, offset) {
|
|
1365
|
+
const lines = content.subarray(from, offset).toString("utf8").split("\n");
|
|
1366
|
+
return lines.some((line) => TERMINAL_LINE_RE.test(line) || isReplacingExec(line));
|
|
1367
|
+
}
|
|
1368
|
+
function mayTerminateBefore(content, from, offset) {
|
|
1369
|
+
return content.subarray(from, offset).toString("utf8").split(/\n|;|&&|\|\||\||[{()]|\b(?:then|do|else)\b/u).some((part) => {
|
|
1370
|
+
const command = part.trim();
|
|
1371
|
+
return TERMINAL_LINE_RE.test(command) || isReplacingExec(command);
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
function withoutRange(content, start, end) {
|
|
1375
|
+
const suffix = content.subarray(end);
|
|
1376
|
+
return Buffer.concat([
|
|
1377
|
+
content.subarray(0, start),
|
|
1378
|
+
suffix.at(0) === 10 ? suffix.subarray(1) : suffix
|
|
1379
|
+
]);
|
|
1380
|
+
}
|
|
1381
|
+
function insertBlock(content, at, spec) {
|
|
1382
|
+
return Buffer.concat([
|
|
1383
|
+
content.subarray(0, at),
|
|
1384
|
+
Buffer.from(managedHookBlock(spec.hookName)),
|
|
1385
|
+
Buffer.from("\n"),
|
|
1386
|
+
content.subarray(at)
|
|
1387
|
+
]);
|
|
1029
1388
|
}
|
|
1030
|
-
function
|
|
1031
|
-
const
|
|
1032
|
-
if (
|
|
1389
|
+
function migratedLegacyContent(existing, allowShebangless, spec, huskyV8) {
|
|
1390
|
+
const text = existing.toString("utf8");
|
|
1391
|
+
if (spec.hookName === "pre-commit" && isOwnedStandalonePreCommit(text)) {
|
|
1033
1392
|
return createdScaffold(spec);
|
|
1034
1393
|
}
|
|
1035
|
-
|
|
1036
|
-
if (
|
|
1394
|
+
if (spec.hookName !== "post-commit") return void 0;
|
|
1395
|
+
if (text.startsWith(`#!/bin/sh
|
|
1037
1396
|
${LEGACY_GLOBAL_OWNED_HEADER}
|
|
1038
1397
|
`)) {
|
|
1039
1398
|
const gateAt = existing.indexOf(Buffer.from('if [ "$(git config --get prim.active'));
|
|
@@ -1048,15 +1407,13 @@ ${LEGACY_GLOBAL_OWNED_HEADER}
|
|
|
1048
1407
|
existing.subarray(0, gateAt),
|
|
1049
1408
|
existing.subarray(chainAt)
|
|
1050
1409
|
]);
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
withoutLegacyGate
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
withoutLegacyGate.subarray(shebangEnd2)
|
|
1057
|
-
]);
|
|
1410
|
+
return insertBlock(
|
|
1411
|
+
withoutLegacyGate,
|
|
1412
|
+
blockInsertionPoint(withoutLegacyGate, allowShebangless, spec, huskyV8),
|
|
1413
|
+
spec
|
|
1414
|
+
);
|
|
1058
1415
|
}
|
|
1059
|
-
if (
|
|
1416
|
+
if (text.startsWith(`#!/bin/sh
|
|
1060
1417
|
${LEGACY_PRIM_OWNED_HEADER}
|
|
1061
1418
|
`)) {
|
|
1062
1419
|
const prefixLength = legacyProjectPrefixLength(existing);
|
|
@@ -1065,42 +1422,59 @@ ${LEGACY_PRIM_OWNED_HEADER}
|
|
|
1065
1422
|
}
|
|
1066
1423
|
return Buffer.concat([createdScaffold(spec), existing.subarray(prefixLength)]);
|
|
1067
1424
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
];
|
|
1079
|
-
const oldCreated = oldCreatedPrefixes.some(
|
|
1425
|
+
return void 0;
|
|
1426
|
+
}
|
|
1427
|
+
function mergedContent(existing, allowShebangless, spec, relocate, huskyV8) {
|
|
1428
|
+
if (!existing) return { content: createdScaffold(spec), migrates: false };
|
|
1429
|
+
const range = blockRange(existing, spec);
|
|
1430
|
+
if (range.kind === "absent") {
|
|
1431
|
+
const migrated = migratedLegacyContent(existing, allowShebangless, spec, huskyV8);
|
|
1432
|
+
if (migrated) return { content: migrated, migrates: true };
|
|
1433
|
+
} else if (range.kind !== "newer") {
|
|
1434
|
+
const oldCreated = oldCreatedPrefixes(spec).some(
|
|
1080
1435
|
(prefix) => range.start === prefix.length && existing.subarray(0, prefix.length).equals(prefix)
|
|
1081
1436
|
);
|
|
1082
1437
|
if (oldCreated) {
|
|
1083
1438
|
const suffix = existing.subarray(range.end);
|
|
1084
1439
|
const tail = suffix.at(0) === 10 ? suffix.subarray(1) : suffix;
|
|
1085
|
-
return
|
|
1440
|
+
return {
|
|
1441
|
+
content: Buffer.concat([createdScaffold(spec), tail]),
|
|
1442
|
+
migrates: range.kind === "stale"
|
|
1443
|
+
};
|
|
1086
1444
|
}
|
|
1087
1445
|
}
|
|
1088
1446
|
const shebangEnd = shellInsertionPoint(existing, allowShebangless, spec);
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
existing
|
|
1095
|
-
|
|
1096
|
-
|
|
1447
|
+
const insertion = blockInsertionPoint(existing, allowShebangless, spec, huskyV8);
|
|
1448
|
+
switch (range.kind) {
|
|
1449
|
+
case "newer":
|
|
1450
|
+
return { content: existing, migrates: false };
|
|
1451
|
+
case "absent":
|
|
1452
|
+
return { content: insertBlock(existing, insertion, spec), migrates: false };
|
|
1453
|
+
case "current":
|
|
1454
|
+
case "stale": {
|
|
1455
|
+
const misplaced = terminatesBefore(existing, shebangEnd, range.start) || range.start < insertion;
|
|
1456
|
+
if (relocate && misplaced) {
|
|
1457
|
+
const without = withoutRange(existing, range.start, range.end);
|
|
1458
|
+
return {
|
|
1459
|
+
content: insertBlock(
|
|
1460
|
+
without,
|
|
1461
|
+
blockInsertionPoint(without, allowShebangless, spec, huskyV8),
|
|
1462
|
+
spec
|
|
1463
|
+
),
|
|
1464
|
+
migrates: range.kind === "stale"
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
if (range.kind === "current") return { content: existing, migrates: false };
|
|
1468
|
+
return {
|
|
1469
|
+
content: Buffer.concat([
|
|
1470
|
+
existing.subarray(0, range.start),
|
|
1471
|
+
Buffer.from(managedHookBlock(spec.hookName)),
|
|
1472
|
+
existing.subarray(range.end)
|
|
1473
|
+
]),
|
|
1474
|
+
migrates: true
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1097
1477
|
}
|
|
1098
|
-
return Buffer.concat([
|
|
1099
|
-
withoutBlock.subarray(0, shebangEnd),
|
|
1100
|
-
block,
|
|
1101
|
-
Buffer.from("\n"),
|
|
1102
|
-
withoutBlock.subarray(shebangEnd)
|
|
1103
|
-
]);
|
|
1104
1478
|
}
|
|
1105
1479
|
function errorCode(error) {
|
|
1106
1480
|
return typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
@@ -1160,137 +1534,143 @@ function unlinkHookUnchanged(path, expected, stat, spec) {
|
|
|
1160
1534
|
assertTargetUnchanged(path, expected, spec, stat);
|
|
1161
1535
|
unlinkSync2(path);
|
|
1162
1536
|
}
|
|
1163
|
-
function
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1537
|
+
function workingLegacyBlock(content, target, spec) {
|
|
1538
|
+
try {
|
|
1539
|
+
const range = blockRange(content, spec);
|
|
1540
|
+
if (range.kind !== "stale") return false;
|
|
1541
|
+
const block = content.subarray(range.start, range.end).toString("utf8");
|
|
1542
|
+
const from = shellInsertionPoint(content, target.kind === "husky_v9", spec);
|
|
1543
|
+
return isRecognizedLegacyBlock(spec.hookName, block) && !mayTerminateBefore(content, from, range.start);
|
|
1544
|
+
} catch {
|
|
1545
|
+
return false;
|
|
1546
|
+
}
|
|
1170
1547
|
}
|
|
1171
|
-
|
|
1548
|
+
var AMBIENT_LOCATIONS = ["repository", "prim"];
|
|
1549
|
+
var EXPLICIT_LOCATIONS = ["repository", "prim", "worktree"];
|
|
1550
|
+
function ensureTarget(target, spec, policy) {
|
|
1551
|
+
const result = { hookName: spec.hookName, path: target.hookPath, kind: target.kind };
|
|
1552
|
+
const skip = (outcome) => ({
|
|
1553
|
+
...result,
|
|
1554
|
+
changed: false,
|
|
1555
|
+
outcome
|
|
1556
|
+
});
|
|
1172
1557
|
assertUsableHuskyDispatcher(target, spec);
|
|
1173
1558
|
const stat = assertSafeFile(target.hookPath, spec);
|
|
1174
1559
|
if (stat && target.kind === "direct" && (stat.mode & 64) === 0) {
|
|
1175
1560
|
throw new Error(`existing ${spec.hookName} hook is not executable: ${target.hookPath}`);
|
|
1176
1561
|
}
|
|
1177
1562
|
const existing = stat ? readHookFile(target.hookPath) : void 0;
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1563
|
+
if (policy.repairOnly && (!existing || blockRange(existing, spec).kind === "absent")) {
|
|
1564
|
+
return skip("skipped");
|
|
1565
|
+
}
|
|
1566
|
+
const huskyV8 = isHuskyV8Hook(target.hookPath);
|
|
1567
|
+
const next = mergedContent(existing, target.kind === "husky_v9", spec, policy.relocate, huskyV8);
|
|
1568
|
+
if (existing && next.content.equals(existing)) return skip("unchanged");
|
|
1569
|
+
if (!policy.writable(target.location)) {
|
|
1570
|
+
if (target.location === "worktree") return skip("deferred");
|
|
1571
|
+
return skip(existing && workingLegacyBlock(existing, target, spec) ? "kept" : "external");
|
|
1572
|
+
}
|
|
1573
|
+
if (next.migrates && inspectGitHookEntrypoint() !== "ready") return skip("runtime_missing");
|
|
1574
|
+
rewriteHookAtomically(target.hookPath, next.content, existing, spec, stat);
|
|
1575
|
+
return { ...result, changed: true, outcome: existing ? "updated" : "created" };
|
|
1576
|
+
}
|
|
1577
|
+
function ensureEffectiveGitHook(hookName, cwd = process.cwd(), options = {}) {
|
|
1578
|
+
const target = resolveEffectiveGitHook(hookName, cwd);
|
|
1579
|
+
if (gitHooksMode({ cwd: target.gitRoot }) === "manual") {
|
|
1580
|
+
return {
|
|
1581
|
+
hookName,
|
|
1582
|
+
path: target.hookPath,
|
|
1583
|
+
kind: target.kind,
|
|
1584
|
+
changed: false,
|
|
1585
|
+
outcome: "manual"
|
|
1586
|
+
};
|
|
1189
1587
|
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
hooksDir: dirname3(absolute),
|
|
1198
|
-
hookPath: absolute,
|
|
1199
|
-
kind: "direct"
|
|
1200
|
-
},
|
|
1201
|
-
POST_COMMIT_MANAGED_HOOK,
|
|
1202
|
-
initialContent
|
|
1203
|
-
);
|
|
1204
|
-
return { ...result, kind: "direct" };
|
|
1588
|
+
const explicit = (options.context ?? "explicit") === "explicit";
|
|
1589
|
+
const allowed = explicit ? EXPLICIT_LOCATIONS : AMBIENT_LOCATIONS;
|
|
1590
|
+
return ensureTarget(target, managedHookSpec(hookName), {
|
|
1591
|
+
writable: (location) => allowed.includes(location),
|
|
1592
|
+
relocate: explicit,
|
|
1593
|
+
repairOnly: options.repairOnly === true
|
|
1594
|
+
});
|
|
1205
1595
|
}
|
|
1206
|
-
function
|
|
1596
|
+
function explicitTarget(hookPath, options) {
|
|
1207
1597
|
const absolute = resolve3(hookPath);
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
return { ...result, kind: "direct" };
|
|
1598
|
+
return {
|
|
1599
|
+
gitRoot: dirname3(dirname3(absolute)),
|
|
1600
|
+
hooksDir: dirname3(absolute),
|
|
1601
|
+
hookPath: absolute,
|
|
1602
|
+
// A `.husky/<hook>` file is run by Husky's dispatcher via `sh`, so it needs
|
|
1603
|
+
// neither a shebang nor the executable bit.
|
|
1604
|
+
kind: options.husky ? "husky_v9" : "direct",
|
|
1605
|
+
// The caller chose this exact file (a --target, or a consented global dir).
|
|
1606
|
+
location: "external"
|
|
1607
|
+
};
|
|
1219
1608
|
}
|
|
1220
|
-
function
|
|
1221
|
-
|
|
1609
|
+
function hasCurrentHookBlock(hookName, hookPath) {
|
|
1610
|
+
try {
|
|
1611
|
+
const kind = blockRange(readHookFile(hookPath), managedHookSpec(hookName)).kind;
|
|
1612
|
+
return kind === "current" || kind === "newer";
|
|
1613
|
+
} catch {
|
|
1614
|
+
return false;
|
|
1615
|
+
}
|
|
1222
1616
|
}
|
|
1223
|
-
function
|
|
1224
|
-
return
|
|
1617
|
+
function ensureGitHookAtPath(hookName, hookPath, options = {}) {
|
|
1618
|
+
return ensureTarget(explicitTarget(hookPath, options), managedHookSpec(hookName), {
|
|
1619
|
+
writable: () => true,
|
|
1620
|
+
relocate: true,
|
|
1621
|
+
repairOnly: false
|
|
1622
|
+
});
|
|
1225
1623
|
}
|
|
1226
|
-
function
|
|
1227
|
-
const
|
|
1624
|
+
function inspectEffectiveGitHook(hookName, cwd = process.cwd()) {
|
|
1625
|
+
const spec = managedHookSpec(hookName);
|
|
1626
|
+
const target = resolveEffectiveGitHook(hookName, cwd);
|
|
1627
|
+
const base = {
|
|
1628
|
+
...target,
|
|
1629
|
+
hookName,
|
|
1630
|
+
mode: gitHooksMode({ cwd: target.gitRoot }),
|
|
1631
|
+
entrypoint: inspectGitHookEntrypoint()
|
|
1632
|
+
};
|
|
1633
|
+
const uncovered = (reason2, executable2 = false) => ({ ...base, covered: false, executable: executable2, current: false, reason: reason2 });
|
|
1228
1634
|
try {
|
|
1229
1635
|
assertUsableHuskyDispatcher(target, spec);
|
|
1230
1636
|
} catch (error) {
|
|
1231
1637
|
const missing = error instanceof Error && (error.message.includes("missing or not executable") || error.message.includes("unsafe"));
|
|
1232
|
-
return
|
|
1233
|
-
...target,
|
|
1234
|
-
covered: false,
|
|
1235
|
-
executable: false,
|
|
1236
|
-
current: false,
|
|
1237
|
-
reason: missing ? "husky_dispatcher_missing" : "husky_dispatcher_invalid"
|
|
1238
|
-
};
|
|
1638
|
+
return uncovered(missing ? "husky_dispatcher_missing" : "husky_dispatcher_invalid");
|
|
1239
1639
|
}
|
|
1240
1640
|
let stat;
|
|
1241
1641
|
try {
|
|
1242
1642
|
stat = assertSafeFile(target.hookPath, spec);
|
|
1243
1643
|
} catch {
|
|
1244
|
-
return
|
|
1245
|
-
...target,
|
|
1246
|
-
covered: false,
|
|
1247
|
-
executable: false,
|
|
1248
|
-
current: false,
|
|
1249
|
-
reason: "unsafe_target"
|
|
1250
|
-
};
|
|
1251
|
-
}
|
|
1252
|
-
if (!stat) {
|
|
1253
|
-
return {
|
|
1254
|
-
...target,
|
|
1255
|
-
covered: false,
|
|
1256
|
-
executable: false,
|
|
1257
|
-
current: false,
|
|
1258
|
-
reason: "missing"
|
|
1259
|
-
};
|
|
1644
|
+
return uncovered("unsafe_target");
|
|
1260
1645
|
}
|
|
1646
|
+
if (!stat) return uncovered("missing");
|
|
1261
1647
|
const executable = target.kind === "husky_v9" || (stat.mode & 64) !== 0;
|
|
1648
|
+
let range;
|
|
1649
|
+
let content;
|
|
1650
|
+
let insertion;
|
|
1262
1651
|
try {
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
const positioned = range.kind !== "absent" && range.start === shebangEnd;
|
|
1267
|
-
const current = range.kind === "current" && positioned;
|
|
1268
|
-
return {
|
|
1269
|
-
...target,
|
|
1270
|
-
covered: current && executable,
|
|
1271
|
-
executable,
|
|
1272
|
-
current,
|
|
1273
|
-
reason: range.kind === "current" && !positioned ? "unreachable_block" : !current ? "stale_block" : executable ? void 0 : "not_executable"
|
|
1274
|
-
};
|
|
1652
|
+
content = readHookFile(target.hookPath);
|
|
1653
|
+
insertion = shellInsertionPoint(content, target.kind === "husky_v9", spec);
|
|
1654
|
+
range = blockRange(content, spec);
|
|
1275
1655
|
} catch (error) {
|
|
1276
1656
|
const message = error instanceof Error ? error.message : "";
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
executable,
|
|
1282
|
-
current: false,
|
|
1283
|
-
reason
|
|
1284
|
-
};
|
|
1657
|
+
return uncovered(
|
|
1658
|
+
message.includes("binary") ? "binary" : message.includes("unsupported") ? "unsupported_interpreter" : "malformed_markers",
|
|
1659
|
+
executable
|
|
1660
|
+
);
|
|
1285
1661
|
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
const
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1662
|
+
const huskyV8 = isHuskyV8Hook(target.hookPath);
|
|
1663
|
+
const preferred = blockInsertionPoint(content, target.kind === "husky_v9", spec, huskyV8);
|
|
1664
|
+
const current = range.kind === "current" || range.kind === "newer";
|
|
1665
|
+
const legacy = range.kind === "stale" && workingLegacyBlock(content, target, spec);
|
|
1666
|
+
const reason = range.kind === "absent" ? "missing_block" : range.kind === "stale" && !legacy ? "stale_block" : terminatesBefore(content, insertion, range.start) ? "unreachable_block" : !executable ? "not_executable" : legacy ? "legacy_block" : base.entrypoint !== "ready" ? "entrypoint_missing" : range.start < preferred ? "misplaced_block" : void 0;
|
|
1667
|
+
return {
|
|
1668
|
+
...base,
|
|
1669
|
+
covered: reason === void 0,
|
|
1670
|
+
executable,
|
|
1671
|
+
current,
|
|
1672
|
+
...reason ? { reason } : {}
|
|
1673
|
+
};
|
|
1294
1674
|
}
|
|
1295
1675
|
function projectHooksPathIsConfigured(gitRoot) {
|
|
1296
1676
|
for (const scope of ["--worktree", "--local"]) {
|
|
@@ -1307,45 +1687,53 @@ function projectHooksPathIsConfigured(gitRoot) {
|
|
|
1307
1687
|
}
|
|
1308
1688
|
return false;
|
|
1309
1689
|
}
|
|
1310
|
-
function
|
|
1311
|
-
const
|
|
1312
|
-
if (
|
|
1313
|
-
|
|
1314
|
-
return uninstallEffectivePostCommitHook(gitRoot);
|
|
1690
|
+
function uninstallProjectGitHook(hookName, cwd = process.cwd()) {
|
|
1691
|
+
const target = projectGitHookTarget(hookName, cwd);
|
|
1692
|
+
if (target.location === "external") {
|
|
1693
|
+
return { path: target.hookPath, changed: false, removedFile: false, skipped: "external" };
|
|
1315
1694
|
}
|
|
1316
|
-
|
|
1317
|
-
cwd: gitRoot,
|
|
1318
|
-
encoding: "utf8",
|
|
1319
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
1320
|
-
timeout: GIT_TIMEOUT_MS
|
|
1321
|
-
}).replace(/\r?\n$/u, "");
|
|
1322
|
-
return uninstallPostCommitHookAtPath(
|
|
1323
|
-
resolve3(safeGitPath(gitRoot, commonDir), "hooks", "post-commit")
|
|
1324
|
-
);
|
|
1695
|
+
return uninstallTarget(target, managedHookSpec(hookName));
|
|
1325
1696
|
}
|
|
1326
|
-
function
|
|
1697
|
+
function projectGitHookTarget(hookName, cwd = process.cwd()) {
|
|
1327
1698
|
const gitRoot = gitToplevel(cwd);
|
|
1328
1699
|
if (!gitRoot) throw new Error("not a git repository");
|
|
1329
|
-
if (projectHooksPathIsConfigured(gitRoot))
|
|
1330
|
-
|
|
1331
|
-
|
|
1700
|
+
if (projectHooksPathIsConfigured(gitRoot)) return resolveEffectiveGitHook(hookName, gitRoot);
|
|
1701
|
+
return {
|
|
1702
|
+
...explicitTarget(resolve3(projectHooksDir(gitRoot), hookName), {}),
|
|
1703
|
+
gitRoot,
|
|
1704
|
+
location: "repository"
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
function projectHooksDir(gitRoot) {
|
|
1332
1708
|
const commonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
|
|
1333
1709
|
cwd: gitRoot,
|
|
1334
1710
|
encoding: "utf8",
|
|
1335
1711
|
stdio: ["ignore", "pipe", "ignore"],
|
|
1336
1712
|
timeout: GIT_TIMEOUT_MS
|
|
1337
1713
|
}).replace(/\r?\n$/u, "");
|
|
1338
|
-
return
|
|
1339
|
-
|
|
1340
|
-
|
|
1714
|
+
return resolve3(safeGitPath(gitRoot, commonDir), "hooks");
|
|
1715
|
+
}
|
|
1716
|
+
function uninstallGitHookAtPath(hookName, hookPath, options = {}) {
|
|
1717
|
+
return uninstallTarget(explicitTarget(hookPath, options), managedHookSpec(hookName));
|
|
1341
1718
|
}
|
|
1342
1719
|
function uninstallTarget(target, spec) {
|
|
1720
|
+
const unchanged = { path: target.hookPath, changed: false, removedFile: false };
|
|
1343
1721
|
const stat = assertSafeFile(target.hookPath, spec);
|
|
1344
|
-
if (!stat) return
|
|
1722
|
+
if (!stat) return unchanged;
|
|
1345
1723
|
const existing = readHookFile(target.hookPath);
|
|
1724
|
+
const remove = () => {
|
|
1725
|
+
unlinkHookUnchanged(target.hookPath, existing, stat, spec);
|
|
1726
|
+
return { path: target.hookPath, changed: true, removedFile: true };
|
|
1727
|
+
};
|
|
1728
|
+
const rewrite = (next) => {
|
|
1729
|
+
rewriteHookAtomically(target.hookPath, next, existing, spec, stat);
|
|
1730
|
+
return { path: target.hookPath, changed: true, removedFile: false };
|
|
1731
|
+
};
|
|
1346
1732
|
const range = blockRange(existing, spec);
|
|
1347
1733
|
if (range.kind === "absent") {
|
|
1348
|
-
|
|
1734
|
+
const text = existing.toString("utf8");
|
|
1735
|
+
if (spec.hookName === "pre-commit" && isOwnedStandalonePreCommit(text)) return remove();
|
|
1736
|
+
if (spec.hookName === "post-commit" && text.startsWith(`#!/bin/sh
|
|
1349
1737
|
${LEGACY_PRIM_OWNED_HEADER}
|
|
1350
1738
|
`)) {
|
|
1351
1739
|
const prefixLength = legacyProjectPrefixLength(existing);
|
|
@@ -1353,20 +1741,9 @@ ${LEGACY_PRIM_OWNED_HEADER}
|
|
|
1353
1741
|
throw new Error("unrecognized legacy Prim post-commit invocation");
|
|
1354
1742
|
}
|
|
1355
1743
|
const tail = existing.subarray(prefixLength);
|
|
1356
|
-
|
|
1357
|
-
unlinkHookUnchanged(target.hookPath, existing, stat, spec);
|
|
1358
|
-
return { path: target.hookPath, changed: true, removedFile: true };
|
|
1359
|
-
}
|
|
1360
|
-
rewriteHookAtomically(
|
|
1361
|
-
target.hookPath,
|
|
1362
|
-
Buffer.concat([Buffer.from("#!/bin/sh\n"), tail]),
|
|
1363
|
-
existing,
|
|
1364
|
-
spec,
|
|
1365
|
-
stat
|
|
1366
|
-
);
|
|
1367
|
-
return { path: target.hookPath, changed: true, removedFile: false };
|
|
1744
|
+
return tail.length === 0 ? remove() : rewrite(Buffer.concat([Buffer.from("#!/bin/sh\n"), tail]));
|
|
1368
1745
|
}
|
|
1369
|
-
return
|
|
1746
|
+
return unchanged;
|
|
1370
1747
|
}
|
|
1371
1748
|
const shebangEnd = shellInsertionPoint(existing, target.kind === "husky_v9", spec);
|
|
1372
1749
|
const suffix = existing.subarray(range.end);
|
|
@@ -1375,77 +1752,16 @@ ${LEGACY_PRIM_OWNED_HEADER}
|
|
|
1375
1752
|
`);
|
|
1376
1753
|
if (range.start === shebangEnd && suffix.subarray(0, createdSuffix.length).equals(createdSuffix)) {
|
|
1377
1754
|
const tail = suffix.subarray(createdSuffix.length);
|
|
1378
|
-
|
|
1379
|
-
unlinkHookUnchanged(target.hookPath, existing, stat, spec);
|
|
1380
|
-
return { path: target.hookPath, changed: true, removedFile: true };
|
|
1381
|
-
}
|
|
1382
|
-
rewriteHookAtomically(
|
|
1383
|
-
target.hookPath,
|
|
1384
|
-
Buffer.concat([existing.subarray(0, shebangEnd), tail]),
|
|
1385
|
-
existing,
|
|
1386
|
-
spec,
|
|
1387
|
-
stat
|
|
1388
|
-
);
|
|
1389
|
-
return { path: target.hookPath, changed: true, removedFile: false };
|
|
1755
|
+
return tail.length === 0 ? remove() : rewrite(Buffer.concat([existing.subarray(0, shebangEnd), tail]));
|
|
1390
1756
|
}
|
|
1391
|
-
const
|
|
1392
|
-
Buffer.from(`#!/bin/sh
|
|
1393
|
-
# ${spec.createdMark}
|
|
1394
|
-
|
|
1395
|
-
`),
|
|
1396
|
-
...spec.hookName === POST_COMMIT_MANAGED_HOOK.hookName ? [Buffer.from(`#!/bin/sh
|
|
1397
|
-
# ${LEGACY_PRIM_CREATED_MARK}
|
|
1398
|
-
|
|
1399
|
-
`)] : []
|
|
1400
|
-
];
|
|
1401
|
-
const createdPrefix = createdPrefixes.find(
|
|
1757
|
+
const createdPrefix = oldCreatedPrefixes(spec).find(
|
|
1402
1758
|
(prefix) => range.start === prefix.length && existing.subarray(0, prefix.length).equals(prefix)
|
|
1403
1759
|
);
|
|
1404
1760
|
if (createdPrefix) {
|
|
1405
1761
|
const tail = suffix.at(0) === 10 ? suffix.subarray(1) : suffix;
|
|
1406
|
-
|
|
1407
|
-
unlinkHookUnchanged(target.hookPath, existing, stat, spec);
|
|
1408
|
-
return { path: target.hookPath, changed: true, removedFile: true };
|
|
1409
|
-
}
|
|
1410
|
-
rewriteHookAtomically(
|
|
1411
|
-
target.hookPath,
|
|
1412
|
-
Buffer.concat([Buffer.from("#!/bin/sh\n"), tail]),
|
|
1413
|
-
existing,
|
|
1414
|
-
spec,
|
|
1415
|
-
stat
|
|
1416
|
-
);
|
|
1417
|
-
return { path: target.hookPath, changed: true, removedFile: false };
|
|
1762
|
+
return tail.length === 0 ? remove() : rewrite(Buffer.concat([Buffer.from("#!/bin/sh\n"), tail]));
|
|
1418
1763
|
}
|
|
1419
|
-
|
|
1420
|
-
existing.subarray(0, range.start),
|
|
1421
|
-
range.start === shebangEnd && suffix.at(0) === 10 ? suffix.subarray(1) : suffix
|
|
1422
|
-
]);
|
|
1423
|
-
rewriteHookAtomically(target.hookPath, next, existing, spec, stat);
|
|
1424
|
-
return { path: target.hookPath, changed: true, removedFile: false };
|
|
1425
|
-
}
|
|
1426
|
-
function uninstallPostCommitHookAtPath(hookPath) {
|
|
1427
|
-
const absolute = resolve3(hookPath);
|
|
1428
|
-
return uninstallTarget(
|
|
1429
|
-
{
|
|
1430
|
-
gitRoot: dirname3(dirname3(absolute)),
|
|
1431
|
-
hooksDir: dirname3(absolute),
|
|
1432
|
-
hookPath: absolute,
|
|
1433
|
-
kind: "direct"
|
|
1434
|
-
},
|
|
1435
|
-
POST_COMMIT_MANAGED_HOOK
|
|
1436
|
-
);
|
|
1437
|
-
}
|
|
1438
|
-
function uninstallPostRewriteHookAtPath(hookPath) {
|
|
1439
|
-
const absolute = resolve3(hookPath);
|
|
1440
|
-
return uninstallTarget(
|
|
1441
|
-
{
|
|
1442
|
-
gitRoot: dirname3(dirname3(absolute)),
|
|
1443
|
-
hooksDir: dirname3(absolute),
|
|
1444
|
-
hookPath: absolute,
|
|
1445
|
-
kind: "direct"
|
|
1446
|
-
},
|
|
1447
|
-
POST_REWRITE_MANAGED_HOOK
|
|
1448
|
-
);
|
|
1764
|
+
return rewrite(withoutRange(existing, range.start, range.end));
|
|
1449
1765
|
}
|
|
1450
1766
|
|
|
1451
1767
|
// src/lib/repository-binding.ts
|
|
@@ -1537,18 +1853,19 @@ function kickDaemonEnsure(options = {}) {
|
|
|
1537
1853
|
|
|
1538
1854
|
export {
|
|
1539
1855
|
warmBinCache,
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1856
|
+
isLegacyOwnedGlobalHook,
|
|
1857
|
+
gitHooksMode,
|
|
1858
|
+
isOwnedStandalonePreCommit,
|
|
1859
|
+
externalHookRemedy,
|
|
1860
|
+
resolveEffectiveGitHook,
|
|
1861
|
+
ensureEffectiveGitHook,
|
|
1862
|
+
hasCurrentHookBlock,
|
|
1863
|
+
ensureGitHookAtPath,
|
|
1864
|
+
inspectEffectiveGitHook,
|
|
1865
|
+
uninstallProjectGitHook,
|
|
1866
|
+
projectGitHookTarget,
|
|
1867
|
+
projectHooksDir,
|
|
1868
|
+
uninstallGitHookAtPath,
|
|
1552
1869
|
resolveRepositoryBinding,
|
|
1553
1870
|
bindRepositoryWithClient,
|
|
1554
1871
|
bindRepository,
|