@primitive.ai/prim 0.1.0-alpha.93 → 0.1.0-alpha.94
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 +50 -8
- package/SKILL.md +5 -2
- package/dist/{chunk-X64HTHUF.js → chunk-AXQQMX2U.js} +200 -8
- package/dist/chunk-FXF4JWK2.js +162 -0
- package/dist/{chunk-C3VAA5CG.js → chunk-IMK4JKTA.js} +1 -1
- package/dist/{chunk-36TWVNBE.js → chunk-O7YAW7G6.js} +1 -1
- package/dist/{chunk-DCJN6LZH.js → chunk-WUXFLE7X.js} +684 -365
- package/dist/hook-runtime/hooks/post-commit.js +154 -38
- package/dist/hook-runtime/hooks/post-tool-use.js +55 -5
- package/dist/hook-runtime/hooks/pre-tool-use.js +55 -5
- package/dist/hook-runtime/hooks/prim-hook.js +55 -5
- package/dist/hook-runtime/hooks/session-end.js +53 -5
- package/dist/hook-runtime/hooks/session-start.js +843 -277
- package/dist/hook-runtime/index.js +2709 -1880
- package/dist/hooks/post-commit.js +4 -0
- package/dist/hooks/post-tool-use.js +3 -3
- package/dist/hooks/pre-tool-use.js +3 -3
- package/dist/hooks/prim-hook.js +3 -3
- package/dist/hooks/session-end.js +2 -2
- package/dist/hooks/session-start.js +28 -14
- package/dist/index.js +553 -339
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,23 +2,30 @@
|
|
|
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
|
-
|
|
21
|
-
|
|
20
|
+
uninstallGitHookAtPath,
|
|
21
|
+
uninstallProjectGitHook
|
|
22
|
+
} from "./chunk-WUXFLE7X.js";
|
|
23
|
+
import {
|
|
24
|
+
POST_COMMIT_GRACE_MS,
|
|
25
|
+
clearHooksWired,
|
|
26
|
+
inspectPostCommitFiring,
|
|
27
|
+
recordHooksWired
|
|
28
|
+
} from "./chunk-FXF4JWK2.js";
|
|
22
29
|
import {
|
|
23
30
|
requireDurableIngestAcknowledgement
|
|
24
31
|
} from "./chunk-TYAHC25F.js";
|
|
@@ -78,6 +85,7 @@ import {
|
|
|
78
85
|
isMoveIngestRequest,
|
|
79
86
|
isRepoActiveForCapture,
|
|
80
87
|
isValidRepoSyncId,
|
|
88
|
+
repoActiveFlag,
|
|
81
89
|
repoSyncId,
|
|
82
90
|
repositoryBindingState,
|
|
83
91
|
setRepoActive,
|
|
@@ -101,6 +109,7 @@ import {
|
|
|
101
109
|
terminalSafeLine
|
|
102
110
|
} from "./chunk-G2VMA2QO.js";
|
|
103
111
|
import {
|
|
112
|
+
MANAGED_GIT_HOOK_NAMES,
|
|
104
113
|
assertOwnedHookRuntime,
|
|
105
114
|
atomicWrite,
|
|
106
115
|
binFile,
|
|
@@ -110,15 +119,16 @@ import {
|
|
|
110
119
|
entryHasCommand,
|
|
111
120
|
hookCommandResolutions,
|
|
112
121
|
hookRuntimePaths,
|
|
122
|
+
inspectGitHookEntrypoint,
|
|
113
123
|
inspectHookRuntime,
|
|
114
124
|
inspectHookRuntimeResolutions,
|
|
115
125
|
inspectHookRuntimeResolutions2,
|
|
116
126
|
makeRegistration,
|
|
127
|
+
managedHookBlock,
|
|
117
128
|
packageRoot,
|
|
118
129
|
packageVersion,
|
|
119
130
|
performStatus,
|
|
120
131
|
performStatus2,
|
|
121
|
-
pinnedHookCommand,
|
|
122
132
|
pinnedNpxArgs,
|
|
123
133
|
projectRoot,
|
|
124
134
|
readSettings,
|
|
@@ -129,7 +139,7 @@ import {
|
|
|
129
139
|
stableNodePath,
|
|
130
140
|
stageHookRuntime,
|
|
131
141
|
stripCommand
|
|
132
|
-
} from "./chunk-
|
|
142
|
+
} from "./chunk-AXQQMX2U.js";
|
|
133
143
|
import {
|
|
134
144
|
canonicalGitRoot,
|
|
135
145
|
canonicalRepositoryPath,
|
|
@@ -157,7 +167,7 @@ import {
|
|
|
157
167
|
|
|
158
168
|
// src/index.ts
|
|
159
169
|
import { readFileSync as readFileSync10 } from "fs";
|
|
160
|
-
import { dirname as
|
|
170
|
+
import { dirname as dirname6, resolve as resolve4 } from "path";
|
|
161
171
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
162
172
|
import { Command } from "commander";
|
|
163
173
|
import updateNotifier from "update-notifier";
|
|
@@ -1352,8 +1362,8 @@ import {
|
|
|
1352
1362
|
writeFileSync
|
|
1353
1363
|
} from "fs";
|
|
1354
1364
|
import { homedir } from "os";
|
|
1355
|
-
import {
|
|
1356
|
-
import { Option } from "commander";
|
|
1365
|
+
import { join, resolve } from "path";
|
|
1366
|
+
import { Argument, Option } from "commander";
|
|
1357
1367
|
var PRE_COMMIT = { hookName: "pre-commit", binName: "prim-pre-commit" };
|
|
1358
1368
|
var POST_COMMIT = { hookName: "post-commit", binName: "prim-post-commit" };
|
|
1359
1369
|
var POST_REWRITE = { hookName: "post-rewrite", binName: "prim-post-rewrite" };
|
|
@@ -1369,84 +1379,15 @@ var PRIM_BLOCK_START = blockMarkers(PRE_COMMIT).start;
|
|
|
1369
1379
|
var PRIM_BLOCK_END = blockMarkers(PRE_COMMIT).end;
|
|
1370
1380
|
var PRIM_MANAGED_MARK = "prim-managed-hook";
|
|
1371
1381
|
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 });
|
|
1382
|
+
function stageGitHookRuntime() {
|
|
1383
|
+
try {
|
|
1384
|
+
stageHookRuntime();
|
|
1441
1385
|
return true;
|
|
1386
|
+
} catch (error) {
|
|
1387
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1388
|
+
console.error(`[prim] git hooks stay inert until the hook runtime is staged: ${detail}`);
|
|
1389
|
+
return false;
|
|
1442
1390
|
}
|
|
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
1391
|
}
|
|
1451
1392
|
function getGitRoot() {
|
|
1452
1393
|
const root = gitToplevel();
|
|
@@ -1473,52 +1414,6 @@ function detectHusky(gitRoot) {
|
|
|
1473
1414
|
}
|
|
1474
1415
|
return false;
|
|
1475
1416
|
}
|
|
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
1417
|
var PRIM_GIT_HOOKS_DIR = join(primConfigDirectory(), "git-hooks");
|
|
1523
1418
|
function expandTilde(p) {
|
|
1524
1419
|
return p.startsWith("~/") ? join(homedir(), p.slice(2)) : p;
|
|
@@ -1557,21 +1452,17 @@ var PASSTHROUGH_HOOKS = [
|
|
|
1557
1452
|
// with a query protocol, not a lifecycle event, so a bare exec stub is wrong.)
|
|
1558
1453
|
];
|
|
1559
1454
|
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
|
-
`;
|
|
1455
|
+
const preCommit = spec.hookName === PRE_COMMIT.hookName;
|
|
1456
|
+
const chainExit = preCommit ? "|| exit $?" : "|| true";
|
|
1457
|
+
const managedRepoGuard = preCommit ? "" : ` && ! grep -Fq '${blockMarkers(spec).start.slice(0, -3)}'">>>" "$repo_hook" 2>/dev/null`;
|
|
1568
1458
|
return `#!/bin/sh
|
|
1569
|
-
${
|
|
1459
|
+
${managedHookBlock(spec.hookName)}
|
|
1460
|
+
# prim global ${spec.hookName} hook (core.hooksPath) \u2014 managed by prim; do not edit.
|
|
1570
1461
|
# Install/uninstall: prim hooks install|uninstall --scope user
|
|
1571
1462
|
# Runs prim only where activated \u2014 'prim enable' (this repo) or
|
|
1572
1463
|
# 'git config --global prim.active true' (every repo). Chains to the repo's own
|
|
1573
1464
|
# hook regardless, so inactive repos are unaffected.
|
|
1574
|
-
|
|
1465
|
+
common_dir=$(git rev-parse --git-common-dir 2>/dev/null) || exit 0
|
|
1575
1466
|
repo_hook="$common_dir/hooks/${spec.hookName}"
|
|
1576
1467
|
if [ -x "$repo_hook" ] && ! grep -q '${PRIM_MANAGED_MARK}' "$repo_hook" 2>/dev/null${managedRepoGuard}; then
|
|
1577
1468
|
"$repo_hook" "$@" ${chainExit}
|
|
@@ -1596,30 +1487,12 @@ function expectedOwnedHookContent(hookName) {
|
|
|
1596
1487
|
}
|
|
1597
1488
|
return null;
|
|
1598
1489
|
}
|
|
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
1490
|
function isExpectedOwnedGlobalHook(content, hookName) {
|
|
1617
1491
|
const expected = expectedOwnedHookContent(hookName);
|
|
1618
1492
|
if (expected === null) return false;
|
|
1619
1493
|
if (content === expected) return true;
|
|
1620
|
-
const
|
|
1621
|
-
|
|
1622
|
-
return normalized !== null && normalized === normalizedExpected;
|
|
1494
|
+
const managed = HOOKS.find((spec) => spec.hookName === hookName);
|
|
1495
|
+
return managed !== void 0 && isLegacyOwnedGlobalHook(content, managed.hookName);
|
|
1623
1496
|
}
|
|
1624
1497
|
function lstatIfPresent(path) {
|
|
1625
1498
|
try {
|
|
@@ -1654,6 +1527,12 @@ function assertOwnedHooksDirectorySafeToRemove() {
|
|
|
1654
1527
|
return entries;
|
|
1655
1528
|
}
|
|
1656
1529
|
function writeOwnHooks() {
|
|
1530
|
+
if (inspectGitHookEntrypoint() !== "ready") {
|
|
1531
|
+
console.error(
|
|
1532
|
+
`[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.`
|
|
1533
|
+
);
|
|
1534
|
+
return false;
|
|
1535
|
+
}
|
|
1657
1536
|
if (!existsSync2(PRIM_GIT_HOOKS_DIR)) {
|
|
1658
1537
|
mkdirSync(PRIM_GIT_HOOKS_DIR, { recursive: true });
|
|
1659
1538
|
}
|
|
@@ -1665,47 +1544,17 @@ function writeOwnHooks() {
|
|
|
1665
1544
|
for (const name of PASSTHROUGH_HOOKS) {
|
|
1666
1545
|
writeFileSync(resolve(PRIM_GIT_HOOKS_DIR, name), passThroughScript(name), { mode: 493 });
|
|
1667
1546
|
}
|
|
1547
|
+
return true;
|
|
1668
1548
|
}
|
|
1669
1549
|
function refreshOwnedGlobalHooks() {
|
|
1670
1550
|
if (!isOurHooksDir(gitConfigGet("--global"))) return false;
|
|
1671
|
-
writeOwnHooks();
|
|
1672
|
-
return true;
|
|
1551
|
+
return writeOwnHooks();
|
|
1673
1552
|
}
|
|
1674
1553
|
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);
|
|
1554
|
+
ensureGitHookAtPath(spec.hookName, hookPath);
|
|
1684
1555
|
}
|
|
1685
1556
|
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 });
|
|
1557
|
+
uninstallGitHookAtPath(spec.hookName, hookPath);
|
|
1709
1558
|
}
|
|
1710
1559
|
function planPreCommitRemoval(hookPath, options) {
|
|
1711
1560
|
if (!existsSync2(hookPath)) return { path: hookPath, action: "missing" };
|
|
@@ -1713,7 +1562,8 @@ function planPreCommitRemoval(hookPath, options) {
|
|
|
1713
1562
|
if (options.allowOwnedStandalone && isOwnedStandalonePreCommit(existing)) {
|
|
1714
1563
|
return { path: hookPath, action: "remove-owned-file" };
|
|
1715
1564
|
}
|
|
1716
|
-
|
|
1565
|
+
const { start, end } = blockMarkers(PRE_COMMIT);
|
|
1566
|
+
if (existing.includes(start) || existing.includes(end)) {
|
|
1717
1567
|
return { path: hookPath, action: "strip-block" };
|
|
1718
1568
|
}
|
|
1719
1569
|
if (existing.includes(PRE_COMMIT.binName) || existing.includes(PRIM_MANAGED_MARK) || existing.includes(PRIM_CREATED_MARK)) {
|
|
@@ -1723,79 +1573,172 @@ function planPreCommitRemoval(hookPath, options) {
|
|
|
1723
1573
|
}
|
|
1724
1574
|
return { path: hookPath, action: "leave-foreign" };
|
|
1725
1575
|
}
|
|
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") {
|
|
1576
|
+
function applyPreCommitRemoval(plan, husky) {
|
|
1577
|
+
if (plan.action === "missing") return;
|
|
1578
|
+
if (plan.action === "leave-foreign") {
|
|
1734
1579
|
console.log(`Left pre-commit hook at ${plan.path} untouched (not a Prim hook).`);
|
|
1580
|
+
return;
|
|
1735
1581
|
}
|
|
1582
|
+
const result = uninstallGitHookAtPath(PRE_COMMIT.hookName, plan.path, { husky });
|
|
1583
|
+
console.log(
|
|
1584
|
+
result.removedFile ? `Removed pre-commit hook at ${plan.path}` : `Removed the Prim pre-commit block from ${plan.path}.`
|
|
1585
|
+
);
|
|
1736
1586
|
}
|
|
1737
1587
|
function uninstallProjectHooks(gitRoot) {
|
|
1738
|
-
const
|
|
1739
|
-
|
|
1588
|
+
const configured = projectGitHookTarget(PRE_COMMIT.hookName, gitRoot);
|
|
1589
|
+
const destinations = [
|
|
1590
|
+
{
|
|
1591
|
+
path: resolve(projectHooksDir(gitRoot), PRE_COMMIT.hookName),
|
|
1592
|
+
husky: false,
|
|
1740
1593
|
allowOwnedStandalone: true
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1594
|
+
},
|
|
1595
|
+
{
|
|
1596
|
+
path: resolve(gitRoot, ".husky", PRE_COMMIT.hookName),
|
|
1597
|
+
husky: true,
|
|
1743
1598
|
allowOwnedStandalone: false
|
|
1744
|
-
}
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1599
|
+
},
|
|
1600
|
+
// A shared hooks dir is never stripped on this repository's behalf.
|
|
1601
|
+
...configured.location === "external" ? [] : [
|
|
1602
|
+
{
|
|
1603
|
+
path: configured.hookPath,
|
|
1604
|
+
husky: configured.kind === "husky_v9",
|
|
1605
|
+
allowOwnedStandalone: false
|
|
1606
|
+
}
|
|
1607
|
+
]
|
|
1608
|
+
].filter(
|
|
1609
|
+
(destination, index, all) => all.findIndex((other) => other.path === destination.path) === index
|
|
1610
|
+
);
|
|
1611
|
+
const plans = destinations.map(({ path, husky, allowOwnedStandalone }) => ({
|
|
1612
|
+
plan: planPreCommitRemoval(path, { allowOwnedStandalone }),
|
|
1613
|
+
husky
|
|
1614
|
+
}));
|
|
1615
|
+
for (const { plan, husky } of plans) applyPreCommitRemoval(plan, husky);
|
|
1616
|
+
for (const spec of [POST_COMMIT, POST_REWRITE]) {
|
|
1617
|
+
const result = uninstallProjectGitHook(spec.hookName, gitRoot);
|
|
1618
|
+
if (result.skipped === "external") {
|
|
1619
|
+
console.log(
|
|
1620
|
+
`Left ${result.path} alone: it is outside this repository, and other repositories may run it.`
|
|
1621
|
+
);
|
|
1622
|
+
} else if (!result.changed) {
|
|
1623
|
+
console.log(`No Prim ${spec.hookName} block found at ${result.path}.`);
|
|
1754
1624
|
} else if (result.removedFile) {
|
|
1755
|
-
console.log(`Removed Prim-created ${hookName} hook at ${result.path}.`);
|
|
1625
|
+
console.log(`Removed Prim-created ${spec.hookName} hook at ${result.path}.`);
|
|
1756
1626
|
} else {
|
|
1757
|
-
console.log(`Removed the Prim ${hookName} block from ${result.path}.`);
|
|
1627
|
+
console.log(`Removed the Prim ${spec.hookName} block from ${result.path}.`);
|
|
1758
1628
|
}
|
|
1759
1629
|
}
|
|
1760
1630
|
}
|
|
1761
|
-
function
|
|
1631
|
+
function globalHooksPathIsPrims() {
|
|
1632
|
+
return isOurHooksDir(gitConfigGet("--global"));
|
|
1633
|
+
}
|
|
1634
|
+
var EXIT_GLOBAL_HOOKS_NOT_INSTALLED = 3;
|
|
1635
|
+
var INSTALLED_OUTCOMES = /* @__PURE__ */ new Set([
|
|
1636
|
+
"installed",
|
|
1637
|
+
"refreshed",
|
|
1638
|
+
"added",
|
|
1639
|
+
"present"
|
|
1640
|
+
]);
|
|
1641
|
+
function planGlobalHooks(opts = {}) {
|
|
1762
1642
|
const global = gitConfigGet("--global");
|
|
1763
|
-
if (global === "") {
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1643
|
+
if (gitHooksMode({ global: true }) === "manual") return { action: "manual", global };
|
|
1644
|
+
if (isOurHooksDir(global)) return { action: "refresh" };
|
|
1645
|
+
if (global !== "") {
|
|
1646
|
+
const dir = expandTilde(global);
|
|
1647
|
+
return HOOKS.every((spec) => hasCurrentHookBlock(spec.hookName, resolve(dir, spec.hookName))) ? { action: "present_in_dir", global } : { action: "add_to_dir", global };
|
|
1648
|
+
}
|
|
1649
|
+
const system = gitConfigGet("--system");
|
|
1650
|
+
if (system !== "" && !isOurHooksDir(system)) {
|
|
1651
|
+
return opts.force ? { action: "set_pointer", overridesSystem: system } : { action: "system_declined", system };
|
|
1652
|
+
}
|
|
1653
|
+
return { action: "set_pointer" };
|
|
1654
|
+
}
|
|
1655
|
+
function planIsMachineWide(plan) {
|
|
1656
|
+
return plan.action === "set_pointer" || plan.action === "add_to_dir";
|
|
1657
|
+
}
|
|
1658
|
+
function installGlobalHooks(opts = {}) {
|
|
1659
|
+
const plan = planGlobalHooks(opts);
|
|
1660
|
+
switch (plan.action) {
|
|
1661
|
+
case "manual":
|
|
1662
|
+
console.error(
|
|
1663
|
+
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>`."
|
|
1664
|
+
);
|
|
1665
|
+
return "manual";
|
|
1666
|
+
case "system_declined":
|
|
1667
|
+
console.error(
|
|
1668
|
+
`[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.`
|
|
1669
|
+
);
|
|
1670
|
+
return "system_declined";
|
|
1671
|
+
case "refresh":
|
|
1672
|
+
stageGitHookRuntime();
|
|
1673
|
+
if (!writeOwnHooks()) return "runtime_missing";
|
|
1674
|
+
console.log(
|
|
1675
|
+
`Prim global git hooks already active (${PRIM_GIT_HOOKS_DIR}); refreshed scripts.`
|
|
1676
|
+
);
|
|
1677
|
+
return "refreshed";
|
|
1678
|
+
case "present_in_dir":
|
|
1679
|
+
if (!hookRuntimeReady()) {
|
|
1767
1680
|
console.error(
|
|
1768
|
-
`[prim]
|
|
1681
|
+
`[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
1682
|
);
|
|
1770
|
-
return
|
|
1683
|
+
return "runtime_missing";
|
|
1771
1684
|
}
|
|
1772
|
-
console.
|
|
1773
|
-
|
|
1774
|
-
|
|
1685
|
+
console.log(`Prim hooks already present in core.hooksPath dir ${plan.global}.`);
|
|
1686
|
+
return "present";
|
|
1687
|
+
case "set_pointer":
|
|
1688
|
+
case "add_to_dir":
|
|
1689
|
+
break;
|
|
1690
|
+
}
|
|
1691
|
+
if (!hookRuntimeReady()) {
|
|
1692
|
+
console.error(
|
|
1693
|
+
`[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.`
|
|
1694
|
+
);
|
|
1695
|
+
return "runtime_missing";
|
|
1696
|
+
}
|
|
1697
|
+
if (!opts.machineWide) {
|
|
1698
|
+
console.error(
|
|
1699
|
+
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.`
|
|
1700
|
+
);
|
|
1701
|
+
return "not_requested";
|
|
1702
|
+
}
|
|
1703
|
+
stageGitHookRuntime();
|
|
1704
|
+
if (plan.action === "add_to_dir") {
|
|
1705
|
+
const dir = expandTilde(plan.global);
|
|
1706
|
+
for (const spec of HOOKS) {
|
|
1707
|
+
appendPrimBlock(resolve(dir, spec.hookName), spec);
|
|
1775
1708
|
}
|
|
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
1709
|
console.log(
|
|
1782
|
-
`
|
|
1710
|
+
`Added prim hooks into existing core.hooksPath dir ${plan.global} (pointer unchanged).`
|
|
1783
1711
|
);
|
|
1784
|
-
return
|
|
1712
|
+
return "added";
|
|
1785
1713
|
}
|
|
1786
|
-
if (
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
}
|
|
1791
|
-
const dir = expandTilde(global);
|
|
1792
|
-
for (const spec of HOOKS) {
|
|
1793
|
-
appendPrimBlock(resolve(dir, spec.hookName), spec);
|
|
1714
|
+
if (plan.overridesSystem !== void 0) {
|
|
1715
|
+
console.error(
|
|
1716
|
+
`[prim] --force: overriding system core.hooksPath ${plan.overridesSystem}; its hooks will no longer fire (prim chains only to .git/hooks).`
|
|
1717
|
+
);
|
|
1794
1718
|
}
|
|
1719
|
+
if (!writeOwnHooks()) return "runtime_missing";
|
|
1720
|
+
execFileSync("git", ["config", "--global", "core.hooksPath", PRIM_GIT_HOOKS_DIR], {
|
|
1721
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
1722
|
+
timeout: GIT_TIMEOUT_MS
|
|
1723
|
+
});
|
|
1795
1724
|
console.log(
|
|
1796
|
-
`
|
|
1725
|
+
`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.`
|
|
1726
|
+
);
|
|
1727
|
+
return "installed";
|
|
1728
|
+
}
|
|
1729
|
+
function hookRuntimeReady() {
|
|
1730
|
+
if (inspectGitHookEntrypoint() === "ready") return true;
|
|
1731
|
+
stageGitHookRuntime();
|
|
1732
|
+
return inspectGitHookEntrypoint() === "ready";
|
|
1733
|
+
}
|
|
1734
|
+
async function consentToMachineWideHooks(plan, flag, nonInteractive) {
|
|
1735
|
+
if (!planIsMachineWide(plan) || !hookRuntimeReady()) return false;
|
|
1736
|
+
if (flag) return true;
|
|
1737
|
+
if (nonInteractive || !process.stdin.isTTY) return false;
|
|
1738
|
+
return askConfirmation(
|
|
1739
|
+
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?`,
|
|
1740
|
+
process.stderr
|
|
1797
1741
|
);
|
|
1798
|
-
return true;
|
|
1799
1742
|
}
|
|
1800
1743
|
function uninstallGlobalHooks() {
|
|
1801
1744
|
const global = gitConfigGet("--global");
|
|
@@ -1819,26 +1762,75 @@ function uninstallGlobalHooks() {
|
|
|
1819
1762
|
}
|
|
1820
1763
|
console.log("No prim global git hooks found.");
|
|
1821
1764
|
}
|
|
1765
|
+
function reportInstall(result, gitRoot) {
|
|
1766
|
+
const { hookName, path } = result;
|
|
1767
|
+
switch (result.outcome) {
|
|
1768
|
+
case "created":
|
|
1769
|
+
console.log(`Installed prim ${hookName} hook at ${path}.`);
|
|
1770
|
+
return;
|
|
1771
|
+
case "updated":
|
|
1772
|
+
console.log(`Updated prim ${hookName} hook at ${path}.`);
|
|
1773
|
+
return;
|
|
1774
|
+
case "unchanged":
|
|
1775
|
+
console.log(`Already current: prim ${hookName} hook at ${path}.`);
|
|
1776
|
+
return;
|
|
1777
|
+
case "external":
|
|
1778
|
+
console.error(
|
|
1779
|
+
`[prim] ${hookName}: Git runs this repository's hooks from ${path}, outside the repository, so prim left it alone. ${externalHookRemedy(hookName, gitRoot)}.`
|
|
1780
|
+
);
|
|
1781
|
+
return;
|
|
1782
|
+
case "kept":
|
|
1783
|
+
console.error(
|
|
1784
|
+
`[prim] ${hookName}: kept the working pre-v1 prim block at ${path}, outside the repository; it still captures. To upgrade it, ${externalHookRemedy(hookName, gitRoot)}.`
|
|
1785
|
+
);
|
|
1786
|
+
return;
|
|
1787
|
+
case "runtime_missing":
|
|
1788
|
+
console.error(
|
|
1789
|
+
`[prim] ${hookName}: kept the existing prim hook at ${path}; it still works, and its replacement would stay inert until the hook runtime is staged.`
|
|
1790
|
+
);
|
|
1791
|
+
return;
|
|
1792
|
+
default:
|
|
1793
|
+
console.error(`[prim] ${hookName}: left ${path} as it was (${result.outcome}).`);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1822
1796
|
function installHooks(gitRoot, target) {
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1797
|
+
let changed = false;
|
|
1798
|
+
if (target !== "effective") {
|
|
1799
|
+
const effectiveDir = resolveEffectiveGitHook(PRE_COMMIT.hookName, gitRoot).hooksDir;
|
|
1800
|
+
const chosenDir = target === "husky" ? resolve(gitRoot, ".husky") : projectHooksDir(gitRoot);
|
|
1801
|
+
if (resolve(effectiveDir) !== chosenDir && resolve(effectiveDir, "..") !== chosenDir) {
|
|
1802
|
+
console.error(
|
|
1803
|
+
`[prim] note: Git runs this repository's hooks from ${effectiveDir}; hooks in ${chosenDir} will not fire unless that directory chains to them.`
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1827
1806
|
}
|
|
1828
|
-
const
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1807
|
+
for (const spec of HOOKS) {
|
|
1808
|
+
try {
|
|
1809
|
+
const result = target === "effective" ? ensureEffectiveGitHook(spec.hookName, gitRoot, { context: "explicit" }) : ensureGitHookAtPath(
|
|
1810
|
+
spec.hookName,
|
|
1811
|
+
target === "husky" ? resolve(gitRoot, ".husky", spec.hookName) : resolve(projectHooksDir(gitRoot), spec.hookName),
|
|
1812
|
+
{ husky: target === "husky" }
|
|
1813
|
+
);
|
|
1814
|
+
reportInstall(result, gitRoot);
|
|
1815
|
+
if (spec === POST_COMMIT) changed ||= result.changed;
|
|
1816
|
+
if (spec === POST_COMMIT && result.outcome === "external") process.exitCode = 1;
|
|
1817
|
+
} catch (error) {
|
|
1818
|
+
if (spec === POST_COMMIT) throw error;
|
|
1819
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1820
|
+
console.error(
|
|
1821
|
+
`[prim] ${spec.hookName} hook coverage is degraded: ${detail}. To wire it by hand, run \`prim hooks snippet ${spec.hookName}\`.`
|
|
1822
|
+
);
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
if (repoActiveFlag(gitRoot) === "true") {
|
|
1826
|
+
recordHooksWired(gitRoot, { onlyIfAbsent: !changed });
|
|
1840
1827
|
}
|
|
1841
1828
|
}
|
|
1829
|
+
function printManualNote(gitRoot) {
|
|
1830
|
+
console.error(
|
|
1831
|
+
`[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.`
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1842
1834
|
function registerHooksCommands(program2) {
|
|
1843
1835
|
const hooks = program2.command("hooks").description("Manage git hooks");
|
|
1844
1836
|
hooks.command("install").description(
|
|
@@ -1851,20 +1843,47 @@ function registerHooksCommands(program2) {
|
|
|
1851
1843
|
).addOption(
|
|
1852
1844
|
new Option(
|
|
1853
1845
|
"--scope <scope>",
|
|
1854
|
-
"project (default, this repo) or user (
|
|
1846
|
+
"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
1847
|
).choices(["project", "user"])
|
|
1856
|
-
).option("--force", "with --scope user, override a system-level core.hooksPath").
|
|
1848
|
+
).option("--force", "with --scope user, override a system-level core.hooksPath").option(
|
|
1849
|
+
"--global-hooks-path",
|
|
1850
|
+
"with --scope user, change git's global hooks so every repository runs prim's hooks"
|
|
1851
|
+
).action(
|
|
1857
1852
|
async (opts, command) => {
|
|
1853
|
+
const globals = command.optsWithGlobals();
|
|
1854
|
+
const nonInteractive = isNonInteractive(globals);
|
|
1858
1855
|
if (opts.scope === "user") {
|
|
1859
|
-
|
|
1856
|
+
const plan = planGlobalHooks({ force: opts.force });
|
|
1857
|
+
const outcome = installGlobalHooks({
|
|
1858
|
+
force: opts.force,
|
|
1859
|
+
machineWide: await consentToMachineWideHooks(
|
|
1860
|
+
plan,
|
|
1861
|
+
opts.globalHooksPath,
|
|
1862
|
+
nonInteractive
|
|
1863
|
+
)
|
|
1864
|
+
});
|
|
1865
|
+
if (outcome === "runtime_missing") {
|
|
1866
|
+
process.exitCode = 1;
|
|
1867
|
+
} else if (opts.globalHooksPath && !INSTALLED_OUTCOMES.has(outcome)) {
|
|
1868
|
+
process.exitCode = EXIT_GLOBAL_HOOKS_NOT_INSTALLED;
|
|
1869
|
+
}
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
if (opts.globalHooksPath) {
|
|
1873
|
+
console.error("[prim] --global-hooks-path applies only with --scope user");
|
|
1874
|
+
process.exitCode = 2;
|
|
1860
1875
|
return;
|
|
1861
1876
|
}
|
|
1862
|
-
const globals = command.optsWithGlobals();
|
|
1863
|
-
const nonInteractive = isNonInteractive(globals);
|
|
1864
1877
|
const gitRoot = getGitRoot();
|
|
1878
|
+
if (gitHooksMode({ cwd: gitRoot }) === "manual") {
|
|
1879
|
+
printManualNote(gitRoot);
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
stageGitHookRuntime();
|
|
1865
1883
|
if (opts.target === "husky") return installHooks(gitRoot, "husky");
|
|
1866
1884
|
if (opts.target === "git-hooks") return installHooks(gitRoot, "git-hooks");
|
|
1867
|
-
|
|
1885
|
+
const effectiveDir = resolveEffectiveGitHook(PRE_COMMIT.hookName, gitRoot).hooksDir;
|
|
1886
|
+
if (resolve(effectiveDir) === projectHooksDir(gitRoot) && detectHusky(gitRoot)) {
|
|
1868
1887
|
if (globals.yes) return installHooks(gitRoot, "husky");
|
|
1869
1888
|
if (nonInteractive) {
|
|
1870
1889
|
throw new Error(
|
|
@@ -1882,10 +1901,25 @@ function registerHooksCommands(program2) {
|
|
|
1882
1901
|
} else {
|
|
1883
1902
|
console.log("Falling back to .git/hooks install.");
|
|
1884
1903
|
}
|
|
1904
|
+
return installHooks(gitRoot, "git-hooks");
|
|
1885
1905
|
}
|
|
1886
|
-
installHooks(gitRoot, "
|
|
1906
|
+
installHooks(gitRoot, "effective");
|
|
1887
1907
|
}
|
|
1888
1908
|
);
|
|
1909
|
+
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) => {
|
|
1910
|
+
process.stdout.write(`${managedHookBlock(hook)}
|
|
1911
|
+
`);
|
|
1912
|
+
let entrypoint = "<prim config>/prim-git-hook-v1";
|
|
1913
|
+
try {
|
|
1914
|
+
entrypoint = hookRuntimePaths().gitHookEntrypoint;
|
|
1915
|
+
} catch {
|
|
1916
|
+
}
|
|
1917
|
+
process.stderr.write(
|
|
1918
|
+
`[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." : ""}
|
|
1919
|
+
[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.
|
|
1920
|
+
`
|
|
1921
|
+
);
|
|
1922
|
+
});
|
|
1889
1923
|
hooks.command("uninstall").description(
|
|
1890
1924
|
"Remove the prim git hooks (.git/hooks, or the global core.hooksPath with --scope user)"
|
|
1891
1925
|
).addOption(
|
|
@@ -1905,6 +1939,49 @@ function registerHooksCommands(program2) {
|
|
|
1905
1939
|
// src/commands/activation.ts
|
|
1906
1940
|
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
1941
|
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.";
|
|
1942
|
+
function wireRepositoryHooks(root) {
|
|
1943
|
+
stageGitHookRuntime();
|
|
1944
|
+
refreshOwnedGlobalHooks();
|
|
1945
|
+
const paths = {};
|
|
1946
|
+
let manual = false;
|
|
1947
|
+
let changed = false;
|
|
1948
|
+
for (const hookName of MANAGED_GIT_HOOK_NAMES) {
|
|
1949
|
+
try {
|
|
1950
|
+
const result = ensureEffectiveGitHook(hookName, root, { context: "explicit" });
|
|
1951
|
+
if (result.outcome === "external") {
|
|
1952
|
+
throw new Error(
|
|
1953
|
+
`Git runs this repository's ${hookName} hook from ${result.path}, outside the repository; ${externalHookRemedy(hookName, root)}`
|
|
1954
|
+
);
|
|
1955
|
+
}
|
|
1956
|
+
if (result.outcome === "kept") {
|
|
1957
|
+
process.stderr.write(
|
|
1958
|
+
`[prim] kept the working pre-v1 ${hookName} hook at ${result.path}, outside the repository; to upgrade it, ${externalHookRemedy(hookName, root)}
|
|
1959
|
+
`
|
|
1960
|
+
);
|
|
1961
|
+
}
|
|
1962
|
+
if (result.outcome === "runtime_missing") {
|
|
1963
|
+
process.stderr.write(
|
|
1964
|
+
`[prim] kept the working pre-v1 ${hookName} hook at ${result.path}; it is replaced once the hook runtime is staged
|
|
1965
|
+
`
|
|
1966
|
+
);
|
|
1967
|
+
}
|
|
1968
|
+
paths[hookName] = result.path;
|
|
1969
|
+
manual ||= result.outcome === "manual";
|
|
1970
|
+
if (hookName === "post-commit") changed ||= result.changed;
|
|
1971
|
+
} catch (error) {
|
|
1972
|
+
if (hookName === "post-commit") throw error;
|
|
1973
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1974
|
+
process.stderr.write(`[prim] ${hookName} hook coverage is degraded: ${detail}
|
|
1975
|
+
`);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
if (manual) {
|
|
1979
|
+
process.stderr.write(
|
|
1980
|
+
"[prim] prim.gitHooks=manual: left hook files untouched; wire them with `prim hooks snippet <hook>`\n"
|
|
1981
|
+
);
|
|
1982
|
+
}
|
|
1983
|
+
return { paths, changed };
|
|
1984
|
+
}
|
|
1908
1985
|
async function maybeConnectRepository(root, globals) {
|
|
1909
1986
|
if (isNonInteractive(globals)) return void 0;
|
|
1910
1987
|
const approved = Boolean(globals.yes) || await askConfirmation(CONNECT_PROMPT, process.stderr);
|
|
@@ -1924,6 +2001,13 @@ async function maybeConnectRepository(root, globals) {
|
|
|
1924
2001
|
}
|
|
1925
2002
|
return void 0;
|
|
1926
2003
|
}
|
|
2004
|
+
function hookPathFields(paths) {
|
|
2005
|
+
return {
|
|
2006
|
+
...paths["pre-commit"] ? { preCommitHook: paths["pre-commit"] } : {},
|
|
2007
|
+
...paths["post-commit"] ? { postCommitHook: paths["post-commit"] } : {},
|
|
2008
|
+
...paths["post-rewrite"] ? { postRewriteHook: paths["post-rewrite"] } : {}
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
1927
2011
|
async function applyActivation(active, globals = {}) {
|
|
1928
2012
|
const root = gitToplevel();
|
|
1929
2013
|
if (!root) {
|
|
@@ -1936,18 +2020,11 @@ async function applyActivation(active, globals = {}) {
|
|
|
1936
2020
|
let phase = active ? "post-commit hook coverage" : "local deactivation";
|
|
1937
2021
|
try {
|
|
1938
2022
|
let binding;
|
|
1939
|
-
let
|
|
1940
|
-
let
|
|
2023
|
+
let hookPaths = {};
|
|
2024
|
+
let hooksChanged = false;
|
|
2025
|
+
const wasActive = repoActiveFlag(root) === "true";
|
|
1941
2026
|
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
|
-
}
|
|
2027
|
+
({ paths: hookPaths, changed: hooksChanged } = wireRepositoryHooks(root));
|
|
1951
2028
|
phase = "GitHub repo connection";
|
|
1952
2029
|
binding = await bindRepository(root);
|
|
1953
2030
|
}
|
|
@@ -1961,8 +2038,7 @@ async function applyActivation(active, globals = {}) {
|
|
|
1961
2038
|
repo: root,
|
|
1962
2039
|
bindingStatus: binding.status,
|
|
1963
2040
|
repositoryFullName: binding.repositoryFullName,
|
|
1964
|
-
...
|
|
1965
|
-
...postRewriteHook ? { postRewriteHook } : {}
|
|
2041
|
+
...hookPathFields(hookPaths)
|
|
1966
2042
|
});
|
|
1967
2043
|
process.exitCode = 1;
|
|
1968
2044
|
return;
|
|
@@ -1977,6 +2053,10 @@ async function applyActivation(active, globals = {}) {
|
|
|
1977
2053
|
}
|
|
1978
2054
|
phase = "local activation";
|
|
1979
2055
|
setRepoActive(root, active);
|
|
2056
|
+
if (!active) clearHooksWired(root);
|
|
2057
|
+
if (active) {
|
|
2058
|
+
recordHooksWired(root, { onlyIfAbsent: wasActive && !hooksChanged });
|
|
2059
|
+
}
|
|
1980
2060
|
await daemonRequest("statusline_invalidate", {}, { timeoutMs: 250 });
|
|
1981
2061
|
process.stderr.write(`[prim] prim ${active ? "enabled" : "disabled"} in ${root}
|
|
1982
2062
|
`);
|
|
@@ -1988,8 +2068,7 @@ async function applyActivation(active, globals = {}) {
|
|
|
1988
2068
|
repositoryFullName: binding.repositoryFullName,
|
|
1989
2069
|
...binding.status === "connected" ? { repoSyncId: binding.repoSyncId } : {}
|
|
1990
2070
|
} : {},
|
|
1991
|
-
...
|
|
1992
|
-
...postRewriteHook ? { postRewriteHook } : {}
|
|
2071
|
+
...hookPathFields(hookPaths)
|
|
1993
2072
|
});
|
|
1994
2073
|
} catch (err) {
|
|
1995
2074
|
const detail = err instanceof Error ? err.message : String(err);
|
|
@@ -2286,7 +2365,7 @@ import {
|
|
|
2286
2365
|
writeFileSync as writeFileSync2
|
|
2287
2366
|
} from "fs";
|
|
2288
2367
|
import { homedir as homedir3 } from "os";
|
|
2289
|
-
import { basename, dirname
|
|
2368
|
+
import { basename, dirname, isAbsolute, join as join4, resolve as resolve2 } from "path";
|
|
2290
2369
|
|
|
2291
2370
|
// src/lib/bounded-file.ts
|
|
2292
2371
|
import { constants, closeSync, fstatSync, lstatSync as lstatSync2, openSync, readSync } from "fs";
|
|
@@ -2398,7 +2477,7 @@ function daemonControlPaths(options = {}) {
|
|
|
2398
2477
|
};
|
|
2399
2478
|
}
|
|
2400
2479
|
function findPackageVersion(entryFile) {
|
|
2401
|
-
let dir =
|
|
2480
|
+
let dir = dirname(resolve2(entryFile));
|
|
2402
2481
|
for (let depth = 0; depth < 6; depth++) {
|
|
2403
2482
|
const packagePath = join4(dir, "package.json");
|
|
2404
2483
|
if (existsSync3(packagePath)) {
|
|
@@ -2410,7 +2489,7 @@ function findPackageVersion(entryFile) {
|
|
|
2410
2489
|
} catch {
|
|
2411
2490
|
}
|
|
2412
2491
|
}
|
|
2413
|
-
const parent =
|
|
2492
|
+
const parent = dirname(dir);
|
|
2414
2493
|
if (parent === dir) break;
|
|
2415
2494
|
dir = parent;
|
|
2416
2495
|
}
|
|
@@ -2832,7 +2911,7 @@ function readDaemonLauncherFile(path) {
|
|
|
2832
2911
|
function readUsableDaemonConfig(path) {
|
|
2833
2912
|
const config = readDaemonLauncher(path);
|
|
2834
2913
|
if (!config) return null;
|
|
2835
|
-
const manifest = readRuntimeManifest(join4(
|
|
2914
|
+
const manifest = readRuntimeManifest(join4(dirname(config.daemonPath), "manifest.json"));
|
|
2836
2915
|
try {
|
|
2837
2916
|
accessSync(config.nodePath, constants2.X_OK);
|
|
2838
2917
|
return manifest?.version === config.runtimeVersion && manifest.nodePath === config.nodePath && manifest.daemonSha256 === sha256File(config.daemonPath) ? config : null;
|
|
@@ -2926,7 +3005,7 @@ function atomicWrite2(path, content, mode, validate) {
|
|
|
2926
3005
|
}
|
|
2927
3006
|
} catch {
|
|
2928
3007
|
}
|
|
2929
|
-
mkdirSync2(
|
|
3008
|
+
mkdirSync2(dirname(path), { recursive: true, mode: RUNTIME_DIR_MODE });
|
|
2930
3009
|
const temp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
2931
3010
|
writeFileSync2(temp, content, { encoding: "utf8", mode, flag: "wx" });
|
|
2932
3011
|
try {
|
|
@@ -2962,7 +3041,7 @@ function canonicalProspectivePath(path) {
|
|
|
2962
3041
|
const suffix = [];
|
|
2963
3042
|
let ancestor = resolve2(path);
|
|
2964
3043
|
while (!existsSync3(ancestor)) {
|
|
2965
|
-
const parent =
|
|
3044
|
+
const parent = dirname(ancestor);
|
|
2966
3045
|
if (parent === ancestor) return resolve2(path);
|
|
2967
3046
|
suffix.unshift(basename(ancestor));
|
|
2968
3047
|
ancestor = parent;
|
|
@@ -3302,7 +3381,7 @@ async function ensureMacDaemonLocked(options) {
|
|
|
3302
3381
|
};
|
|
3303
3382
|
}
|
|
3304
3383
|
const servicePaths = launchdPaths(options);
|
|
3305
|
-
mkdirSync2(
|
|
3384
|
+
mkdirSync2(dirname(servicePaths.logPath), { recursive: true, mode: RUNTIME_DIR_MODE });
|
|
3306
3385
|
writeFileSync2(servicePaths.logPath, "", { mode: RUNTIME_FILE_MODE, flag: "a" });
|
|
3307
3386
|
chmodSync(servicePaths.logPath, RUNTIME_FILE_MODE);
|
|
3308
3387
|
const launcher = generateDaemonLauncher(daemonConfig);
|
|
@@ -7534,7 +7613,26 @@ async function checkRepositoryBinding() {
|
|
|
7534
7613
|
};
|
|
7535
7614
|
}
|
|
7536
7615
|
}
|
|
7537
|
-
function
|
|
7616
|
+
function managedHookRemedy(inspection) {
|
|
7617
|
+
switch (inspection.reason) {
|
|
7618
|
+
case "missing":
|
|
7619
|
+
case "missing_block":
|
|
7620
|
+
case "stale_block":
|
|
7621
|
+
case "legacy_block":
|
|
7622
|
+
case "unreachable_block":
|
|
7623
|
+
case "misplaced_block":
|
|
7624
|
+
if (inspection.location === "prim") return "run `prim enable` to refresh prim's global hooks";
|
|
7625
|
+
if (inspection.location === "external") {
|
|
7626
|
+
return `outside this repository \u2014 ${externalHookRemedy(inspection.hookName, inspection.gitRoot)}`;
|
|
7627
|
+
}
|
|
7628
|
+
return "run `prim hooks install`";
|
|
7629
|
+
case "entrypoint_missing":
|
|
7630
|
+
return "run `prim enable` to stage the hook runtime";
|
|
7631
|
+
default:
|
|
7632
|
+
return void 0;
|
|
7633
|
+
}
|
|
7634
|
+
}
|
|
7635
|
+
function classifyManagedHook(hookName, inspection, firing) {
|
|
7538
7636
|
if (inspection.covered) {
|
|
7539
7637
|
return {
|
|
7540
7638
|
name: hookName,
|
|
@@ -7542,15 +7640,78 @@ function classifyManagedHook(hookName, inspection) {
|
|
|
7542
7640
|
detail: `effective and executable \xB7 ${inspection.kind} \xB7 ${inspection.hookPath}`
|
|
7543
7641
|
};
|
|
7544
7642
|
}
|
|
7643
|
+
const reason = inspection.reason ?? "uncovered";
|
|
7644
|
+
if (inspection.mode === "manual") {
|
|
7645
|
+
if (hookName === "post-commit" && firing?.state === "fired") {
|
|
7646
|
+
return {
|
|
7647
|
+
name: hookName,
|
|
7648
|
+
status: "ok",
|
|
7649
|
+
detail: `manual (prim.gitHooks=manual) \xB7 ran for the latest commit \xB7 ${inspection.hookPath}`
|
|
7650
|
+
};
|
|
7651
|
+
}
|
|
7652
|
+
return {
|
|
7653
|
+
name: hookName,
|
|
7654
|
+
status: "warn",
|
|
7655
|
+
detail: `manual (prim.gitHooks=manual) \xB7 ${reason} \xB7 wire with \`prim hooks snippet ${hookName}\` \xB7 ${inspection.hookPath}`
|
|
7656
|
+
};
|
|
7657
|
+
}
|
|
7658
|
+
const remedy = managedHookRemedy(inspection);
|
|
7659
|
+
const stillCaptures = reason === "legacy_block" || reason === "misplaced_block";
|
|
7545
7660
|
return {
|
|
7546
7661
|
name: hookName,
|
|
7547
|
-
status: "fail",
|
|
7548
|
-
detail: `${
|
|
7662
|
+
status: hookName === "pre-commit" || stillCaptures ? "warn" : "fail",
|
|
7663
|
+
detail: `${reason}${remedy ? ` \xB7 ${remedy}` : ""} \xB7 ${inspection.hookPath}`
|
|
7549
7664
|
};
|
|
7550
7665
|
}
|
|
7551
|
-
function
|
|
7666
|
+
function minutesAgo(at, now) {
|
|
7667
|
+
const minutes = Math.max(0, Math.round((now - at) / 6e4));
|
|
7668
|
+
return minutes === 0 ? "just now" : `${String(minutes)}m ago`;
|
|
7669
|
+
}
|
|
7670
|
+
function classifyHookFiring(firing, now = Date.now()) {
|
|
7671
|
+
const name = "hook-fired";
|
|
7672
|
+
switch (firing.state) {
|
|
7673
|
+
case "inactive":
|
|
7674
|
+
return { name, status: "ok", detail: "repository not active \u2014 no post-commit run expected" };
|
|
7675
|
+
case "unverified":
|
|
7676
|
+
return {
|
|
7677
|
+
name,
|
|
7678
|
+
status: "ok",
|
|
7679
|
+
detail: "no local commit since the hooks were wired \u2014 the next commit is checked"
|
|
7680
|
+
};
|
|
7681
|
+
case "pending":
|
|
7682
|
+
return {
|
|
7683
|
+
name,
|
|
7684
|
+
status: "ok",
|
|
7685
|
+
detail: `latest commit is under ${String(POST_COMMIT_GRACE_MS / 1e3)}s old \u2014 capture may still be starting`
|
|
7686
|
+
};
|
|
7687
|
+
case "fired":
|
|
7688
|
+
return {
|
|
7689
|
+
name,
|
|
7690
|
+
status: "ok",
|
|
7691
|
+
detail: `post-commit reached prim for the latest commit (${minutesAgo(firing.firedAt, now)})`
|
|
7692
|
+
};
|
|
7693
|
+
case "not_firing":
|
|
7694
|
+
return {
|
|
7695
|
+
name,
|
|
7696
|
+
status: "fail",
|
|
7697
|
+
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`
|
|
7698
|
+
};
|
|
7699
|
+
}
|
|
7700
|
+
}
|
|
7701
|
+
function inspectFiringHere() {
|
|
7702
|
+
try {
|
|
7703
|
+
const root = gitToplevel();
|
|
7704
|
+
return root ? inspectPostCommitFiring(root) : void 0;
|
|
7705
|
+
} catch {
|
|
7706
|
+
return void 0;
|
|
7707
|
+
}
|
|
7708
|
+
}
|
|
7709
|
+
function checkHookFiring(firing) {
|
|
7710
|
+
return firing ? classifyHookFiring(firing) : { name: "hook-fired", status: "warn", detail: "not a git repository" };
|
|
7711
|
+
}
|
|
7712
|
+
function checkManagedHook(hookName, firing) {
|
|
7552
7713
|
try {
|
|
7553
|
-
return classifyManagedHook(hookName,
|
|
7714
|
+
return classifyManagedHook(hookName, inspectEffectiveGitHook(hookName), firing);
|
|
7554
7715
|
} catch (error) {
|
|
7555
7716
|
const detail = error instanceof Error ? error.message : String(error);
|
|
7556
7717
|
return {
|
|
@@ -7869,15 +8030,9 @@ async function checkFeedbackCapability() {
|
|
|
7869
8030
|
return { name: "feedback-api", status: "warn", detail: message.slice(0, 80) };
|
|
7870
8031
|
}
|
|
7871
8032
|
}
|
|
7872
|
-
function refreshOwnedGlobalHooksForHealth() {
|
|
7873
|
-
try {
|
|
7874
|
-
refreshOwnedGlobalHooks();
|
|
7875
|
-
} catch {
|
|
7876
|
-
}
|
|
7877
|
-
}
|
|
7878
8033
|
async function independentChecks() {
|
|
7879
|
-
refreshOwnedGlobalHooksForHealth();
|
|
7880
8034
|
const backend = await checkBackend();
|
|
8035
|
+
const firing = inspectFiringHere();
|
|
7881
8036
|
return {
|
|
7882
8037
|
before: [checkAuth()],
|
|
7883
8038
|
after: [
|
|
@@ -7886,8 +8041,10 @@ async function independentChecks() {
|
|
|
7886
8041
|
...checkAgentHooks(),
|
|
7887
8042
|
checkHookRuntime(),
|
|
7888
8043
|
await checkRepositoryBinding(),
|
|
7889
|
-
checkManagedHook("
|
|
7890
|
-
checkManagedHook("post-
|
|
8044
|
+
checkManagedHook("pre-commit"),
|
|
8045
|
+
checkManagedHook("post-commit", firing),
|
|
8046
|
+
checkManagedHook("post-rewrite"),
|
|
8047
|
+
checkHookFiring(firing),
|
|
7891
8048
|
...backend,
|
|
7892
8049
|
await checkFeedbackCapability()
|
|
7893
8050
|
]
|
|
@@ -7936,7 +8093,7 @@ import { join as join9 } from "path";
|
|
|
7936
8093
|
|
|
7937
8094
|
// src/flusher.ts
|
|
7938
8095
|
import { createReadStream, renameSync as renameSync3, statSync as statSync3, unlinkSync as unlinkSync3 } from "fs";
|
|
7939
|
-
import { dirname as
|
|
8096
|
+
import { dirname as dirname3, join as join8 } from "path";
|
|
7940
8097
|
|
|
7941
8098
|
// src/dead-letter.ts
|
|
7942
8099
|
import { createHash as createHash3, randomBytes as randomBytes4 } from "crypto";
|
|
@@ -7953,12 +8110,12 @@ import {
|
|
|
7953
8110
|
rmSync as rmSync3,
|
|
7954
8111
|
writeFileSync as writeFileSync3
|
|
7955
8112
|
} from "fs";
|
|
7956
|
-
import { dirname as
|
|
8113
|
+
import { dirname as dirname2, join as join7 } from "path";
|
|
7957
8114
|
var DIRECTORY_MODE = 448;
|
|
7958
8115
|
var FILE_MODE = 384;
|
|
7959
8116
|
var DEAD_LETTER_DIRNAME = "dead-letter";
|
|
7960
8117
|
function deadLetterDirectoryForRotation(flushingPath) {
|
|
7961
|
-
return join7(
|
|
8118
|
+
return join7(dirname2(flushingPath), DEAD_LETTER_DIRNAME);
|
|
7962
8119
|
}
|
|
7963
8120
|
function deadLetterPathForRawLine(flushingPath, rawLine) {
|
|
7964
8121
|
const quarantineId = createHash3("sha256").update(rawLine).digest("hex");
|
|
@@ -7968,7 +8125,7 @@ function ensureDeadLetterDirectory(flushingPath, persistDirectory) {
|
|
|
7968
8125
|
const directory = deadLetterDirectoryForRotation(flushingPath);
|
|
7969
8126
|
mkdirSync4(directory, { recursive: true, mode: DIRECTORY_MODE });
|
|
7970
8127
|
chmodSync2(directory, DIRECTORY_MODE);
|
|
7971
|
-
persistDirectory(
|
|
8128
|
+
persistDirectory(dirname2(directory));
|
|
7972
8129
|
return directory;
|
|
7973
8130
|
}
|
|
7974
8131
|
function persistDeadLetter(directory, path, quarantineId, record3, persistDirectory) {
|
|
@@ -8387,7 +8544,7 @@ async function flushOnce() {
|
|
|
8387
8544
|
return inspection.retainedBuckets.length > 0 ? { flushed: total, quarantined, retained: inspection.retainedBuckets } : { flushed: total, quarantined };
|
|
8388
8545
|
}
|
|
8389
8546
|
var flushInFlight;
|
|
8390
|
-
var FLUSH_LOCK_PATH = join8(
|
|
8547
|
+
var FLUSH_LOCK_PATH = join8(dirname3(JOURNAL_DIR), ".flush.lock");
|
|
8391
8548
|
var FLUSH_LOCK_TIMEOUT_MS = 250;
|
|
8392
8549
|
function isFlushLockContended(error) {
|
|
8393
8550
|
return error instanceof Error && error.message.startsWith("timed out waiting for file lock");
|
|
@@ -8735,12 +8892,16 @@ function planSetupSteps(opts) {
|
|
|
8735
8892
|
required: true
|
|
8736
8893
|
});
|
|
8737
8894
|
}
|
|
8738
|
-
|
|
8739
|
-
key: "hooks",
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8895
|
+
if (opts.scope === "project") {
|
|
8896
|
+
steps.push({ key: "hooks", label: "Git hooks", args: ["hooks", "install"], required: true });
|
|
8897
|
+
} else if (opts.globalHooksPath) {
|
|
8898
|
+
steps.push({
|
|
8899
|
+
key: "hooks",
|
|
8900
|
+
label: "Git hooks (global)",
|
|
8901
|
+
args: ["hooks", "install", "--scope", "user", "--global-hooks-path"],
|
|
8902
|
+
required: true
|
|
8903
|
+
});
|
|
8904
|
+
}
|
|
8744
8905
|
const skillArgs = ["skill", "install", "--agent", opts.agent, ...scopeArgs];
|
|
8745
8906
|
steps.push({ key: "skill", label: "Agent skill", args: skillArgs, required: true });
|
|
8746
8907
|
steps.push({
|
|
@@ -8771,7 +8932,44 @@ function planCleanupUninstalls(agent, conflicts) {
|
|
|
8771
8932
|
if (conflicts.includes(CONFLICT_SKILL)) steps.push(["skill", "uninstall", "--agent", agent]);
|
|
8772
8933
|
return steps;
|
|
8773
8934
|
}
|
|
8774
|
-
function
|
|
8935
|
+
function preCommitRunsPrim(content) {
|
|
8936
|
+
return content.includes("# >>> prim pre-commit hook >>>") || content.includes("prim-pre-commit");
|
|
8937
|
+
}
|
|
8938
|
+
function projectHooksConflict(globalHooksActive, preCommit) {
|
|
8939
|
+
return globalHooksActive && preCommit !== void 0 && preCommitRunsPrim(preCommit);
|
|
8940
|
+
}
|
|
8941
|
+
function enableWiresRepository(root) {
|
|
8942
|
+
if (!root) return void 0;
|
|
8943
|
+
try {
|
|
8944
|
+
if (gitHooksMode({ cwd: root }) === "manual") return void 0;
|
|
8945
|
+
return resolveEffectiveGitHook("post-commit", root).location !== "external";
|
|
8946
|
+
} catch {
|
|
8947
|
+
return void 0;
|
|
8948
|
+
}
|
|
8949
|
+
}
|
|
8950
|
+
function setupGitHooksNote(plan, wiresThisRepository) {
|
|
8951
|
+
if (wiresThisRepository === true && plan.action === "add_to_dir") {
|
|
8952
|
+
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`;
|
|
8953
|
+
}
|
|
8954
|
+
if (wiresThisRepository === true && plan.action === "system_declined") {
|
|
8955
|
+
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`;
|
|
8956
|
+
}
|
|
8957
|
+
switch (plan.action) {
|
|
8958
|
+
case "refresh":
|
|
8959
|
+
return "git hooks \xB7 prim's global hooks stay active; `prim enable` refreshes them";
|
|
8960
|
+
case "present_in_dir":
|
|
8961
|
+
return `git hooks \xB7 prim's hooks are already in your global hooks dir ${plan.global}`;
|
|
8962
|
+
case "add_to_dir":
|
|
8963
|
+
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`;
|
|
8964
|
+
case "system_declined":
|
|
8965
|
+
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)`;
|
|
8966
|
+
case "manual":
|
|
8967
|
+
return "git hooks \xB7 prim.gitHooks=manual: prim writes no hook files; wire them with `prim hooks snippet <hook>`";
|
|
8968
|
+
default:
|
|
8969
|
+
return "git hooks \xB7 wired per repository by `prim enable`; git's global hooks are untouched (opt in with --global-hooks-path)";
|
|
8970
|
+
}
|
|
8971
|
+
}
|
|
8972
|
+
function detectProjectConflicts(agent, run, globalHooksActive) {
|
|
8775
8973
|
const conflicts = [];
|
|
8776
8974
|
if (agent !== "hermes") {
|
|
8777
8975
|
try {
|
|
@@ -8780,14 +8978,14 @@ function detectProjectConflicts(agent, run) {
|
|
|
8780
8978
|
} catch {
|
|
8781
8979
|
}
|
|
8782
8980
|
}
|
|
8981
|
+
let preCommit;
|
|
8783
8982
|
try {
|
|
8784
8983
|
const root = gitToplevel();
|
|
8785
|
-
const
|
|
8786
|
-
if (
|
|
8787
|
-
conflicts.push(CONFLICT_HOOKS);
|
|
8788
|
-
}
|
|
8984
|
+
const path = root && join11(root, ".git", "hooks", "pre-commit");
|
|
8985
|
+
if (path && existsSync10(path)) preCommit = readFileSync7(path, "utf-8");
|
|
8789
8986
|
} catch {
|
|
8790
8987
|
}
|
|
8988
|
+
if (projectHooksConflict(globalHooksActive, preCommit)) conflicts.push(CONFLICT_HOOKS);
|
|
8791
8989
|
try {
|
|
8792
8990
|
const parsed = JSON.parse(
|
|
8793
8991
|
run(["skill", "status", "--agent", agent, "--json"], true).stdout || "{}"
|
|
@@ -8827,8 +9025,11 @@ function registerSetupCommand(program2, dependencies = {}) {
|
|
|
8827
9025
|
"Install everything in one shot (auth, session + git hooks, daemon, skill, welcome)"
|
|
8828
9026
|
).option("--agent <agent>", "claude, codex, cursor, or hermes (auto-detected when omitted)").option(
|
|
8829
9027
|
"--scope <scope>",
|
|
8830
|
-
"user (default \u2014
|
|
9028
|
+
"user (default \u2014 agent integration and skill for every repo; git hooks per repo via enable) or project (this repo only)",
|
|
8831
9029
|
"user"
|
|
9030
|
+
).option(
|
|
9031
|
+
"--global-hooks-path",
|
|
9032
|
+
"with the default user scope, also change git's global hooks so prim's hooks run in every repository"
|
|
8832
9033
|
).option(
|
|
8833
9034
|
"--migrate",
|
|
8834
9035
|
"with the default user scope, remove any project-scoped prim config in this repo (else just warn)"
|
|
@@ -8848,6 +9049,11 @@ function registerSetupCommand(program2, dependencies = {}) {
|
|
|
8848
9049
|
(dependencies.exit ?? process.exit)(EXIT_USAGE3);
|
|
8849
9050
|
return;
|
|
8850
9051
|
}
|
|
9052
|
+
if (opts.globalHooksPath && opts.scope !== "user") {
|
|
9053
|
+
process.stderr.write("[prim] --global-hooks-path applies only with --scope user\n");
|
|
9054
|
+
(dependencies.exit ?? process.exit)(EXIT_USAGE3);
|
|
9055
|
+
return;
|
|
9056
|
+
}
|
|
8851
9057
|
const agent = agentInput;
|
|
8852
9058
|
const scope = opts.scope;
|
|
8853
9059
|
const self = process.argv[1];
|
|
@@ -8899,16 +9105,24 @@ function registerSetupCommand(program2, dependencies = {}) {
|
|
|
8899
9105
|
note("pre-authorize \xB7 writing prim allow-rule (user scope)\u2026");
|
|
8900
9106
|
results.preauth = run(["claude", "preauth", "--scope", "user"]).code === 0 ? "ok" : "skipped";
|
|
8901
9107
|
}
|
|
8902
|
-
const setupSteps = planSetupSteps({
|
|
9108
|
+
const setupSteps = planSetupSteps({
|
|
9109
|
+
agent,
|
|
9110
|
+
daemon: opts.daemon,
|
|
9111
|
+
scope,
|
|
9112
|
+
globalHooksPath: opts.globalHooksPath === true
|
|
9113
|
+
});
|
|
8903
9114
|
for (const step of setupSteps.filter(
|
|
8904
9115
|
(candidate) => candidate.key !== "enable" && candidate.key !== "health"
|
|
8905
9116
|
)) {
|
|
8906
9117
|
note(`${step.label} \xB7 installing\u2026`);
|
|
8907
9118
|
const { code } = run(step.args);
|
|
8908
|
-
results[step.key] = code === 0 ? "ok" : step.required ? "failed" : "skipped";
|
|
9119
|
+
results[step.key] = code === 0 ? "ok" : code === EXIT_GLOBAL_HOOKS_NOT_INSTALLED && step.key === "hooks" ? "skipped" : step.required ? "failed" : "skipped";
|
|
9120
|
+
}
|
|
9121
|
+
if (scope === "user" && !opts.globalHooksPath) {
|
|
9122
|
+
note(setupGitHooksNote(planGlobalHooks(), enableWiresRepository(gitToplevel())));
|
|
8909
9123
|
}
|
|
8910
9124
|
if (scope === "user") {
|
|
8911
|
-
const conflicts = detectProjectConflicts(agent, run);
|
|
9125
|
+
const conflicts = detectProjectConflicts(agent, run, globalHooksPathIsPrims());
|
|
8912
9126
|
if (conflicts.length > 0 && opts.migrate) {
|
|
8913
9127
|
note(`migrate \xB7 removing project-scoped config (${conflicts.join(", ")})\u2026`);
|
|
8914
9128
|
const ok = planCleanupUninstalls(agent, conflicts).map((args) => run(args).code === 0).every(Boolean);
|
|
@@ -8949,12 +9163,12 @@ function registerSetupCommand(program2, dependencies = {}) {
|
|
|
8949
9163
|
|
|
8950
9164
|
// src/commands/statusline.ts
|
|
8951
9165
|
import { readFileSync as readFileSync8 } from "fs";
|
|
8952
|
-
import { dirname as
|
|
9166
|
+
import { dirname as dirname4, resolve as resolve3 } from "path";
|
|
8953
9167
|
import { fileURLToPath } from "url";
|
|
8954
9168
|
var STATUSLINE_TIMEOUT_MS = 200;
|
|
8955
9169
|
function readPackageVersion() {
|
|
8956
9170
|
try {
|
|
8957
|
-
const here =
|
|
9171
|
+
const here = dirname4(fileURLToPath(import.meta.url));
|
|
8958
9172
|
const candidates = [
|
|
8959
9173
|
// Schema-v2 runtimes staged a manifest beside this standalone bundle.
|
|
8960
9174
|
// Keep resolving it while those legacy launchers transition to schema v3.
|
|
@@ -9470,7 +9684,7 @@ import { readFileSync as readFileSync9 } from "fs";
|
|
|
9470
9684
|
|
|
9471
9685
|
// src/daemon/health.ts
|
|
9472
9686
|
import { chmodSync as chmodSync3, existsSync as existsSync11, mkdirSync as mkdirSync7, renameSync as renameSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
9473
|
-
import { dirname as
|
|
9687
|
+
import { dirname as dirname5, join as join12 } from "path";
|
|
9474
9688
|
var HEARTBEAT_FRESH_MS = 9e4;
|
|
9475
9689
|
var DAEMON_HEALTH_PATH = join12(primConfigDirectory(), "daemon-health.json");
|
|
9476
9690
|
|
|
@@ -9599,7 +9813,7 @@ function runStartupBackgroundWork(argv, env, work) {
|
|
|
9599
9813
|
}
|
|
9600
9814
|
|
|
9601
9815
|
// src/index.ts
|
|
9602
|
-
var __dirname =
|
|
9816
|
+
var __dirname = dirname6(fileURLToPath2(import.meta.url));
|
|
9603
9817
|
var pkg = JSON.parse(readFileSync10(resolve4(__dirname, "../package.json"), "utf-8"));
|
|
9604
9818
|
runStartupBackgroundWork(process.argv.slice(2), process.env, {
|
|
9605
9819
|
notify: () => updateNotifier({ pkg }).notify(),
|