@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
package/dist/index.js
CHANGED
|
@@ -2,34 +2,42 @@
|
|
|
2
2
|
import {
|
|
3
3
|
bindRepository,
|
|
4
4
|
bindRepositoryWithClient,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
ensureEffectiveGitHook,
|
|
6
|
+
ensureGitHookAtPath,
|
|
7
|
+
externalHookRemedy,
|
|
8
|
+
gitHooksMode,
|
|
9
|
+
hasCurrentHookBlock,
|
|
10
|
+
inspectEffectiveGitHook,
|
|
11
|
+
isLegacyOwnedGlobalHook,
|
|
12
|
+
isOwnedStandalonePreCommit,
|
|
11
13
|
kickDaemonEnsure,
|
|
12
|
-
postCommitHookBlock,
|
|
13
|
-
postRewriteHookBlock,
|
|
14
14
|
printJson,
|
|
15
|
+
projectGitHookTarget,
|
|
16
|
+
projectHooksDir,
|
|
15
17
|
registerSkillCommands,
|
|
18
|
+
resolveEffectiveGitHook,
|
|
16
19
|
resolveRepositoryBinding,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
uninstallProjectPostRewriteHook
|
|
21
|
-
} from "./chunk-DCJN6LZH.js";
|
|
20
|
+
uninstallGitHookAtPath,
|
|
21
|
+
uninstallProjectGitHook
|
|
22
|
+
} from "./chunk-7KABYYWG.js";
|
|
22
23
|
import {
|
|
23
24
|
requireDurableIngestAcknowledgement
|
|
24
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-QCBB5HFX.js";
|
|
25
26
|
import {
|
|
26
27
|
checkAffectedDecisions,
|
|
27
28
|
daemonOrDirectGet,
|
|
28
29
|
formatDecisionsWarning
|
|
29
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-27ADT6FQ.js";
|
|
30
31
|
import {
|
|
31
32
|
fetchFeedbackCapability
|
|
32
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-UO2U6IKF.js";
|
|
34
|
+
import {
|
|
35
|
+
POST_COMMIT_GRACE_MS,
|
|
36
|
+
clearHooksWired,
|
|
37
|
+
inspectPostCommitFiring,
|
|
38
|
+
lastConversationCaptured,
|
|
39
|
+
recordHooksWired
|
|
40
|
+
} from "./chunk-XEDJ5N3M.js";
|
|
33
41
|
import {
|
|
34
42
|
isUnattended,
|
|
35
43
|
unattendedEnv
|
|
@@ -48,7 +56,7 @@ import {
|
|
|
48
56
|
rotationIdentity,
|
|
49
57
|
sweepOrphanedDrainProgress,
|
|
50
58
|
writeDrainCheckpoint
|
|
51
|
-
} from "./chunk-
|
|
59
|
+
} from "./chunk-4GHANPWQ.js";
|
|
52
60
|
import {
|
|
53
61
|
MS_PER_SECOND,
|
|
54
62
|
deliveryBacklogState,
|
|
@@ -57,13 +65,12 @@ import {
|
|
|
57
65
|
formatStatusline,
|
|
58
66
|
formatTeammates,
|
|
59
67
|
repositoryBindingDiagnosticLabel
|
|
60
|
-
} from "./chunk-
|
|
68
|
+
} from "./chunk-ROLYLXJS.js";
|
|
61
69
|
import {
|
|
62
70
|
bold,
|
|
63
71
|
boundedHealthError,
|
|
64
72
|
color,
|
|
65
73
|
colorForArea,
|
|
66
|
-
decisionIngestionStatus,
|
|
67
74
|
dim,
|
|
68
75
|
fetchAndCacheCollectScope,
|
|
69
76
|
isCliErrorResponse,
|
|
@@ -76,13 +83,8 @@ import {
|
|
|
76
83
|
isGitHubInstallIntentStartResponse,
|
|
77
84
|
isGitHubInstallIntentStatusResponse,
|
|
78
85
|
isMoveIngestRequest,
|
|
79
|
-
isRepoActiveForCapture,
|
|
80
|
-
isValidRepoSyncId,
|
|
81
|
-
repoSyncId,
|
|
82
|
-
repositoryBindingState,
|
|
83
|
-
setRepoActive,
|
|
84
86
|
stripAnsi
|
|
85
|
-
} from "./chunk-
|
|
87
|
+
} from "./chunk-Z6NPNLOT.js";
|
|
86
88
|
import {
|
|
87
89
|
CliManagementResponseError,
|
|
88
90
|
HttpError,
|
|
@@ -99,8 +101,9 @@ import {
|
|
|
99
101
|
setStoredToken,
|
|
100
102
|
stripControlChars,
|
|
101
103
|
terminalSafeLine
|
|
102
|
-
} from "./chunk-
|
|
104
|
+
} from "./chunk-LY6NJ7KL.js";
|
|
103
105
|
import {
|
|
106
|
+
MANAGED_GIT_HOOK_NAMES,
|
|
104
107
|
assertOwnedHookRuntime,
|
|
105
108
|
atomicWrite,
|
|
106
109
|
binFile,
|
|
@@ -110,15 +113,16 @@ import {
|
|
|
110
113
|
entryHasCommand,
|
|
111
114
|
hookCommandResolutions,
|
|
112
115
|
hookRuntimePaths,
|
|
116
|
+
inspectGitHookEntrypoint,
|
|
113
117
|
inspectHookRuntime,
|
|
114
118
|
inspectHookRuntimeResolutions,
|
|
115
119
|
inspectHookRuntimeResolutions2,
|
|
116
120
|
makeRegistration,
|
|
121
|
+
managedHookBlock,
|
|
117
122
|
packageRoot,
|
|
118
123
|
packageVersion,
|
|
119
124
|
performStatus,
|
|
120
125
|
performStatus2,
|
|
121
|
-
pinnedHookCommand,
|
|
122
126
|
pinnedNpxArgs,
|
|
123
127
|
projectRoot,
|
|
124
128
|
readSettings,
|
|
@@ -129,16 +133,24 @@ import {
|
|
|
129
133
|
stableNodePath,
|
|
130
134
|
stageHookRuntime,
|
|
131
135
|
stripCommand
|
|
132
|
-
} from "./chunk-
|
|
136
|
+
} from "./chunk-Y5W4TZM7.js";
|
|
133
137
|
import {
|
|
134
138
|
canonicalGitRoot,
|
|
135
139
|
canonicalRepositoryPath,
|
|
136
|
-
|
|
137
|
-
|
|
140
|
+
decisionIngestionStatus,
|
|
141
|
+
gitToplevel,
|
|
142
|
+
isRepoActiveForCapture,
|
|
143
|
+
isValidRepoSyncId,
|
|
144
|
+
mainWorktreeRoot,
|
|
145
|
+
repoActiveFlag,
|
|
146
|
+
repoSyncId,
|
|
147
|
+
repositoryBindingState,
|
|
148
|
+
setRepoActive
|
|
149
|
+
} from "./chunk-DHOQGHKQ.js";
|
|
138
150
|
import {
|
|
139
151
|
daemonIsLive,
|
|
140
152
|
daemonRequest
|
|
141
|
-
} from "./chunk-
|
|
153
|
+
} from "./chunk-35FMRWJH.js";
|
|
142
154
|
import {
|
|
143
155
|
REFRESH_TOKEN_PATH,
|
|
144
156
|
TOKEN_FILE_PATH,
|
|
@@ -153,11 +165,11 @@ import {
|
|
|
153
165
|
syncDirectory,
|
|
154
166
|
withFileLock,
|
|
155
167
|
withFileLockSync
|
|
156
|
-
} from "./chunk-
|
|
168
|
+
} from "./chunk-DWRSEC34.js";
|
|
157
169
|
|
|
158
170
|
// src/index.ts
|
|
159
171
|
import { readFileSync as readFileSync10 } from "fs";
|
|
160
|
-
import { dirname as
|
|
172
|
+
import { dirname as dirname6, resolve as resolve4 } from "path";
|
|
161
173
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
162
174
|
import { Command } from "commander";
|
|
163
175
|
import updateNotifier from "update-notifier";
|
|
@@ -1352,8 +1364,8 @@ import {
|
|
|
1352
1364
|
writeFileSync
|
|
1353
1365
|
} from "fs";
|
|
1354
1366
|
import { homedir } from "os";
|
|
1355
|
-
import {
|
|
1356
|
-
import { Option } from "commander";
|
|
1367
|
+
import { join, resolve } from "path";
|
|
1368
|
+
import { Argument, Option } from "commander";
|
|
1357
1369
|
var PRE_COMMIT = { hookName: "pre-commit", binName: "prim-pre-commit" };
|
|
1358
1370
|
var POST_COMMIT = { hookName: "post-commit", binName: "prim-post-commit" };
|
|
1359
1371
|
var POST_REWRITE = { hookName: "post-rewrite", binName: "prim-post-rewrite" };
|
|
@@ -1369,84 +1381,15 @@ var PRIM_BLOCK_START = blockMarkers(PRE_COMMIT).start;
|
|
|
1369
1381
|
var PRIM_BLOCK_END = blockMarkers(PRE_COMMIT).end;
|
|
1370
1382
|
var PRIM_MANAGED_MARK = "prim-managed-hook";
|
|
1371
1383
|
var PRIM_CREATED_MARK = "prim-created-hook";
|
|
1372
|
-
function
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
function gatedShim(binName) {
|
|
1376
|
-
return `if [ "$(git config --get prim.active 2>/dev/null)" = "true" ]; then
|
|
1377
|
-
${hookShim(binName)}
|
|
1378
|
-
fi`;
|
|
1379
|
-
}
|
|
1380
|
-
function dotGitScript(spec) {
|
|
1381
|
-
return `#!/bin/sh
|
|
1382
|
-
# prim ${spec.hookName} hook \u2014 installed by: prim hooks install (${PRIM_MANAGED_MARK})
|
|
1383
|
-
|
|
1384
|
-
${hookShim(spec.binName)}
|
|
1385
|
-
`;
|
|
1386
|
-
}
|
|
1387
|
-
function isOwnedStandalonePreCommit(content) {
|
|
1388
|
-
if (content === "#!/bin/sh\nprim-pre-commit\n") return true;
|
|
1389
|
-
const prefix = `#!/bin/sh
|
|
1390
|
-
# prim pre-commit hook \u2014 installed by: prim hooks install (${PRIM_MANAGED_MARK})
|
|
1391
|
-
|
|
1392
|
-
`;
|
|
1393
|
-
const suffix = "; } || true\n";
|
|
1394
|
-
if (!content.startsWith(prefix) || !content.endsWith(suffix)) return false;
|
|
1395
|
-
const body = content.slice(prefix.length, -suffix.length);
|
|
1396
|
-
if (!body.startsWith("{ ") || /[\r\n]/u.test(body)) return false;
|
|
1397
|
-
return commandMatchesBin(body.slice(2), PRE_COMMIT.binName);
|
|
1398
|
-
}
|
|
1399
|
-
function huskyBlock(spec) {
|
|
1400
|
-
if (spec.hookName === POST_COMMIT.hookName) return postCommitHookBlock();
|
|
1401
|
-
if (spec.hookName === POST_REWRITE.hookName) return postRewriteHookBlock();
|
|
1402
|
-
const { start, end } = blockMarkers(spec);
|
|
1403
|
-
return `${start}
|
|
1404
|
-
${hookShim(spec.binName)}
|
|
1405
|
-
${end}`;
|
|
1406
|
-
}
|
|
1407
|
-
function gatedBlock(spec) {
|
|
1408
|
-
if (spec.hookName === POST_COMMIT.hookName) return postCommitHookBlock();
|
|
1409
|
-
if (spec.hookName === POST_REWRITE.hookName) return postRewriteHookBlock();
|
|
1410
|
-
const { start, end } = blockMarkers(spec);
|
|
1411
|
-
return `${start}
|
|
1412
|
-
${gatedShim(spec.binName)}
|
|
1413
|
-
${end}`;
|
|
1414
|
-
}
|
|
1415
|
-
function primBlockRange(existing, spec) {
|
|
1416
|
-
const { start, end } = blockMarkers(spec);
|
|
1417
|
-
const starts = existing.split(start).length - 1;
|
|
1418
|
-
const ends = existing.split(end).length - 1;
|
|
1419
|
-
if (starts === 0 && ends === 0) return null;
|
|
1420
|
-
const from = existing.indexOf(start);
|
|
1421
|
-
const endAt = existing.indexOf(end);
|
|
1422
|
-
if (starts !== 1 || ends !== 1 || endAt < from) {
|
|
1423
|
-
throw new Error(`malformed Prim hook markers for ${spec.hookName}`);
|
|
1424
|
-
}
|
|
1425
|
-
return { start: from, through: endAt + end.length };
|
|
1426
|
-
}
|
|
1427
|
-
function mergePrimBlock(hookPath, block, spec) {
|
|
1428
|
-
if (existsSync2(hookPath)) {
|
|
1429
|
-
const existing = readFileSync2(hookPath, "utf-8");
|
|
1430
|
-
const range = primBlockRange(existing, spec);
|
|
1431
|
-
if (range) {
|
|
1432
|
-
const refreshed = existing.slice(0, range.start) + block + existing.slice(range.through);
|
|
1433
|
-
if (refreshed === existing) return false;
|
|
1434
|
-
writeFileSync(hookPath, refreshed, { mode: 493 });
|
|
1435
|
-
return true;
|
|
1436
|
-
}
|
|
1437
|
-
if (containsPrimHook(existing, spec.binName)) return false;
|
|
1438
|
-
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
1439
|
-
writeFileSync(hookPath, `${existing}${separator}${block}
|
|
1440
|
-
`, { mode: 493 });
|
|
1384
|
+
function stageGitHookRuntime() {
|
|
1385
|
+
try {
|
|
1386
|
+
stageHookRuntime();
|
|
1441
1387
|
return true;
|
|
1388
|
+
} catch (error) {
|
|
1389
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1390
|
+
console.error(`[prim] git hooks stay inert until the hook runtime is staged: ${detail}`);
|
|
1391
|
+
return false;
|
|
1442
1392
|
}
|
|
1443
|
-
mkdirSync(dirname(hookPath), { recursive: true });
|
|
1444
|
-
writeFileSync(hookPath, `#!/bin/sh
|
|
1445
|
-
# ${PRIM_CREATED_MARK}
|
|
1446
|
-
|
|
1447
|
-
${block}
|
|
1448
|
-
`, { mode: 493 });
|
|
1449
|
-
return true;
|
|
1450
1393
|
}
|
|
1451
1394
|
function getGitRoot() {
|
|
1452
1395
|
const root = gitToplevel();
|
|
@@ -1473,52 +1416,6 @@ function detectHusky(gitRoot) {
|
|
|
1473
1416
|
}
|
|
1474
1417
|
return false;
|
|
1475
1418
|
}
|
|
1476
|
-
function containsPrimHook(content, binName = PRE_COMMIT.binName) {
|
|
1477
|
-
return content.includes(binName);
|
|
1478
|
-
}
|
|
1479
|
-
function installToHusky(gitRoot, spec = PRE_COMMIT) {
|
|
1480
|
-
const hookPath = resolve(gitRoot, ".husky", spec.hookName);
|
|
1481
|
-
const existed = existsSync2(hookPath);
|
|
1482
|
-
const wrote = mergePrimBlock(hookPath, huskyBlock(spec), spec);
|
|
1483
|
-
if (!wrote) {
|
|
1484
|
-
console.log(`Prim ${spec.hookName} hook is already installed in .husky/${spec.hookName}.`);
|
|
1485
|
-
} else if (existed) {
|
|
1486
|
-
console.log(`Appended prim hook block to .husky/${spec.hookName}.`);
|
|
1487
|
-
} else {
|
|
1488
|
-
console.log(`Created .husky/${spec.hookName} with prim hook block.`);
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
function installToDotGit(gitRoot, spec = PRE_COMMIT) {
|
|
1492
|
-
const hooksDir = projectHooksDir(gitRoot);
|
|
1493
|
-
const hookPath = resolve(hooksDir, spec.hookName);
|
|
1494
|
-
if (!existsSync2(hooksDir)) {
|
|
1495
|
-
mkdirSync(hooksDir, { recursive: true });
|
|
1496
|
-
}
|
|
1497
|
-
if (existsSync2(hookPath)) {
|
|
1498
|
-
const existing = readFileSync2(hookPath, "utf-8");
|
|
1499
|
-
if (containsPrimHook(existing, spec.binName)) {
|
|
1500
|
-
console.log(`Prim ${spec.hookName} hook is already installed at ${hookPath}.`);
|
|
1501
|
-
return;
|
|
1502
|
-
}
|
|
1503
|
-
console.log(`A ${spec.hookName} hook already exists at ${hookPath}.`);
|
|
1504
|
-
console.log("To replace it, run: prim hooks uninstall && prim hooks install");
|
|
1505
|
-
return;
|
|
1506
|
-
}
|
|
1507
|
-
writeFileSync(hookPath, dotGitScript(spec), { mode: 493 });
|
|
1508
|
-
console.log(`Installed ${spec.hookName} hook at ${hookPath}`);
|
|
1509
|
-
}
|
|
1510
|
-
function projectHooksDir(gitRoot) {
|
|
1511
|
-
const commonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
|
|
1512
|
-
cwd: gitRoot,
|
|
1513
|
-
encoding: "utf-8",
|
|
1514
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
1515
|
-
timeout: GIT_TIMEOUT_MS
|
|
1516
|
-
}).trim();
|
|
1517
|
-
if (commonDir === "") {
|
|
1518
|
-
throw new Error("git returned an empty common directory");
|
|
1519
|
-
}
|
|
1520
|
-
return resolve(gitRoot, commonDir, "hooks");
|
|
1521
|
-
}
|
|
1522
1419
|
var PRIM_GIT_HOOKS_DIR = join(primConfigDirectory(), "git-hooks");
|
|
1523
1420
|
function expandTilde(p) {
|
|
1524
1421
|
return p.startsWith("~/") ? join(homedir(), p.slice(2)) : p;
|
|
@@ -1557,21 +1454,17 @@ var PASSTHROUGH_HOOKS = [
|
|
|
1557
1454
|
// with a query protocol, not a lifecycle event, so a bare exec stub is wrong.)
|
|
1558
1455
|
];
|
|
1559
1456
|
function globalHookScript(spec) {
|
|
1560
|
-
const
|
|
1561
|
-
const
|
|
1562
|
-
const
|
|
1563
|
-
const managedRepoGuard = managedBlock ? ` && ! grep -Fq '${blockMarkers(spec).start.slice(0, -3)}'">>>" "$repo_hook" 2>/dev/null` : "";
|
|
1564
|
-
const beforeComments = managedBlock ? `${invocation}
|
|
1565
|
-
` : "";
|
|
1566
|
-
const afterComments = managedBlock ? "" : `${invocation}
|
|
1567
|
-
`;
|
|
1457
|
+
const preCommit = spec.hookName === PRE_COMMIT.hookName;
|
|
1458
|
+
const chainExit = preCommit ? "|| exit $?" : "|| true";
|
|
1459
|
+
const managedRepoGuard = preCommit ? "" : ` && ! grep -Fq '${blockMarkers(spec).start.slice(0, -3)}'">>>" "$repo_hook" 2>/dev/null`;
|
|
1568
1460
|
return `#!/bin/sh
|
|
1569
|
-
${
|
|
1461
|
+
${managedHookBlock(spec.hookName)}
|
|
1462
|
+
# prim global ${spec.hookName} hook (core.hooksPath) \u2014 managed by prim; do not edit.
|
|
1570
1463
|
# Install/uninstall: prim hooks install|uninstall --scope user
|
|
1571
1464
|
# Runs prim only where activated \u2014 'prim enable' (this repo) or
|
|
1572
1465
|
# 'git config --global prim.active true' (every repo). Chains to the repo's own
|
|
1573
1466
|
# hook regardless, so inactive repos are unaffected.
|
|
1574
|
-
|
|
1467
|
+
common_dir=$(git rev-parse --git-common-dir 2>/dev/null) || exit 0
|
|
1575
1468
|
repo_hook="$common_dir/hooks/${spec.hookName}"
|
|
1576
1469
|
if [ -x "$repo_hook" ] && ! grep -q '${PRIM_MANAGED_MARK}' "$repo_hook" 2>/dev/null${managedRepoGuard}; then
|
|
1577
1470
|
"$repo_hook" "$@" ${chainExit}
|
|
@@ -1596,30 +1489,12 @@ function expectedOwnedHookContent(hookName) {
|
|
|
1596
1489
|
}
|
|
1597
1490
|
return null;
|
|
1598
1491
|
}
|
|
1599
|
-
var PINNED_PACKAGE_VERSION_RE = /@primitive\.ai\/prim@[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?/gu;
|
|
1600
|
-
function normalizeOwnedGlobalHook(content, hookName) {
|
|
1601
|
-
if (hookName === PRE_COMMIT.hookName) {
|
|
1602
|
-
const lines = content.split("\n");
|
|
1603
|
-
const invocationIndexes = lines.flatMap(
|
|
1604
|
-
(line2, index) => line2.startsWith("{ ") && line2.endsWith("; } || true") ? [index] : []
|
|
1605
|
-
);
|
|
1606
|
-
if (invocationIndexes.length !== 1) return null;
|
|
1607
|
-
const invocationIndex = invocationIndexes[0];
|
|
1608
|
-
const line = lines[invocationIndex];
|
|
1609
|
-
const command = line.slice(2, -"; } || true".length);
|
|
1610
|
-
if (!commandMatchesBin(command, PRE_COMMIT.binName)) return null;
|
|
1611
|
-
lines[invocationIndex] = "{ <recognized Prim pre-commit invocation>; } || true";
|
|
1612
|
-
return lines.join("\n");
|
|
1613
|
-
}
|
|
1614
|
-
return content.replace(PINNED_PACKAGE_VERSION_RE, "@primitive.ai/prim@<version>");
|
|
1615
|
-
}
|
|
1616
1492
|
function isExpectedOwnedGlobalHook(content, hookName) {
|
|
1617
1493
|
const expected = expectedOwnedHookContent(hookName);
|
|
1618
1494
|
if (expected === null) return false;
|
|
1619
1495
|
if (content === expected) return true;
|
|
1620
|
-
const
|
|
1621
|
-
|
|
1622
|
-
return normalized !== null && normalized === normalizedExpected;
|
|
1496
|
+
const managed = HOOKS.find((spec) => spec.hookName === hookName);
|
|
1497
|
+
return managed !== void 0 && isLegacyOwnedGlobalHook(content, managed.hookName);
|
|
1623
1498
|
}
|
|
1624
1499
|
function lstatIfPresent(path) {
|
|
1625
1500
|
try {
|
|
@@ -1654,6 +1529,12 @@ function assertOwnedHooksDirectorySafeToRemove() {
|
|
|
1654
1529
|
return entries;
|
|
1655
1530
|
}
|
|
1656
1531
|
function writeOwnHooks() {
|
|
1532
|
+
if (inspectGitHookEntrypoint() !== "ready") {
|
|
1533
|
+
console.error(
|
|
1534
|
+
`[prim] did not write prim's global hooks in ${PRIM_GIT_HOOKS_DIR}: the hook runtime they run is not staged yet. Run \`prim setup\` (or an agent install such as \`prim claude install\`) first, then retry.`
|
|
1535
|
+
);
|
|
1536
|
+
return false;
|
|
1537
|
+
}
|
|
1657
1538
|
if (!existsSync2(PRIM_GIT_HOOKS_DIR)) {
|
|
1658
1539
|
mkdirSync(PRIM_GIT_HOOKS_DIR, { recursive: true });
|
|
1659
1540
|
}
|
|
@@ -1665,47 +1546,17 @@ function writeOwnHooks() {
|
|
|
1665
1546
|
for (const name of PASSTHROUGH_HOOKS) {
|
|
1666
1547
|
writeFileSync(resolve(PRIM_GIT_HOOKS_DIR, name), passThroughScript(name), { mode: 493 });
|
|
1667
1548
|
}
|
|
1549
|
+
return true;
|
|
1668
1550
|
}
|
|
1669
1551
|
function refreshOwnedGlobalHooks() {
|
|
1670
1552
|
if (!isOurHooksDir(gitConfigGet("--global"))) return false;
|
|
1671
|
-
writeOwnHooks();
|
|
1672
|
-
return true;
|
|
1553
|
+
return writeOwnHooks();
|
|
1673
1554
|
}
|
|
1674
1555
|
function appendPrimBlock(hookPath, spec) {
|
|
1675
|
-
|
|
1676
|
-
ensurePostCommitHookAtPath(hookPath);
|
|
1677
|
-
return;
|
|
1678
|
-
}
|
|
1679
|
-
if (spec.hookName === POST_REWRITE.hookName) {
|
|
1680
|
-
ensurePostRewriteHookAtPath(hookPath);
|
|
1681
|
-
return;
|
|
1682
|
-
}
|
|
1683
|
-
mergePrimBlock(hookPath, gatedBlock(spec), spec);
|
|
1556
|
+
ensureGitHookAtPath(spec.hookName, hookPath);
|
|
1684
1557
|
}
|
|
1685
1558
|
function stripPrimBlock(hookPath, spec) {
|
|
1686
|
-
|
|
1687
|
-
uninstallPostCommitHookAtPath(hookPath);
|
|
1688
|
-
return;
|
|
1689
|
-
}
|
|
1690
|
-
if (spec.hookName === POST_REWRITE.hookName) {
|
|
1691
|
-
uninstallPostRewriteHookAtPath(hookPath);
|
|
1692
|
-
return;
|
|
1693
|
-
}
|
|
1694
|
-
if (!existsSync2(hookPath)) return;
|
|
1695
|
-
const existing = readFileSync2(hookPath, "utf-8");
|
|
1696
|
-
const primCreated = existing.includes(PRIM_CREATED_MARK);
|
|
1697
|
-
const range = primBlockRange(existing, spec);
|
|
1698
|
-
if (!range) return;
|
|
1699
|
-
const out = (existing.slice(0, range.start) + existing.slice(range.through)).replace(
|
|
1700
|
-
/\n{2,}$/,
|
|
1701
|
-
"\n"
|
|
1702
|
-
);
|
|
1703
|
-
const remainder = out.replaceAll("#!/bin/sh", "").replaceAll(`# ${PRIM_CREATED_MARK}`, "").trim();
|
|
1704
|
-
if (primCreated && remainder === "") {
|
|
1705
|
-
unlinkSync(hookPath);
|
|
1706
|
-
return;
|
|
1707
|
-
}
|
|
1708
|
-
writeFileSync(hookPath, out, { mode: 493 });
|
|
1559
|
+
uninstallGitHookAtPath(spec.hookName, hookPath);
|
|
1709
1560
|
}
|
|
1710
1561
|
function planPreCommitRemoval(hookPath, options) {
|
|
1711
1562
|
if (!existsSync2(hookPath)) return { path: hookPath, action: "missing" };
|
|
@@ -1713,7 +1564,8 @@ function planPreCommitRemoval(hookPath, options) {
|
|
|
1713
1564
|
if (options.allowOwnedStandalone && isOwnedStandalonePreCommit(existing)) {
|
|
1714
1565
|
return { path: hookPath, action: "remove-owned-file" };
|
|
1715
1566
|
}
|
|
1716
|
-
|
|
1567
|
+
const { start, end } = blockMarkers(PRE_COMMIT);
|
|
1568
|
+
if (existing.includes(start) || existing.includes(end)) {
|
|
1717
1569
|
return { path: hookPath, action: "strip-block" };
|
|
1718
1570
|
}
|
|
1719
1571
|
if (existing.includes(PRE_COMMIT.binName) || existing.includes(PRIM_MANAGED_MARK) || existing.includes(PRIM_CREATED_MARK)) {
|
|
@@ -1723,79 +1575,172 @@ function planPreCommitRemoval(hookPath, options) {
|
|
|
1723
1575
|
}
|
|
1724
1576
|
return { path: hookPath, action: "leave-foreign" };
|
|
1725
1577
|
}
|
|
1726
|
-
function applyPreCommitRemoval(plan) {
|
|
1727
|
-
if (plan.action === "
|
|
1728
|
-
|
|
1729
|
-
console.log(`Removed pre-commit hook at ${plan.path}`);
|
|
1730
|
-
} else if (plan.action === "strip-block") {
|
|
1731
|
-
stripPrimBlock(plan.path, PRE_COMMIT);
|
|
1732
|
-
console.log(`Removed the Prim pre-commit block from ${plan.path}.`);
|
|
1733
|
-
} else if (plan.action === "leave-foreign") {
|
|
1578
|
+
function applyPreCommitRemoval(plan, husky) {
|
|
1579
|
+
if (plan.action === "missing") return;
|
|
1580
|
+
if (plan.action === "leave-foreign") {
|
|
1734
1581
|
console.log(`Left pre-commit hook at ${plan.path} untouched (not a Prim hook).`);
|
|
1582
|
+
return;
|
|
1735
1583
|
}
|
|
1584
|
+
const result = uninstallGitHookAtPath(PRE_COMMIT.hookName, plan.path, { husky });
|
|
1585
|
+
console.log(
|
|
1586
|
+
result.removedFile ? `Removed pre-commit hook at ${plan.path}` : `Removed the Prim pre-commit block from ${plan.path}.`
|
|
1587
|
+
);
|
|
1736
1588
|
}
|
|
1737
1589
|
function uninstallProjectHooks(gitRoot) {
|
|
1738
|
-
const
|
|
1739
|
-
|
|
1590
|
+
const configured = projectGitHookTarget(PRE_COMMIT.hookName, gitRoot);
|
|
1591
|
+
const destinations = [
|
|
1592
|
+
{
|
|
1593
|
+
path: resolve(projectHooksDir(gitRoot), PRE_COMMIT.hookName),
|
|
1594
|
+
husky: false,
|
|
1740
1595
|
allowOwnedStandalone: true
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1596
|
+
},
|
|
1597
|
+
{
|
|
1598
|
+
path: resolve(gitRoot, ".husky", PRE_COMMIT.hookName),
|
|
1599
|
+
husky: true,
|
|
1743
1600
|
allowOwnedStandalone: false
|
|
1744
|
-
}
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1601
|
+
},
|
|
1602
|
+
// A shared hooks dir is never stripped on this repository's behalf.
|
|
1603
|
+
...configured.location === "external" ? [] : [
|
|
1604
|
+
{
|
|
1605
|
+
path: configured.hookPath,
|
|
1606
|
+
husky: configured.kind === "husky_v9",
|
|
1607
|
+
allowOwnedStandalone: false
|
|
1608
|
+
}
|
|
1609
|
+
]
|
|
1610
|
+
].filter(
|
|
1611
|
+
(destination, index, all) => all.findIndex((other) => other.path === destination.path) === index
|
|
1612
|
+
);
|
|
1613
|
+
const plans = destinations.map(({ path, husky, allowOwnedStandalone }) => ({
|
|
1614
|
+
plan: planPreCommitRemoval(path, { allowOwnedStandalone }),
|
|
1615
|
+
husky
|
|
1616
|
+
}));
|
|
1617
|
+
for (const { plan, husky } of plans) applyPreCommitRemoval(plan, husky);
|
|
1618
|
+
for (const spec of [POST_COMMIT, POST_REWRITE]) {
|
|
1619
|
+
const result = uninstallProjectGitHook(spec.hookName, gitRoot);
|
|
1620
|
+
if (result.skipped === "external") {
|
|
1621
|
+
console.log(
|
|
1622
|
+
`Left ${result.path} alone: it is outside this repository, and other repositories may run it.`
|
|
1623
|
+
);
|
|
1624
|
+
} else if (!result.changed) {
|
|
1625
|
+
console.log(`No Prim ${spec.hookName} block found at ${result.path}.`);
|
|
1754
1626
|
} else if (result.removedFile) {
|
|
1755
|
-
console.log(`Removed Prim-created ${hookName} hook at ${result.path}.`);
|
|
1627
|
+
console.log(`Removed Prim-created ${spec.hookName} hook at ${result.path}.`);
|
|
1756
1628
|
} else {
|
|
1757
|
-
console.log(`Removed the Prim ${hookName} block from ${result.path}.`);
|
|
1629
|
+
console.log(`Removed the Prim ${spec.hookName} block from ${result.path}.`);
|
|
1758
1630
|
}
|
|
1759
1631
|
}
|
|
1760
1632
|
}
|
|
1761
|
-
function
|
|
1633
|
+
function globalHooksPathIsPrims() {
|
|
1634
|
+
return isOurHooksDir(gitConfigGet("--global"));
|
|
1635
|
+
}
|
|
1636
|
+
var EXIT_GLOBAL_HOOKS_NOT_INSTALLED = 3;
|
|
1637
|
+
var INSTALLED_OUTCOMES = /* @__PURE__ */ new Set([
|
|
1638
|
+
"installed",
|
|
1639
|
+
"refreshed",
|
|
1640
|
+
"added",
|
|
1641
|
+
"present"
|
|
1642
|
+
]);
|
|
1643
|
+
function planGlobalHooks(opts = {}) {
|
|
1762
1644
|
const global = gitConfigGet("--global");
|
|
1763
|
-
if (global === "") {
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1645
|
+
if (gitHooksMode({ global: true }) === "manual") return { action: "manual", global };
|
|
1646
|
+
if (isOurHooksDir(global)) return { action: "refresh" };
|
|
1647
|
+
if (global !== "") {
|
|
1648
|
+
const dir = expandTilde(global);
|
|
1649
|
+
return HOOKS.every((spec) => hasCurrentHookBlock(spec.hookName, resolve(dir, spec.hookName))) ? { action: "present_in_dir", global } : { action: "add_to_dir", global };
|
|
1650
|
+
}
|
|
1651
|
+
const system = gitConfigGet("--system");
|
|
1652
|
+
if (system !== "" && !isOurHooksDir(system)) {
|
|
1653
|
+
return opts.force ? { action: "set_pointer", overridesSystem: system } : { action: "system_declined", system };
|
|
1654
|
+
}
|
|
1655
|
+
return { action: "set_pointer" };
|
|
1656
|
+
}
|
|
1657
|
+
function planIsMachineWide(plan) {
|
|
1658
|
+
return plan.action === "set_pointer" || plan.action === "add_to_dir";
|
|
1659
|
+
}
|
|
1660
|
+
function installGlobalHooks(opts = {}) {
|
|
1661
|
+
const plan = planGlobalHooks(opts);
|
|
1662
|
+
switch (plan.action) {
|
|
1663
|
+
case "manual":
|
|
1664
|
+
console.error(
|
|
1665
|
+
isOurHooksDir(plan.global) ? `[prim] prim.gitHooks=manual: left hook files alone, but prim's global hooks in ${PRIM_GIT_HOOKS_DIR} are still active. Run \`prim hooks uninstall --scope user\` to stop them.` : "[prim] prim.gitHooks=manual: left hook files and core.hooksPath untouched. Wire prim yourself with `prim hooks snippet <hook>`."
|
|
1666
|
+
);
|
|
1667
|
+
return "manual";
|
|
1668
|
+
case "system_declined":
|
|
1669
|
+
console.error(
|
|
1670
|
+
`[prim] system core.hooksPath is set to ${plan.system}; a --global set would override it, and prim chains only to .git/hooks (not a system dir), so those hooks would stop firing. Skipped. Either place \`prim hooks snippet <hook>\` in ${plan.system} yourself, or override it with \`prim hooks install --scope user --global-hooks-path --force\`. It changes every repository's hooks: an agent must ask the user before running it.`
|
|
1671
|
+
);
|
|
1672
|
+
return "system_declined";
|
|
1673
|
+
case "refresh":
|
|
1674
|
+
stageGitHookRuntime();
|
|
1675
|
+
if (!writeOwnHooks()) return "runtime_missing";
|
|
1676
|
+
console.log(
|
|
1677
|
+
`Prim global git hooks already active (${PRIM_GIT_HOOKS_DIR}); refreshed scripts.`
|
|
1678
|
+
);
|
|
1679
|
+
return "refreshed";
|
|
1680
|
+
case "present_in_dir":
|
|
1681
|
+
if (!hookRuntimeReady()) {
|
|
1767
1682
|
console.error(
|
|
1768
|
-
`[prim]
|
|
1683
|
+
`[prim] prim's hooks are in ${plan.global}, but the hook runtime they run is not staged, so they do nothing yet. Run \`prim setup\` (or an agent install such as \`prim claude install\`), then retry.`
|
|
1769
1684
|
);
|
|
1770
|
-
return
|
|
1685
|
+
return "runtime_missing";
|
|
1771
1686
|
}
|
|
1772
|
-
console.
|
|
1773
|
-
|
|
1774
|
-
|
|
1687
|
+
console.log(`Prim hooks already present in core.hooksPath dir ${plan.global}.`);
|
|
1688
|
+
return "present";
|
|
1689
|
+
case "set_pointer":
|
|
1690
|
+
case "add_to_dir":
|
|
1691
|
+
break;
|
|
1692
|
+
}
|
|
1693
|
+
if (!hookRuntimeReady()) {
|
|
1694
|
+
console.error(
|
|
1695
|
+
`[prim] did not install prim's global hooks: the hook runtime they run is not staged yet. Run \`prim setup\` (or an agent install such as \`prim claude install\`) first, then retry.`
|
|
1696
|
+
);
|
|
1697
|
+
return "runtime_missing";
|
|
1698
|
+
}
|
|
1699
|
+
if (!opts.machineWide) {
|
|
1700
|
+
console.error(
|
|
1701
|
+
plan.action === "set_pointer" ? plan.overridesSystem !== void 0 ? `[prim] Left git's global hooks alone: setting core.hooksPath would override the system core.hooksPath ${plan.overridesSystem} (its hooks stop firing) and route every repository's hooks through prim. To do that, run \`prim hooks install --scope user --global-hooks-path --force\`. It changes every repository's hooks: an agent must ask the user before running it.` : "[prim] Left git's global hooks alone: setting core.hooksPath would route every repository's hooks through prim. `prim enable` wires each repository you activate. To route every repository through prim instead, run `prim hooks install --scope user --global-hooks-path`. It changes every repository's hooks: an agent must ask the user before running it." : `[prim] Left the hooks in ${plan.global} alone: every repository runs them, so prim edits them only with consent. Repositories that run hooks from there lack prim's hooks until you run \`prim hooks install --scope user --global-hooks-path\`. It changes every repository's hooks: an agent must ask the user before running it.`
|
|
1702
|
+
);
|
|
1703
|
+
return "not_requested";
|
|
1704
|
+
}
|
|
1705
|
+
stageGitHookRuntime();
|
|
1706
|
+
if (plan.action === "add_to_dir") {
|
|
1707
|
+
const dir = expandTilde(plan.global);
|
|
1708
|
+
for (const spec of HOOKS) {
|
|
1709
|
+
appendPrimBlock(resolve(dir, spec.hookName), spec);
|
|
1775
1710
|
}
|
|
1776
|
-
writeOwnHooks();
|
|
1777
|
-
execFileSync("git", ["config", "--global", "core.hooksPath", PRIM_GIT_HOOKS_DIR], {
|
|
1778
|
-
stdio: ["ignore", "ignore", "pipe"],
|
|
1779
|
-
timeout: GIT_TIMEOUT_MS
|
|
1780
|
-
});
|
|
1781
1711
|
console.log(
|
|
1782
|
-
`
|
|
1712
|
+
`Added prim hooks into existing core.hooksPath dir ${plan.global} (pointer unchanged).`
|
|
1783
1713
|
);
|
|
1784
|
-
return
|
|
1714
|
+
return "added";
|
|
1785
1715
|
}
|
|
1786
|
-
if (
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
}
|
|
1791
|
-
const dir = expandTilde(global);
|
|
1792
|
-
for (const spec of HOOKS) {
|
|
1793
|
-
appendPrimBlock(resolve(dir, spec.hookName), spec);
|
|
1716
|
+
if (plan.overridesSystem !== void 0) {
|
|
1717
|
+
console.error(
|
|
1718
|
+
`[prim] --force: overriding system core.hooksPath ${plan.overridesSystem}; its hooks will no longer fire (prim chains only to .git/hooks).`
|
|
1719
|
+
);
|
|
1794
1720
|
}
|
|
1721
|
+
if (!writeOwnHooks()) return "runtime_missing";
|
|
1722
|
+
execFileSync("git", ["config", "--global", "core.hooksPath", PRIM_GIT_HOOKS_DIR], {
|
|
1723
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
1724
|
+
timeout: GIT_TIMEOUT_MS
|
|
1725
|
+
});
|
|
1795
1726
|
console.log(
|
|
1796
|
-
`
|
|
1727
|
+
`Installed prim global git hooks; set core.hooksPath to ${PRIM_GIT_HOOKS_DIR}. Repos are opt-in: run \`prim enable\` in each repo to capture, or \`git config --global prim.active true\` for all.`
|
|
1728
|
+
);
|
|
1729
|
+
return "installed";
|
|
1730
|
+
}
|
|
1731
|
+
function hookRuntimeReady() {
|
|
1732
|
+
if (inspectGitHookEntrypoint() === "ready") return true;
|
|
1733
|
+
stageGitHookRuntime();
|
|
1734
|
+
return inspectGitHookEntrypoint() === "ready";
|
|
1735
|
+
}
|
|
1736
|
+
async function consentToMachineWideHooks(plan, flag, nonInteractive) {
|
|
1737
|
+
if (!planIsMachineWide(plan) || !hookRuntimeReady()) return false;
|
|
1738
|
+
if (flag) return true;
|
|
1739
|
+
if (nonInteractive || !process.stdin.isTTY) return false;
|
|
1740
|
+
return askConfirmation(
|
|
1741
|
+
plan.action === "set_pointer" ? plan.overridesSystem !== void 0 ? `Set git's global core.hooksPath so every repository's hooks run through prim? This overrides the system core.hooksPath ${plan.overridesSystem}, whose hooks stop firing.` : "Set git's global core.hooksPath so every repository's hooks run through prim?" : `Add prim's hooks to ${plan.action === "add_to_dir" ? plan.global : "the global hooks dir"}, which every repository runs?`,
|
|
1742
|
+
process.stderr
|
|
1797
1743
|
);
|
|
1798
|
-
return true;
|
|
1799
1744
|
}
|
|
1800
1745
|
function uninstallGlobalHooks() {
|
|
1801
1746
|
const global = gitConfigGet("--global");
|
|
@@ -1819,26 +1764,75 @@ function uninstallGlobalHooks() {
|
|
|
1819
1764
|
}
|
|
1820
1765
|
console.log("No prim global git hooks found.");
|
|
1821
1766
|
}
|
|
1767
|
+
function reportInstall(result, gitRoot) {
|
|
1768
|
+
const { hookName, path } = result;
|
|
1769
|
+
switch (result.outcome) {
|
|
1770
|
+
case "created":
|
|
1771
|
+
console.log(`Installed prim ${hookName} hook at ${path}.`);
|
|
1772
|
+
return;
|
|
1773
|
+
case "updated":
|
|
1774
|
+
console.log(`Updated prim ${hookName} hook at ${path}.`);
|
|
1775
|
+
return;
|
|
1776
|
+
case "unchanged":
|
|
1777
|
+
console.log(`Already current: prim ${hookName} hook at ${path}.`);
|
|
1778
|
+
return;
|
|
1779
|
+
case "external":
|
|
1780
|
+
console.error(
|
|
1781
|
+
`[prim] ${hookName}: Git runs this repository's hooks from ${path}, outside the repository, so prim left it alone. ${externalHookRemedy(hookName, gitRoot)}.`
|
|
1782
|
+
);
|
|
1783
|
+
return;
|
|
1784
|
+
case "kept":
|
|
1785
|
+
console.error(
|
|
1786
|
+
`[prim] ${hookName}: kept the working pre-v1 prim block at ${path}, outside the repository; it still captures. To upgrade it, ${externalHookRemedy(hookName, gitRoot)}.`
|
|
1787
|
+
);
|
|
1788
|
+
return;
|
|
1789
|
+
case "runtime_missing":
|
|
1790
|
+
console.error(
|
|
1791
|
+
`[prim] ${hookName}: kept the existing prim hook at ${path}; it still works, and its replacement would stay inert until the hook runtime is staged.`
|
|
1792
|
+
);
|
|
1793
|
+
return;
|
|
1794
|
+
default:
|
|
1795
|
+
console.error(`[prim] ${hookName}: left ${path} as it was (${result.outcome}).`);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1822
1798
|
function installHooks(gitRoot, target) {
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1799
|
+
let changed = false;
|
|
1800
|
+
if (target !== "effective") {
|
|
1801
|
+
const effectiveDir = resolveEffectiveGitHook(PRE_COMMIT.hookName, gitRoot).hooksDir;
|
|
1802
|
+
const chosenDir = target === "husky" ? resolve(gitRoot, ".husky") : projectHooksDir(gitRoot);
|
|
1803
|
+
if (resolve(effectiveDir) !== chosenDir && resolve(effectiveDir, "..") !== chosenDir) {
|
|
1804
|
+
console.error(
|
|
1805
|
+
`[prim] note: Git runs this repository's hooks from ${effectiveDir}; hooks in ${chosenDir} will not fire unless that directory chains to them.`
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1827
1808
|
}
|
|
1828
|
-
const
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1809
|
+
for (const spec of HOOKS) {
|
|
1810
|
+
try {
|
|
1811
|
+
const result = target === "effective" ? ensureEffectiveGitHook(spec.hookName, gitRoot, { context: "explicit" }) : ensureGitHookAtPath(
|
|
1812
|
+
spec.hookName,
|
|
1813
|
+
target === "husky" ? resolve(gitRoot, ".husky", spec.hookName) : resolve(projectHooksDir(gitRoot), spec.hookName),
|
|
1814
|
+
{ husky: target === "husky" }
|
|
1815
|
+
);
|
|
1816
|
+
reportInstall(result, gitRoot);
|
|
1817
|
+
if (spec === POST_COMMIT) changed ||= result.changed;
|
|
1818
|
+
if (spec === POST_COMMIT && result.outcome === "external") process.exitCode = 1;
|
|
1819
|
+
} catch (error) {
|
|
1820
|
+
if (spec === POST_COMMIT) throw error;
|
|
1821
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1822
|
+
console.error(
|
|
1823
|
+
`[prim] ${spec.hookName} hook coverage is degraded: ${detail}. To wire it by hand, run \`prim hooks snippet ${spec.hookName}\`.`
|
|
1824
|
+
);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
if (repoActiveFlag(gitRoot) === "true") {
|
|
1828
|
+
recordHooksWired(gitRoot, { onlyIfAbsent: !changed });
|
|
1840
1829
|
}
|
|
1841
1830
|
}
|
|
1831
|
+
function printManualNote(gitRoot) {
|
|
1832
|
+
console.error(
|
|
1833
|
+
`[prim] prim.gitHooks=manual: left the hook files in ${gitRoot} untouched. Wire each hook with \`prim hooks snippet <hook>\`; \`prim doctor\` reports what it finds.`
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1842
1836
|
function registerHooksCommands(program2) {
|
|
1843
1837
|
const hooks = program2.command("hooks").description("Manage git hooks");
|
|
1844
1838
|
hooks.command("install").description(
|
|
@@ -1851,20 +1845,47 @@ function registerHooksCommands(program2) {
|
|
|
1851
1845
|
).addOption(
|
|
1852
1846
|
new Option(
|
|
1853
1847
|
"--scope <scope>",
|
|
1854
|
-
"project (default, this repo) or user (
|
|
1848
|
+
"project (default, this repo) or user (prim's global hooks; needs --global-hooks-path or a yes at a terminal to change git's global config)"
|
|
1855
1849
|
).choices(["project", "user"])
|
|
1856
|
-
).option("--force", "with --scope user, override a system-level core.hooksPath").
|
|
1850
|
+
).option("--force", "with --scope user, override a system-level core.hooksPath").option(
|
|
1851
|
+
"--global-hooks-path",
|
|
1852
|
+
"with --scope user, change git's global hooks so every repository runs prim's hooks"
|
|
1853
|
+
).action(
|
|
1857
1854
|
async (opts, command) => {
|
|
1855
|
+
const globals = command.optsWithGlobals();
|
|
1856
|
+
const nonInteractive = isNonInteractive(globals);
|
|
1858
1857
|
if (opts.scope === "user") {
|
|
1859
|
-
|
|
1858
|
+
const plan = planGlobalHooks({ force: opts.force });
|
|
1859
|
+
const outcome = installGlobalHooks({
|
|
1860
|
+
force: opts.force,
|
|
1861
|
+
machineWide: await consentToMachineWideHooks(
|
|
1862
|
+
plan,
|
|
1863
|
+
opts.globalHooksPath,
|
|
1864
|
+
nonInteractive
|
|
1865
|
+
)
|
|
1866
|
+
});
|
|
1867
|
+
if (outcome === "runtime_missing") {
|
|
1868
|
+
process.exitCode = 1;
|
|
1869
|
+
} else if (opts.globalHooksPath && !INSTALLED_OUTCOMES.has(outcome)) {
|
|
1870
|
+
process.exitCode = EXIT_GLOBAL_HOOKS_NOT_INSTALLED;
|
|
1871
|
+
}
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
if (opts.globalHooksPath) {
|
|
1875
|
+
console.error("[prim] --global-hooks-path applies only with --scope user");
|
|
1876
|
+
process.exitCode = 2;
|
|
1860
1877
|
return;
|
|
1861
1878
|
}
|
|
1862
|
-
const globals = command.optsWithGlobals();
|
|
1863
|
-
const nonInteractive = isNonInteractive(globals);
|
|
1864
1879
|
const gitRoot = getGitRoot();
|
|
1880
|
+
if (gitHooksMode({ cwd: gitRoot }) === "manual") {
|
|
1881
|
+
printManualNote(gitRoot);
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
stageGitHookRuntime();
|
|
1865
1885
|
if (opts.target === "husky") return installHooks(gitRoot, "husky");
|
|
1866
1886
|
if (opts.target === "git-hooks") return installHooks(gitRoot, "git-hooks");
|
|
1867
|
-
|
|
1887
|
+
const effectiveDir = resolveEffectiveGitHook(PRE_COMMIT.hookName, gitRoot).hooksDir;
|
|
1888
|
+
if (resolve(effectiveDir) === projectHooksDir(gitRoot) && detectHusky(gitRoot)) {
|
|
1868
1889
|
if (globals.yes) return installHooks(gitRoot, "husky");
|
|
1869
1890
|
if (nonInteractive) {
|
|
1870
1891
|
throw new Error(
|
|
@@ -1882,10 +1903,25 @@ function registerHooksCommands(program2) {
|
|
|
1882
1903
|
} else {
|
|
1883
1904
|
console.log("Falling back to .git/hooks install.");
|
|
1884
1905
|
}
|
|
1906
|
+
return installHooks(gitRoot, "git-hooks");
|
|
1885
1907
|
}
|
|
1886
|
-
installHooks(gitRoot, "
|
|
1908
|
+
installHooks(gitRoot, "effective");
|
|
1887
1909
|
}
|
|
1888
1910
|
);
|
|
1911
|
+
hooks.command("snippet").description("Print the block that wires one prim git hook (STDOUT), to place by hand").addArgument(new Argument("<hook>", "git hook").choices(MANAGED_GIT_HOOK_NAMES)).action((hook) => {
|
|
1912
|
+
process.stdout.write(`${managedHookBlock(hook)}
|
|
1913
|
+
`);
|
|
1914
|
+
let entrypoint = "<prim config>/prim-git-hook-v1";
|
|
1915
|
+
try {
|
|
1916
|
+
entrypoint = hookRuntimePaths().gitHookEntrypoint;
|
|
1917
|
+
} catch {
|
|
1918
|
+
}
|
|
1919
|
+
process.stderr.write(
|
|
1920
|
+
`[prim] Place this block right after the shebang of your ${hook} hook (in a Husky v8 hook, right after the husky.sh line), before any exit or exec. It runs prim's ${hook} step where prim is installed and enabled, and does nothing elsewhere.${hook === "post-rewrite" ? " It re-arms stdin, so commands after it still read Git's rewrite pairs." : ""}
|
|
1921
|
+
[prim] From a hook manager, run \`${entrypoint} ${hook} "$@"\`${hook === "post-rewrite" ? " with Git's stdin" : ""} instead. Set \`git config prim.gitHooks manual\` so prim leaves your hook files alone.
|
|
1922
|
+
`
|
|
1923
|
+
);
|
|
1924
|
+
});
|
|
1889
1925
|
hooks.command("uninstall").description(
|
|
1890
1926
|
"Remove the prim git hooks (.git/hooks, or the global core.hooksPath with --scope user)"
|
|
1891
1927
|
).addOption(
|
|
@@ -1905,6 +1941,49 @@ function registerHooksCommands(program2) {
|
|
|
1905
1941
|
// src/commands/activation.ts
|
|
1906
1942
|
var CONNECT_PROMPT = "[prim] GitHub repo connection is required to enable repository-specific file attribution, Conflict Gate verification, and commit correlation. Start the GitHub App connection now?";
|
|
1907
1943
|
var GITHUB_CONNECTION_REQUIRED = "GitHub repo connection is required before using Primitive in this repository. It enables repository-specific file attribution, Conflict Gate verification, and commit correlation. Run `prim github connect` to complete it.";
|
|
1944
|
+
function wireRepositoryHooks(root) {
|
|
1945
|
+
stageGitHookRuntime();
|
|
1946
|
+
refreshOwnedGlobalHooks();
|
|
1947
|
+
const paths = {};
|
|
1948
|
+
let manual = false;
|
|
1949
|
+
let changed = false;
|
|
1950
|
+
for (const hookName of MANAGED_GIT_HOOK_NAMES) {
|
|
1951
|
+
try {
|
|
1952
|
+
const result = ensureEffectiveGitHook(hookName, root, { context: "explicit" });
|
|
1953
|
+
if (result.outcome === "external") {
|
|
1954
|
+
throw new Error(
|
|
1955
|
+
`Git runs this repository's ${hookName} hook from ${result.path}, outside the repository; ${externalHookRemedy(hookName, root)}`
|
|
1956
|
+
);
|
|
1957
|
+
}
|
|
1958
|
+
if (result.outcome === "kept") {
|
|
1959
|
+
process.stderr.write(
|
|
1960
|
+
`[prim] kept the working pre-v1 ${hookName} hook at ${result.path}, outside the repository; to upgrade it, ${externalHookRemedy(hookName, root)}
|
|
1961
|
+
`
|
|
1962
|
+
);
|
|
1963
|
+
}
|
|
1964
|
+
if (result.outcome === "runtime_missing") {
|
|
1965
|
+
process.stderr.write(
|
|
1966
|
+
`[prim] kept the working pre-v1 ${hookName} hook at ${result.path}; it is replaced once the hook runtime is staged
|
|
1967
|
+
`
|
|
1968
|
+
);
|
|
1969
|
+
}
|
|
1970
|
+
paths[hookName] = result.path;
|
|
1971
|
+
manual ||= result.outcome === "manual";
|
|
1972
|
+
if (hookName === "post-commit") changed ||= result.changed;
|
|
1973
|
+
} catch (error) {
|
|
1974
|
+
if (hookName === "post-commit") throw error;
|
|
1975
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1976
|
+
process.stderr.write(`[prim] ${hookName} hook coverage is degraded: ${detail}
|
|
1977
|
+
`);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
if (manual) {
|
|
1981
|
+
process.stderr.write(
|
|
1982
|
+
"[prim] prim.gitHooks=manual: left hook files untouched; wire them with `prim hooks snippet <hook>`\n"
|
|
1983
|
+
);
|
|
1984
|
+
}
|
|
1985
|
+
return { paths, changed };
|
|
1986
|
+
}
|
|
1908
1987
|
async function maybeConnectRepository(root, globals) {
|
|
1909
1988
|
if (isNonInteractive(globals)) return void 0;
|
|
1910
1989
|
const approved = Boolean(globals.yes) || await askConfirmation(CONNECT_PROMPT, process.stderr);
|
|
@@ -1924,6 +2003,13 @@ async function maybeConnectRepository(root, globals) {
|
|
|
1924
2003
|
}
|
|
1925
2004
|
return void 0;
|
|
1926
2005
|
}
|
|
2006
|
+
function hookPathFields(paths) {
|
|
2007
|
+
return {
|
|
2008
|
+
...paths["pre-commit"] ? { preCommitHook: paths["pre-commit"] } : {},
|
|
2009
|
+
...paths["post-commit"] ? { postCommitHook: paths["post-commit"] } : {},
|
|
2010
|
+
...paths["post-rewrite"] ? { postRewriteHook: paths["post-rewrite"] } : {}
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
1927
2013
|
async function applyActivation(active, globals = {}) {
|
|
1928
2014
|
const root = gitToplevel();
|
|
1929
2015
|
if (!root) {
|
|
@@ -1936,18 +2022,11 @@ async function applyActivation(active, globals = {}) {
|
|
|
1936
2022
|
let phase = active ? "post-commit hook coverage" : "local deactivation";
|
|
1937
2023
|
try {
|
|
1938
2024
|
let binding;
|
|
1939
|
-
let
|
|
1940
|
-
let
|
|
2025
|
+
let hookPaths = {};
|
|
2026
|
+
let hooksChanged = false;
|
|
2027
|
+
const wasActive = repoActiveFlag(root) === "true";
|
|
1941
2028
|
if (active) {
|
|
1942
|
-
|
|
1943
|
-
postCommitHook = ensureEffectivePostCommitHook(root).path;
|
|
1944
|
-
try {
|
|
1945
|
-
postRewriteHook = ensureEffectivePostRewriteHook(root).path;
|
|
1946
|
-
} catch (error) {
|
|
1947
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
1948
|
-
process.stderr.write(`[prim] post-rewrite hook coverage is degraded: ${detail}
|
|
1949
|
-
`);
|
|
1950
|
-
}
|
|
2029
|
+
({ paths: hookPaths, changed: hooksChanged } = wireRepositoryHooks(root));
|
|
1951
2030
|
phase = "GitHub repo connection";
|
|
1952
2031
|
binding = await bindRepository(root);
|
|
1953
2032
|
}
|
|
@@ -1961,8 +2040,7 @@ async function applyActivation(active, globals = {}) {
|
|
|
1961
2040
|
repo: root,
|
|
1962
2041
|
bindingStatus: binding.status,
|
|
1963
2042
|
repositoryFullName: binding.repositoryFullName,
|
|
1964
|
-
...
|
|
1965
|
-
...postRewriteHook ? { postRewriteHook } : {}
|
|
2043
|
+
...hookPathFields(hookPaths)
|
|
1966
2044
|
});
|
|
1967
2045
|
process.exitCode = 1;
|
|
1968
2046
|
return;
|
|
@@ -1977,6 +2055,10 @@ async function applyActivation(active, globals = {}) {
|
|
|
1977
2055
|
}
|
|
1978
2056
|
phase = "local activation";
|
|
1979
2057
|
setRepoActive(root, active);
|
|
2058
|
+
if (!active) clearHooksWired(root);
|
|
2059
|
+
if (active) {
|
|
2060
|
+
recordHooksWired(root, { onlyIfAbsent: wasActive && !hooksChanged });
|
|
2061
|
+
}
|
|
1980
2062
|
await daemonRequest("statusline_invalidate", {}, { timeoutMs: 250 });
|
|
1981
2063
|
process.stderr.write(`[prim] prim ${active ? "enabled" : "disabled"} in ${root}
|
|
1982
2064
|
`);
|
|
@@ -1988,8 +2070,7 @@ async function applyActivation(active, globals = {}) {
|
|
|
1988
2070
|
repositoryFullName: binding.repositoryFullName,
|
|
1989
2071
|
...binding.status === "connected" ? { repoSyncId: binding.repoSyncId } : {}
|
|
1990
2072
|
} : {},
|
|
1991
|
-
...
|
|
1992
|
-
...postRewriteHook ? { postRewriteHook } : {}
|
|
2073
|
+
...hookPathFields(hookPaths)
|
|
1993
2074
|
});
|
|
1994
2075
|
} catch (err) {
|
|
1995
2076
|
const detail = err instanceof Error ? err.message : String(err);
|
|
@@ -2142,22 +2223,32 @@ function performStatus3() {
|
|
|
2142
2223
|
};
|
|
2143
2224
|
return { user: statusFor(USER_SCOPE_PATH), project: statusFor(projectScopePath()) };
|
|
2144
2225
|
}
|
|
2226
|
+
function uncoveredWorktreeHooks(status) {
|
|
2227
|
+
if (status.user.present || status.project.present) return null;
|
|
2228
|
+
const main = mainWorktreeRoot();
|
|
2229
|
+
if (main === null) return null;
|
|
2230
|
+
const path = join2(main, ".codex", "hooks.json");
|
|
2231
|
+
return hasAnyHookRegistration(readSettings(path)) ? path : null;
|
|
2232
|
+
}
|
|
2233
|
+
function uncoveredWorktreeAdvice(path) {
|
|
2234
|
+
return `prim Codex hooks in ${path} don't load in this worktree \u2014 run \`prim codex install\`, then \`prim codex uninstall --scope project\` in that checkout`;
|
|
2235
|
+
}
|
|
2145
2236
|
function resolveScope(input) {
|
|
2146
|
-
if (input === void 0 || input === "
|
|
2147
|
-
return "project";
|
|
2148
|
-
}
|
|
2149
|
-
if (input === "user") {
|
|
2237
|
+
if (input === void 0 || input === "user") {
|
|
2150
2238
|
return "user";
|
|
2151
2239
|
}
|
|
2240
|
+
if (input === "project") {
|
|
2241
|
+
return "project";
|
|
2242
|
+
}
|
|
2152
2243
|
console.error(`[prim] unknown --scope "${input}" (expected: user or project)`);
|
|
2153
2244
|
process.exit(1);
|
|
2154
2245
|
}
|
|
2155
2246
|
var TRUST_NOTICE = "[prim] Codex requires hook trust: run `/hooks` in Codex to review and trust these hooks (or start Codex with --dangerously-bypass-hook-trust). Until trusted, the hooks will not fire.";
|
|
2156
2247
|
function registerCodexCommands(program2) {
|
|
2157
2248
|
const codex = program2.command("codex").description("Manage the prim Codex integration (capture, gate, ingest, presence)");
|
|
2158
|
-
codex.command("install").description("Register the prim hooks in Codex's hooks.json (
|
|
2249
|
+
codex.command("install").description("Register the prim hooks in Codex's hooks.json (user scope by default)").option(
|
|
2159
2250
|
"--scope <scope>",
|
|
2160
|
-
"
|
|
2251
|
+
"user (default, ~/.codex/hooks.json) or project (this checkout's .codex/hooks.json)"
|
|
2161
2252
|
).option("--force", "Replace any drifted prim hook entries").action((opts) => {
|
|
2162
2253
|
const scope = resolveScope(opts.scope);
|
|
2163
2254
|
const result = performInstall(scope, opts.force ?? false);
|
|
@@ -2171,7 +2262,7 @@ function registerCodexCommands(program2) {
|
|
|
2171
2262
|
});
|
|
2172
2263
|
codex.command("uninstall").description("Remove all prim hooks from Codex's hooks.json").option(
|
|
2173
2264
|
"--scope <scope>",
|
|
2174
|
-
"
|
|
2265
|
+
"user (default, ~/.codex/hooks.json) or project (this checkout's .codex/hooks.json)"
|
|
2175
2266
|
).action((opts) => {
|
|
2176
2267
|
const scope = resolveScope(opts.scope);
|
|
2177
2268
|
const result = performUninstall(scope);
|
|
@@ -2188,6 +2279,9 @@ function registerCodexCommands(program2) {
|
|
|
2188
2279
|
const line = (label, s) => `[prim] ${label}: gate ${mark(s.gate)} \xB7 capture ${mark(s.capture)} (${s.path})`;
|
|
2189
2280
|
console.error(`${line("user", result.user)}
|
|
2190
2281
|
${line("project", result.project)}`);
|
|
2282
|
+
const uncovered = uncoveredWorktreeHooks(result);
|
|
2283
|
+
if (uncovered !== null)
|
|
2284
|
+
console.error(`[prim] warning: ${uncoveredWorktreeAdvice(uncovered)}`);
|
|
2191
2285
|
console.log(JSON.stringify(result, null, JSON_INDENT));
|
|
2192
2286
|
});
|
|
2193
2287
|
}
|
|
@@ -2286,7 +2380,7 @@ import {
|
|
|
2286
2380
|
writeFileSync as writeFileSync2
|
|
2287
2381
|
} from "fs";
|
|
2288
2382
|
import { homedir as homedir3 } from "os";
|
|
2289
|
-
import { basename, dirname
|
|
2383
|
+
import { basename, dirname, isAbsolute, join as join4, resolve as resolve2 } from "path";
|
|
2290
2384
|
|
|
2291
2385
|
// src/lib/bounded-file.ts
|
|
2292
2386
|
import { constants, closeSync, fstatSync, lstatSync as lstatSync2, openSync, readSync } from "fs";
|
|
@@ -2398,7 +2492,7 @@ function daemonControlPaths(options = {}) {
|
|
|
2398
2492
|
};
|
|
2399
2493
|
}
|
|
2400
2494
|
function findPackageVersion(entryFile) {
|
|
2401
|
-
let dir =
|
|
2495
|
+
let dir = dirname(resolve2(entryFile));
|
|
2402
2496
|
for (let depth = 0; depth < 6; depth++) {
|
|
2403
2497
|
const packagePath = join4(dir, "package.json");
|
|
2404
2498
|
if (existsSync3(packagePath)) {
|
|
@@ -2410,7 +2504,7 @@ function findPackageVersion(entryFile) {
|
|
|
2410
2504
|
} catch {
|
|
2411
2505
|
}
|
|
2412
2506
|
}
|
|
2413
|
-
const parent =
|
|
2507
|
+
const parent = dirname(dir);
|
|
2414
2508
|
if (parent === dir) break;
|
|
2415
2509
|
dir = parent;
|
|
2416
2510
|
}
|
|
@@ -2832,7 +2926,7 @@ function readDaemonLauncherFile(path) {
|
|
|
2832
2926
|
function readUsableDaemonConfig(path) {
|
|
2833
2927
|
const config = readDaemonLauncher(path);
|
|
2834
2928
|
if (!config) return null;
|
|
2835
|
-
const manifest = readRuntimeManifest(join4(
|
|
2929
|
+
const manifest = readRuntimeManifest(join4(dirname(config.daemonPath), "manifest.json"));
|
|
2836
2930
|
try {
|
|
2837
2931
|
accessSync(config.nodePath, constants2.X_OK);
|
|
2838
2932
|
return manifest?.version === config.runtimeVersion && manifest.nodePath === config.nodePath && manifest.daemonSha256 === sha256File(config.daemonPath) ? config : null;
|
|
@@ -2926,7 +3020,7 @@ function atomicWrite2(path, content, mode, validate) {
|
|
|
2926
3020
|
}
|
|
2927
3021
|
} catch {
|
|
2928
3022
|
}
|
|
2929
|
-
mkdirSync2(
|
|
3023
|
+
mkdirSync2(dirname(path), { recursive: true, mode: RUNTIME_DIR_MODE });
|
|
2930
3024
|
const temp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
2931
3025
|
writeFileSync2(temp, content, { encoding: "utf8", mode, flag: "wx" });
|
|
2932
3026
|
try {
|
|
@@ -2962,7 +3056,7 @@ function canonicalProspectivePath(path) {
|
|
|
2962
3056
|
const suffix = [];
|
|
2963
3057
|
let ancestor = resolve2(path);
|
|
2964
3058
|
while (!existsSync3(ancestor)) {
|
|
2965
|
-
const parent =
|
|
3059
|
+
const parent = dirname(ancestor);
|
|
2966
3060
|
if (parent === ancestor) return resolve2(path);
|
|
2967
3061
|
suffix.unshift(basename(ancestor));
|
|
2968
3062
|
ancestor = parent;
|
|
@@ -3302,7 +3396,7 @@ async function ensureMacDaemonLocked(options) {
|
|
|
3302
3396
|
};
|
|
3303
3397
|
}
|
|
3304
3398
|
const servicePaths = launchdPaths(options);
|
|
3305
|
-
mkdirSync2(
|
|
3399
|
+
mkdirSync2(dirname(servicePaths.logPath), { recursive: true, mode: RUNTIME_DIR_MODE });
|
|
3306
3400
|
writeFileSync2(servicePaths.logPath, "", { mode: RUNTIME_FILE_MODE, flag: "a" });
|
|
3307
3401
|
chmodSync(servicePaths.logPath, RUNTIME_FILE_MODE);
|
|
3308
3402
|
const launcher = generateDaemonLauncher(daemonConfig);
|
|
@@ -4800,8 +4894,6 @@ var DECISION_NOT_FOUND_MESSAGE = "Decision not found";
|
|
|
4800
4894
|
var REPLACEMENT_NOT_FOUND_MESSAGE = "Replacement decision not found";
|
|
4801
4895
|
var SELF_REPLACEMENT_MESSAGE = "A decision cannot supersede itself";
|
|
4802
4896
|
var OLD_SERVER_NOT_FOUND_MESSAGE = "Not found";
|
|
4803
|
-
var FEATURE_DISABLED_MESSAGE = "feature_disabled";
|
|
4804
|
-
var FEATURE_CHECK_UNAVAILABLE_MESSAGE = "feature_check_unavailable";
|
|
4805
4897
|
var STAGE_NAME = "(?:draft|provisional|adopted|superseded|abandoned)";
|
|
4806
4898
|
var IMMUTABLE_MESSAGE = new RegExp(
|
|
4807
4899
|
`^An ${STAGE_NAME} decision is immutable \u2014 supersede it to change it$`,
|
|
@@ -4878,12 +4970,6 @@ function classifyHttpError2(error) {
|
|
|
4878
4970
|
if (error.status === 403 && message === NOT_AUTHOR_MESSAGE) {
|
|
4879
4971
|
return failure2("not_author", DECISION_LIFECYCLE_EXIT.rejected, error.status);
|
|
4880
4972
|
}
|
|
4881
|
-
if (error.status === 403 && message === FEATURE_DISABLED_MESSAGE) {
|
|
4882
|
-
return failure2("feature_disabled", DECISION_LIFECYCLE_EXIT.rejected, error.status);
|
|
4883
|
-
}
|
|
4884
|
-
if (error.status === 503 && message === FEATURE_CHECK_UNAVAILABLE_MESSAGE) {
|
|
4885
|
-
return failure2("feature_check_unavailable", DECISION_LIFECYCLE_EXIT.server, error.status);
|
|
4886
|
-
}
|
|
4887
4973
|
if (error.status === 409 && message === AMBIGUOUS_MESSAGE) {
|
|
4888
4974
|
return failure2("ambiguous_identifier", DECISION_LIFECYCLE_EXIT.rejected, error.status);
|
|
4889
4975
|
}
|
|
@@ -4943,10 +5029,6 @@ function formatFailureHuman(operation, result) {
|
|
|
4943
5029
|
return `[prim] ${verb} rejected: Decision not found.`;
|
|
4944
5030
|
case "replacement_not_found":
|
|
4945
5031
|
return "[prim] supersede rejected: replacement Decision not found.";
|
|
4946
|
-
case "feature_disabled":
|
|
4947
|
-
return `[prim] ${verb} rejected: manual lifecycle actions are not enabled for this account.`;
|
|
4948
|
-
case "feature_check_unavailable":
|
|
4949
|
-
return `[prim] ${verb} unavailable: the server could not verify feature availability; retry shortly.`;
|
|
4950
5032
|
case "unsupported_server":
|
|
4951
5033
|
return `[prim] ${verb} unavailable: this Primitive server does not support this Decision lifecycle operation; upgrade the server before retrying.`;
|
|
4952
5034
|
case "rejected":
|
|
@@ -7534,7 +7616,26 @@ async function checkRepositoryBinding() {
|
|
|
7534
7616
|
};
|
|
7535
7617
|
}
|
|
7536
7618
|
}
|
|
7537
|
-
function
|
|
7619
|
+
function managedHookRemedy(inspection) {
|
|
7620
|
+
switch (inspection.reason) {
|
|
7621
|
+
case "missing":
|
|
7622
|
+
case "missing_block":
|
|
7623
|
+
case "stale_block":
|
|
7624
|
+
case "legacy_block":
|
|
7625
|
+
case "unreachable_block":
|
|
7626
|
+
case "misplaced_block":
|
|
7627
|
+
if (inspection.location === "prim") return "run `prim enable` to refresh prim's global hooks";
|
|
7628
|
+
if (inspection.location === "external") {
|
|
7629
|
+
return `outside this repository \u2014 ${externalHookRemedy(inspection.hookName, inspection.gitRoot)}`;
|
|
7630
|
+
}
|
|
7631
|
+
return "run `prim hooks install`";
|
|
7632
|
+
case "entrypoint_missing":
|
|
7633
|
+
return "run `prim enable` to stage the hook runtime";
|
|
7634
|
+
default:
|
|
7635
|
+
return void 0;
|
|
7636
|
+
}
|
|
7637
|
+
}
|
|
7638
|
+
function classifyManagedHook(hookName, inspection, firing) {
|
|
7538
7639
|
if (inspection.covered) {
|
|
7539
7640
|
return {
|
|
7540
7641
|
name: hookName,
|
|
@@ -7542,15 +7643,78 @@ function classifyManagedHook(hookName, inspection) {
|
|
|
7542
7643
|
detail: `effective and executable \xB7 ${inspection.kind} \xB7 ${inspection.hookPath}`
|
|
7543
7644
|
};
|
|
7544
7645
|
}
|
|
7646
|
+
const reason = inspection.reason ?? "uncovered";
|
|
7647
|
+
if (inspection.mode === "manual") {
|
|
7648
|
+
if (hookName === "post-commit" && firing?.state === "fired") {
|
|
7649
|
+
return {
|
|
7650
|
+
name: hookName,
|
|
7651
|
+
status: "ok",
|
|
7652
|
+
detail: `manual (prim.gitHooks=manual) \xB7 ran for the latest commit \xB7 ${inspection.hookPath}`
|
|
7653
|
+
};
|
|
7654
|
+
}
|
|
7655
|
+
return {
|
|
7656
|
+
name: hookName,
|
|
7657
|
+
status: "warn",
|
|
7658
|
+
detail: `manual (prim.gitHooks=manual) \xB7 ${reason} \xB7 wire with \`prim hooks snippet ${hookName}\` \xB7 ${inspection.hookPath}`
|
|
7659
|
+
};
|
|
7660
|
+
}
|
|
7661
|
+
const remedy = managedHookRemedy(inspection);
|
|
7662
|
+
const stillCaptures = reason === "legacy_block" || reason === "misplaced_block";
|
|
7545
7663
|
return {
|
|
7546
7664
|
name: hookName,
|
|
7547
|
-
status: "fail",
|
|
7548
|
-
detail: `${
|
|
7665
|
+
status: hookName === "pre-commit" || stillCaptures ? "warn" : "fail",
|
|
7666
|
+
detail: `${reason}${remedy ? ` \xB7 ${remedy}` : ""} \xB7 ${inspection.hookPath}`
|
|
7549
7667
|
};
|
|
7550
7668
|
}
|
|
7551
|
-
function
|
|
7669
|
+
function minutesAgo(at, now) {
|
|
7670
|
+
const minutes = Math.max(0, Math.round((now - at) / 6e4));
|
|
7671
|
+
return minutes === 0 ? "just now" : `${String(minutes)}m ago`;
|
|
7672
|
+
}
|
|
7673
|
+
function classifyHookFiring(firing, now = Date.now()) {
|
|
7674
|
+
const name = "hook-fired";
|
|
7675
|
+
switch (firing.state) {
|
|
7676
|
+
case "inactive":
|
|
7677
|
+
return { name, status: "ok", detail: "repository not active \u2014 no post-commit run expected" };
|
|
7678
|
+
case "unverified":
|
|
7679
|
+
return {
|
|
7680
|
+
name,
|
|
7681
|
+
status: "ok",
|
|
7682
|
+
detail: "no local commit since the hooks were wired \u2014 the next commit is checked"
|
|
7683
|
+
};
|
|
7684
|
+
case "pending":
|
|
7685
|
+
return {
|
|
7686
|
+
name,
|
|
7687
|
+
status: "ok",
|
|
7688
|
+
detail: `latest commit is under ${String(POST_COMMIT_GRACE_MS / 1e3)}s old \u2014 capture may still be starting`
|
|
7689
|
+
};
|
|
7690
|
+
case "fired":
|
|
7691
|
+
return {
|
|
7692
|
+
name,
|
|
7693
|
+
status: "ok",
|
|
7694
|
+
detail: `post-commit reached prim for the latest commit (${minutesAgo(firing.firedAt, now)})`
|
|
7695
|
+
};
|
|
7696
|
+
case "not_firing":
|
|
7697
|
+
return {
|
|
7698
|
+
name,
|
|
7699
|
+
status: "fail",
|
|
7700
|
+
detail: `post-commit never reached prim for the commit made ${minutesAgo(firing.commitAt, now)}${firing.firedAt === void 0 ? "" : ` (last run ${minutesAgo(firing.firedAt, now)})`} \xB7 likely an exit/exec before prim's block, hooks turned off for that commit (HUSKY=0, a core.hooksPath override), or a git client without prim's environment \xB7 fix the cause, then commit to re-check \xB7 if prim was off for that commit (set with \`git config prim.active\` directly), run \`prim disable\` then \`prim enable\` to restart the check`
|
|
7701
|
+
};
|
|
7702
|
+
}
|
|
7703
|
+
}
|
|
7704
|
+
function inspectFiringHere() {
|
|
7705
|
+
try {
|
|
7706
|
+
const root = gitToplevel();
|
|
7707
|
+
return root ? inspectPostCommitFiring(root) : void 0;
|
|
7708
|
+
} catch {
|
|
7709
|
+
return void 0;
|
|
7710
|
+
}
|
|
7711
|
+
}
|
|
7712
|
+
function checkHookFiring(firing) {
|
|
7713
|
+
return firing ? classifyHookFiring(firing) : { name: "hook-fired", status: "warn", detail: "not a git repository" };
|
|
7714
|
+
}
|
|
7715
|
+
function checkManagedHook(hookName, firing) {
|
|
7552
7716
|
try {
|
|
7553
|
-
return classifyManagedHook(hookName,
|
|
7717
|
+
return classifyManagedHook(hookName, inspectEffectiveGitHook(hookName), firing);
|
|
7554
7718
|
} catch (error) {
|
|
7555
7719
|
const detail = error instanceof Error ? error.message : String(error);
|
|
7556
7720
|
return {
|
|
@@ -7587,9 +7751,16 @@ function classifyClaudeHooks(statuses) {
|
|
|
7587
7751
|
}
|
|
7588
7752
|
return { name: "feedback-hooks", status: "ok", detail: "complete Claude lifecycle ready" };
|
|
7589
7753
|
}
|
|
7590
|
-
function classifyCodexHooks(statuses) {
|
|
7754
|
+
function classifyCodexHooks(statuses, uncoveredWorktree = null, capturedAt) {
|
|
7591
7755
|
const installed = statuses.filter((status) => status.present);
|
|
7592
7756
|
if (installed.length === 0) {
|
|
7757
|
+
if (uncoveredWorktree !== null) {
|
|
7758
|
+
return {
|
|
7759
|
+
name: "codex-hooks",
|
|
7760
|
+
status: "warn",
|
|
7761
|
+
detail: uncoveredWorktreeAdvice(uncoveredWorktree)
|
|
7762
|
+
};
|
|
7763
|
+
}
|
|
7593
7764
|
return { name: "codex-hooks", status: "ok", detail: "not installed" };
|
|
7594
7765
|
}
|
|
7595
7766
|
if (installed.some((status) => !status.complete)) {
|
|
@@ -7601,8 +7772,8 @@ function classifyCodexHooks(statuses) {
|
|
|
7601
7772
|
}
|
|
7602
7773
|
return {
|
|
7603
7774
|
name: "codex-hooks",
|
|
7604
|
-
status: "warn",
|
|
7605
|
-
detail: "installed; Codex trust
|
|
7775
|
+
status: capturedAt === void 0 ? "warn" : "ok",
|
|
7776
|
+
detail: capturedAt === void 0 ? "installed; conversation capture unverified; Codex trust unknown \u2014 verify with `/hooks`" : `installed; conversation capture observed ${new Date(capturedAt).toISOString()}; current trust unknown`
|
|
7606
7777
|
};
|
|
7607
7778
|
}
|
|
7608
7779
|
function classifyCursorHooks(statuses) {
|
|
@@ -7654,7 +7825,13 @@ function checkAgentHooks() {
|
|
|
7654
7825
|
const checks = [];
|
|
7655
7826
|
try {
|
|
7656
7827
|
const status = performStatus3();
|
|
7657
|
-
checks.push(
|
|
7828
|
+
checks.push(
|
|
7829
|
+
classifyCodexHooks(
|
|
7830
|
+
[status.project, status.user],
|
|
7831
|
+
uncoveredWorktreeHooks(status),
|
|
7832
|
+
lastConversationCaptured(process.cwd(), "codex")
|
|
7833
|
+
)
|
|
7834
|
+
);
|
|
7658
7835
|
} catch (error) {
|
|
7659
7836
|
const detail = boundedHealthError(error instanceof Error ? error.message : String(error));
|
|
7660
7837
|
checks.push({
|
|
@@ -7806,7 +7983,7 @@ function parseMovesStatus(value) {
|
|
|
7806
7983
|
}
|
|
7807
7984
|
const status = value;
|
|
7808
7985
|
const nullableNumber = (candidate) => candidate === null || typeof candidate === "number" && Number.isFinite(candidate);
|
|
7809
|
-
if (status.captureState !== "enabled"
|
|
7986
|
+
if (status.captureState !== "enabled" || !nullableNumber(status.latestIngestAt) || !nullableNumber(status.latestClassificationAt) || !nullableNumber(status.highWaterMark) || typeof status.pendingSessionCount !== "number" || !Number.isInteger(status.pendingSessionCount) || status.pendingSessionCount < 0 || typeof status.sampled !== "boolean") {
|
|
7810
7987
|
throw new Error("moves status returned an invalid response");
|
|
7811
7988
|
}
|
|
7812
7989
|
if (status.oldestPendingAt !== void 0 && !nullableNumber(status.oldestPendingAt) || status.oldestPendingAgeMs !== void 0 && !nullableNumber(status.oldestPendingAgeMs) || typeof status.oldestPendingAgeMs === "number" && status.oldestPendingAgeMs < 0 || status.pendingCommitCorrelationCount !== void 0 && (!Number.isInteger(status.pendingCommitCorrelationCount) || status.pendingCommitCorrelationCount < 0)) {
|
|
@@ -7815,10 +7992,10 @@ function parseMovesStatus(value) {
|
|
|
7815
7992
|
return status;
|
|
7816
7993
|
}
|
|
7817
7994
|
function classifyMovesStatus(status) {
|
|
7818
|
-
const capture =
|
|
7995
|
+
const capture = {
|
|
7819
7996
|
name: "capture",
|
|
7820
|
-
status: "
|
|
7821
|
-
detail: "
|
|
7997
|
+
status: "ok",
|
|
7998
|
+
detail: "enabled; ingest endpoint durable"
|
|
7822
7999
|
};
|
|
7823
8000
|
const correlation = status.pendingCommitCorrelationCount === void 0 ? void 0 : status.pendingCommitCorrelationCount > 0 ? {
|
|
7824
8001
|
name: "commit-correlation",
|
|
@@ -7869,15 +8046,9 @@ async function checkFeedbackCapability() {
|
|
|
7869
8046
|
return { name: "feedback-api", status: "warn", detail: message.slice(0, 80) };
|
|
7870
8047
|
}
|
|
7871
8048
|
}
|
|
7872
|
-
function refreshOwnedGlobalHooksForHealth() {
|
|
7873
|
-
try {
|
|
7874
|
-
refreshOwnedGlobalHooks();
|
|
7875
|
-
} catch {
|
|
7876
|
-
}
|
|
7877
|
-
}
|
|
7878
8049
|
async function independentChecks() {
|
|
7879
|
-
refreshOwnedGlobalHooksForHealth();
|
|
7880
8050
|
const backend = await checkBackend();
|
|
8051
|
+
const firing = inspectFiringHere();
|
|
7881
8052
|
return {
|
|
7882
8053
|
before: [checkAuth()],
|
|
7883
8054
|
after: [
|
|
@@ -7886,8 +8057,10 @@ async function independentChecks() {
|
|
|
7886
8057
|
...checkAgentHooks(),
|
|
7887
8058
|
checkHookRuntime(),
|
|
7888
8059
|
await checkRepositoryBinding(),
|
|
7889
|
-
checkManagedHook("
|
|
7890
|
-
checkManagedHook("post-
|
|
8060
|
+
checkManagedHook("pre-commit"),
|
|
8061
|
+
checkManagedHook("post-commit", firing),
|
|
8062
|
+
checkManagedHook("post-rewrite"),
|
|
8063
|
+
checkHookFiring(firing),
|
|
7891
8064
|
...backend,
|
|
7892
8065
|
await checkFeedbackCapability()
|
|
7893
8066
|
]
|
|
@@ -7936,7 +8109,7 @@ import { join as join9 } from "path";
|
|
|
7936
8109
|
|
|
7937
8110
|
// src/flusher.ts
|
|
7938
8111
|
import { createReadStream, renameSync as renameSync3, statSync as statSync3, unlinkSync as unlinkSync3 } from "fs";
|
|
7939
|
-
import { dirname as
|
|
8112
|
+
import { dirname as dirname3, join as join8 } from "path";
|
|
7940
8113
|
|
|
7941
8114
|
// src/dead-letter.ts
|
|
7942
8115
|
import { createHash as createHash3, randomBytes as randomBytes4 } from "crypto";
|
|
@@ -7953,12 +8126,12 @@ import {
|
|
|
7953
8126
|
rmSync as rmSync3,
|
|
7954
8127
|
writeFileSync as writeFileSync3
|
|
7955
8128
|
} from "fs";
|
|
7956
|
-
import { dirname as
|
|
8129
|
+
import { dirname as dirname2, join as join7 } from "path";
|
|
7957
8130
|
var DIRECTORY_MODE = 448;
|
|
7958
8131
|
var FILE_MODE = 384;
|
|
7959
8132
|
var DEAD_LETTER_DIRNAME = "dead-letter";
|
|
7960
8133
|
function deadLetterDirectoryForRotation(flushingPath) {
|
|
7961
|
-
return join7(
|
|
8134
|
+
return join7(dirname2(flushingPath), DEAD_LETTER_DIRNAME);
|
|
7962
8135
|
}
|
|
7963
8136
|
function deadLetterPathForRawLine(flushingPath, rawLine) {
|
|
7964
8137
|
const quarantineId = createHash3("sha256").update(rawLine).digest("hex");
|
|
@@ -7968,7 +8141,7 @@ function ensureDeadLetterDirectory(flushingPath, persistDirectory) {
|
|
|
7968
8141
|
const directory = deadLetterDirectoryForRotation(flushingPath);
|
|
7969
8142
|
mkdirSync4(directory, { recursive: true, mode: DIRECTORY_MODE });
|
|
7970
8143
|
chmodSync2(directory, DIRECTORY_MODE);
|
|
7971
|
-
persistDirectory(
|
|
8144
|
+
persistDirectory(dirname2(directory));
|
|
7972
8145
|
return directory;
|
|
7973
8146
|
}
|
|
7974
8147
|
function persistDeadLetter(directory, path, quarantineId, record3, persistDirectory) {
|
|
@@ -8387,7 +8560,7 @@ async function flushOnce() {
|
|
|
8387
8560
|
return inspection.retainedBuckets.length > 0 ? { flushed: total, quarantined, retained: inspection.retainedBuckets } : { flushed: total, quarantined };
|
|
8388
8561
|
}
|
|
8389
8562
|
var flushInFlight;
|
|
8390
|
-
var FLUSH_LOCK_PATH = join8(
|
|
8563
|
+
var FLUSH_LOCK_PATH = join8(dirname3(JOURNAL_DIR), ".flush.lock");
|
|
8391
8564
|
var FLUSH_LOCK_TIMEOUT_MS = 250;
|
|
8392
8565
|
function isFlushLockContended(error) {
|
|
8393
8566
|
return error instanceof Error && error.message.startsWith("timed out waiting for file lock");
|
|
@@ -8711,7 +8884,7 @@ var SESSION_LABELS = {
|
|
|
8711
8884
|
};
|
|
8712
8885
|
function planSetupSteps(opts) {
|
|
8713
8886
|
const scopeArgs = opts.scope === "user" ? ["--scope", "user"] : [];
|
|
8714
|
-
const sessionArgs = opts.agent === "hermes" ? [opts.agent, "install"] : [opts.agent, "install",
|
|
8887
|
+
const sessionArgs = opts.agent === "hermes" ? [opts.agent, "install"] : [opts.agent, "install", "--scope", opts.scope];
|
|
8715
8888
|
const steps = [
|
|
8716
8889
|
{
|
|
8717
8890
|
key: "session",
|
|
@@ -8735,12 +8908,16 @@ function planSetupSteps(opts) {
|
|
|
8735
8908
|
required: true
|
|
8736
8909
|
});
|
|
8737
8910
|
}
|
|
8738
|
-
|
|
8739
|
-
key: "hooks",
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8911
|
+
if (opts.scope === "project") {
|
|
8912
|
+
steps.push({ key: "hooks", label: "Git hooks", args: ["hooks", "install"], required: true });
|
|
8913
|
+
} else if (opts.globalHooksPath) {
|
|
8914
|
+
steps.push({
|
|
8915
|
+
key: "hooks",
|
|
8916
|
+
label: "Git hooks (global)",
|
|
8917
|
+
args: ["hooks", "install", "--scope", "user", "--global-hooks-path"],
|
|
8918
|
+
required: true
|
|
8919
|
+
});
|
|
8920
|
+
}
|
|
8744
8921
|
const skillArgs = ["skill", "install", "--agent", opts.agent, ...scopeArgs];
|
|
8745
8922
|
steps.push({ key: "skill", label: "Agent skill", args: skillArgs, required: true });
|
|
8746
8923
|
steps.push({
|
|
@@ -8771,7 +8948,44 @@ function planCleanupUninstalls(agent, conflicts) {
|
|
|
8771
8948
|
if (conflicts.includes(CONFLICT_SKILL)) steps.push(["skill", "uninstall", "--agent", agent]);
|
|
8772
8949
|
return steps;
|
|
8773
8950
|
}
|
|
8774
|
-
function
|
|
8951
|
+
function preCommitRunsPrim(content) {
|
|
8952
|
+
return content.includes("# >>> prim pre-commit hook >>>") || content.includes("prim-pre-commit");
|
|
8953
|
+
}
|
|
8954
|
+
function projectHooksConflict(globalHooksActive, preCommit) {
|
|
8955
|
+
return globalHooksActive && preCommit !== void 0 && preCommitRunsPrim(preCommit);
|
|
8956
|
+
}
|
|
8957
|
+
function enableWiresRepository(root) {
|
|
8958
|
+
if (!root) return void 0;
|
|
8959
|
+
try {
|
|
8960
|
+
if (gitHooksMode({ cwd: root }) === "manual") return void 0;
|
|
8961
|
+
return resolveEffectiveGitHook("post-commit", root).location !== "external";
|
|
8962
|
+
} catch {
|
|
8963
|
+
return void 0;
|
|
8964
|
+
}
|
|
8965
|
+
}
|
|
8966
|
+
function setupGitHooksNote(plan, wiresThisRepository) {
|
|
8967
|
+
if (wiresThisRepository === true && plan.action === "add_to_dir") {
|
|
8968
|
+
return `git hooks \xB7 Git runs this repository's hooks from inside it, so \`prim enable\` wires it; other repositories may run ${plan.global}, which prim edits only with --global-hooks-path, after asking the user`;
|
|
8969
|
+
}
|
|
8970
|
+
if (wiresThisRepository === true && plan.action === "system_declined") {
|
|
8971
|
+
return `git hooks \xB7 Git runs this repository's hooks from inside it, so \`prim enable\` wires it; other repositories may run the system hooks dir ${plan.system}, which prim never edits`;
|
|
8972
|
+
}
|
|
8973
|
+
switch (plan.action) {
|
|
8974
|
+
case "refresh":
|
|
8975
|
+
return "git hooks \xB7 prim's global hooks stay active; `prim enable` refreshes them";
|
|
8976
|
+
case "present_in_dir":
|
|
8977
|
+
return `git hooks \xB7 prim's hooks are already in your global hooks dir ${plan.global}`;
|
|
8978
|
+
case "add_to_dir":
|
|
8979
|
+
return `git hooks \xB7 your global core.hooksPath (${plan.global}) runs every repository's hooks, so \`prim enable\` cannot wire this repository without it; add prim there with --global-hooks-path, after asking the user`;
|
|
8980
|
+
case "system_declined":
|
|
8981
|
+
return `git hooks \xB7 the system core.hooksPath (${plan.system}) runs every repository's hooks, so \`prim enable\` cannot wire this repository: place \`prim hooks snippet <hook>\` there, or, after asking the user, run \`prim hooks install --scope user --global-hooks-path --force\` (its hooks stop firing)`;
|
|
8982
|
+
case "manual":
|
|
8983
|
+
return "git hooks \xB7 prim.gitHooks=manual: prim writes no hook files; wire them with `prim hooks snippet <hook>`";
|
|
8984
|
+
default:
|
|
8985
|
+
return "git hooks \xB7 wired per repository by `prim enable`; git's global hooks are untouched (opt in with --global-hooks-path)";
|
|
8986
|
+
}
|
|
8987
|
+
}
|
|
8988
|
+
function detectProjectConflicts(agent, run, globalHooksActive) {
|
|
8775
8989
|
const conflicts = [];
|
|
8776
8990
|
if (agent !== "hermes") {
|
|
8777
8991
|
try {
|
|
@@ -8780,14 +8994,14 @@ function detectProjectConflicts(agent, run) {
|
|
|
8780
8994
|
} catch {
|
|
8781
8995
|
}
|
|
8782
8996
|
}
|
|
8997
|
+
let preCommit;
|
|
8783
8998
|
try {
|
|
8784
8999
|
const root = gitToplevel();
|
|
8785
|
-
const
|
|
8786
|
-
if (
|
|
8787
|
-
conflicts.push(CONFLICT_HOOKS);
|
|
8788
|
-
}
|
|
9000
|
+
const path = root && join11(root, ".git", "hooks", "pre-commit");
|
|
9001
|
+
if (path && existsSync10(path)) preCommit = readFileSync7(path, "utf-8");
|
|
8789
9002
|
} catch {
|
|
8790
9003
|
}
|
|
9004
|
+
if (projectHooksConflict(globalHooksActive, preCommit)) conflicts.push(CONFLICT_HOOKS);
|
|
8791
9005
|
try {
|
|
8792
9006
|
const parsed = JSON.parse(
|
|
8793
9007
|
run(["skill", "status", "--agent", agent, "--json"], true).stdout || "{}"
|
|
@@ -8827,8 +9041,11 @@ function registerSetupCommand(program2, dependencies = {}) {
|
|
|
8827
9041
|
"Install everything in one shot (auth, session + git hooks, daemon, skill, welcome)"
|
|
8828
9042
|
).option("--agent <agent>", "claude, codex, cursor, or hermes (auto-detected when omitted)").option(
|
|
8829
9043
|
"--scope <scope>",
|
|
8830
|
-
"user (default \u2014
|
|
9044
|
+
"user (default \u2014 agent integration and skill for every repo; git hooks per repo via enable) or project (this repo only)",
|
|
8831
9045
|
"user"
|
|
9046
|
+
).option(
|
|
9047
|
+
"--global-hooks-path",
|
|
9048
|
+
"with the default user scope, also change git's global hooks so prim's hooks run in every repository"
|
|
8832
9049
|
).option(
|
|
8833
9050
|
"--migrate",
|
|
8834
9051
|
"with the default user scope, remove any project-scoped prim config in this repo (else just warn)"
|
|
@@ -8848,6 +9065,11 @@ function registerSetupCommand(program2, dependencies = {}) {
|
|
|
8848
9065
|
(dependencies.exit ?? process.exit)(EXIT_USAGE3);
|
|
8849
9066
|
return;
|
|
8850
9067
|
}
|
|
9068
|
+
if (opts.globalHooksPath && opts.scope !== "user") {
|
|
9069
|
+
process.stderr.write("[prim] --global-hooks-path applies only with --scope user\n");
|
|
9070
|
+
(dependencies.exit ?? process.exit)(EXIT_USAGE3);
|
|
9071
|
+
return;
|
|
9072
|
+
}
|
|
8851
9073
|
const agent = agentInput;
|
|
8852
9074
|
const scope = opts.scope;
|
|
8853
9075
|
const self = process.argv[1];
|
|
@@ -8899,16 +9121,24 @@ function registerSetupCommand(program2, dependencies = {}) {
|
|
|
8899
9121
|
note("pre-authorize \xB7 writing prim allow-rule (user scope)\u2026");
|
|
8900
9122
|
results.preauth = run(["claude", "preauth", "--scope", "user"]).code === 0 ? "ok" : "skipped";
|
|
8901
9123
|
}
|
|
8902
|
-
const setupSteps = planSetupSteps({
|
|
9124
|
+
const setupSteps = planSetupSteps({
|
|
9125
|
+
agent,
|
|
9126
|
+
daemon: opts.daemon,
|
|
9127
|
+
scope,
|
|
9128
|
+
globalHooksPath: opts.globalHooksPath === true
|
|
9129
|
+
});
|
|
8903
9130
|
for (const step of setupSteps.filter(
|
|
8904
9131
|
(candidate) => candidate.key !== "enable" && candidate.key !== "health"
|
|
8905
9132
|
)) {
|
|
8906
9133
|
note(`${step.label} \xB7 installing\u2026`);
|
|
8907
9134
|
const { code } = run(step.args);
|
|
8908
|
-
results[step.key] = code === 0 ? "ok" : step.required ? "failed" : "skipped";
|
|
9135
|
+
results[step.key] = code === 0 ? "ok" : code === EXIT_GLOBAL_HOOKS_NOT_INSTALLED && step.key === "hooks" ? "skipped" : step.required ? "failed" : "skipped";
|
|
9136
|
+
}
|
|
9137
|
+
if (scope === "user" && !opts.globalHooksPath) {
|
|
9138
|
+
note(setupGitHooksNote(planGlobalHooks(), enableWiresRepository(gitToplevel())));
|
|
8909
9139
|
}
|
|
8910
9140
|
if (scope === "user") {
|
|
8911
|
-
const conflicts = detectProjectConflicts(agent, run);
|
|
9141
|
+
const conflicts = detectProjectConflicts(agent, run, globalHooksPathIsPrims());
|
|
8912
9142
|
if (conflicts.length > 0 && opts.migrate) {
|
|
8913
9143
|
note(`migrate \xB7 removing project-scoped config (${conflicts.join(", ")})\u2026`);
|
|
8914
9144
|
const ok = planCleanupUninstalls(agent, conflicts).map((args) => run(args).code === 0).every(Boolean);
|
|
@@ -8949,12 +9179,12 @@ function registerSetupCommand(program2, dependencies = {}) {
|
|
|
8949
9179
|
|
|
8950
9180
|
// src/commands/statusline.ts
|
|
8951
9181
|
import { readFileSync as readFileSync8 } from "fs";
|
|
8952
|
-
import { dirname as
|
|
9182
|
+
import { dirname as dirname4, resolve as resolve3 } from "path";
|
|
8953
9183
|
import { fileURLToPath } from "url";
|
|
8954
9184
|
var STATUSLINE_TIMEOUT_MS = 200;
|
|
8955
9185
|
function readPackageVersion() {
|
|
8956
9186
|
try {
|
|
8957
|
-
const here =
|
|
9187
|
+
const here = dirname4(fileURLToPath(import.meta.url));
|
|
8958
9188
|
const candidates = [
|
|
8959
9189
|
// Schema-v2 runtimes staged a manifest beside this standalone bundle.
|
|
8960
9190
|
// Keep resolving it while those legacy launchers transition to schema v3.
|
|
@@ -9438,14 +9668,17 @@ function startDriftHeal(options) {
|
|
|
9438
9668
|
if (!runsConfigRoot()) return false;
|
|
9439
9669
|
const primEntry = options.primEntry === void 0 ? binFile("prim") : options.primEntry;
|
|
9440
9670
|
if (!primEntry) return false;
|
|
9441
|
-
const attempt = {
|
|
9442
|
-
attemptedAt: (options.nowMs ?? Date.now)(),
|
|
9443
|
-
fromVersion: launcher.runtimeVersion,
|
|
9444
|
-
toVersion
|
|
9445
|
-
};
|
|
9446
9671
|
const claimed = withFileLockSync(
|
|
9447
9672
|
daemonDriftHealClaimPath(paths),
|
|
9448
|
-
() =>
|
|
9673
|
+
() => (
|
|
9674
|
+
// Sample after acquiring the claim: an earlier timestamp from a delayed
|
|
9675
|
+
// command must not make the previous winner look like a clock rollback.
|
|
9676
|
+
claimAttempt(daemonDriftHealMarkerPath(paths), {
|
|
9677
|
+
attemptedAt: (options.nowMs ?? Date.now)(),
|
|
9678
|
+
fromVersion: launcher.runtimeVersion,
|
|
9679
|
+
toVersion
|
|
9680
|
+
})
|
|
9681
|
+
),
|
|
9449
9682
|
{ timeoutMs: 0, sleep: claimHeld }
|
|
9450
9683
|
);
|
|
9451
9684
|
if (!claimed) return false;
|
|
@@ -9470,7 +9703,7 @@ import { readFileSync as readFileSync9 } from "fs";
|
|
|
9470
9703
|
|
|
9471
9704
|
// src/daemon/health.ts
|
|
9472
9705
|
import { chmodSync as chmodSync3, existsSync as existsSync11, mkdirSync as mkdirSync7, renameSync as renameSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
9473
|
-
import { dirname as
|
|
9706
|
+
import { dirname as dirname5, join as join12 } from "path";
|
|
9474
9707
|
var HEARTBEAT_FRESH_MS = 9e4;
|
|
9475
9708
|
var DAEMON_HEALTH_PATH = join12(primConfigDirectory(), "daemon-health.json");
|
|
9476
9709
|
|
|
@@ -9599,7 +9832,7 @@ function runStartupBackgroundWork(argv, env, work) {
|
|
|
9599
9832
|
}
|
|
9600
9833
|
|
|
9601
9834
|
// src/index.ts
|
|
9602
|
-
var __dirname =
|
|
9835
|
+
var __dirname = dirname6(fileURLToPath2(import.meta.url));
|
|
9603
9836
|
var pkg = JSON.parse(readFileSync10(resolve4(__dirname, "../package.json"), "utf-8"));
|
|
9604
9837
|
runStartupBackgroundWork(process.argv.slice(2), process.env, {
|
|
9605
9838
|
notify: () => updateNotifier({ pkg }).notify(),
|