@primitive.ai/prim 0.1.0-alpha.61 → 0.1.0-alpha.63
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/dist/{chunk-I3HIPAIR.js → chunk-CPX3BUOC.js} +7 -4
- package/dist/chunk-DA4BMHEB.js +77 -0
- package/dist/{chunk-B4SHJYIN.js → chunk-GF5QII7T.js} +83 -110
- package/dist/{chunk-S4LFRVGD.js → chunk-HIMWM526.js} +1 -1
- package/dist/{chunk-YIMUIPMD.js → chunk-KBFFVYBC.js} +7 -1
- package/dist/{chunk-R4CUX6FY.js → chunk-KHZNYEUV.js} +1 -1
- package/dist/chunk-QGVHLP5J.js +117 -0
- package/dist/{chunk-IMAIBPUC.js → chunk-UVIWZWM6.js} +1 -0
- package/dist/chunk-WNIY6Z6N.js +1244 -0
- package/dist/{chunk-YITJ2H3B.js → chunk-WS3MCNFD.js} +1 -1
- package/dist/chunk-XTNMSS4C.js +283 -0
- package/dist/daemon/server.js +343 -44
- package/dist/hooks/post-commit.js +84 -18
- package/dist/hooks/post-tool-use.js +42 -26
- package/dist/hooks/pre-commit.js +2 -2
- package/dist/hooks/pre-tool-use.js +103 -11
- package/dist/hooks/prim-hook.js +33 -12
- package/dist/hooks/session-start.js +29 -8
- package/dist/index.js +361 -155
- package/dist/statusline-main.js +57 -42
- package/package.json +1 -1
- package/dist/chunk-GUXBUBQ7.js +0 -84
- package/dist/chunk-LDQZASIF.js +0 -166
- package/dist/chunk-N4R7EXZT.js +0 -620
- package/dist/chunk-YDBZTFW5.js +0 -67
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
checkAffectedDecisions,
|
|
14
14
|
daemonOrDirectGet,
|
|
15
15
|
formatDecisionsWarning
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-WS3MCNFD.js";
|
|
17
17
|
import {
|
|
18
18
|
JOURNAL_DIR,
|
|
19
19
|
SESSIONS_DIR,
|
|
@@ -22,19 +22,25 @@ import {
|
|
|
22
22
|
listFlushing,
|
|
23
23
|
pendingJournalStats,
|
|
24
24
|
readMovesFromPath
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-HIMWM526.js";
|
|
26
26
|
import {
|
|
27
27
|
bindRepository,
|
|
28
28
|
compareSemver,
|
|
29
|
+
ensureEffectivePostCommitHook,
|
|
30
|
+
ensurePostCommitHookAtPath,
|
|
31
|
+
inspectEffectivePostCommitHook,
|
|
32
|
+
postCommitHookBlock,
|
|
29
33
|
printJson,
|
|
30
|
-
registerSkillCommands
|
|
31
|
-
|
|
34
|
+
registerSkillCommands,
|
|
35
|
+
uninstallPostCommitHookAtPath,
|
|
36
|
+
uninstallProjectPostCommitHook
|
|
37
|
+
} from "./chunk-WNIY6Z6N.js";
|
|
32
38
|
import {
|
|
33
39
|
fetchFeedbackCapability
|
|
34
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-KHZNYEUV.js";
|
|
35
41
|
import {
|
|
36
42
|
inspectWorkspaceId
|
|
37
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-UVIWZWM6.js";
|
|
38
44
|
import {
|
|
39
45
|
binFile,
|
|
40
46
|
commandMatchesBin,
|
|
@@ -42,16 +48,17 @@ import {
|
|
|
42
48
|
packageVersion,
|
|
43
49
|
pinnedHookCommand,
|
|
44
50
|
warmBinCache
|
|
45
|
-
} from "./chunk-
|
|
51
|
+
} from "./chunk-KBFFVYBC.js";
|
|
46
52
|
import {
|
|
47
53
|
canonicalGitRoot,
|
|
48
54
|
canonicalRepositoryPath,
|
|
49
55
|
decisionIngestionStatus,
|
|
50
56
|
gitToplevel,
|
|
51
57
|
isRepoActiveForCapture,
|
|
58
|
+
isValidRepoSyncId,
|
|
52
59
|
repoSyncId,
|
|
53
60
|
setRepoActive
|
|
54
|
-
} from "./chunk-
|
|
61
|
+
} from "./chunk-XTNMSS4C.js";
|
|
55
62
|
import {
|
|
56
63
|
HttpError,
|
|
57
64
|
REFRESH_TOKEN_PATH,
|
|
@@ -66,7 +73,7 @@ import {
|
|
|
66
73
|
resolveAuthCredential,
|
|
67
74
|
setStoredToken,
|
|
68
75
|
withFileLock
|
|
69
|
-
} from "./chunk-
|
|
76
|
+
} from "./chunk-CPX3BUOC.js";
|
|
70
77
|
import {
|
|
71
78
|
daemonIsLive,
|
|
72
79
|
daemonRequest
|
|
@@ -91,17 +98,20 @@ async function applyActivation(active) {
|
|
|
91
98
|
}
|
|
92
99
|
try {
|
|
93
100
|
let bound;
|
|
101
|
+
let postCommitHook;
|
|
94
102
|
if (active) {
|
|
103
|
+
postCommitHook = ensureEffectivePostCommitHook(root).path;
|
|
95
104
|
bound = await bindRepository(root);
|
|
96
105
|
}
|
|
97
106
|
setRepoActive(root, active);
|
|
107
|
+
await daemonRequest("statusline_invalidate", {}, { timeoutMs: 250 });
|
|
98
108
|
process.stderr.write(`[prim] prim ${active ? "enabled" : "disabled"} in ${root}
|
|
99
109
|
`);
|
|
100
|
-
printJson({ active, repo: root, ...bound });
|
|
110
|
+
printJson({ active, repo: root, ...bound, ...postCommitHook ? { postCommitHook } : {} });
|
|
101
111
|
} catch (err) {
|
|
102
112
|
const detail = err instanceof Error ? err.message : String(err);
|
|
103
113
|
process.stderr.write(
|
|
104
|
-
`[prim] failed to ${active ? "bind and enable" : "disable"} prim: ${detail}
|
|
114
|
+
`[prim] failed to ${active ? "cover, bind, and enable" : "disable"} prim: ${detail}
|
|
105
115
|
`
|
|
106
116
|
);
|
|
107
117
|
process.exit(1);
|
|
@@ -662,7 +672,7 @@ function normalizeApiUrl(url) {
|
|
|
662
672
|
|
|
663
673
|
// src/daemon/launchd.ts
|
|
664
674
|
var LAUNCHD_LABEL = "ai.getprimitive.prim-daemon";
|
|
665
|
-
var RUNTIME_SCHEMA_VERSION =
|
|
675
|
+
var RUNTIME_SCHEMA_VERSION = 3;
|
|
666
676
|
var RUNTIME_DIR_MODE = 448;
|
|
667
677
|
var RUNTIME_FILE_MODE = 384;
|
|
668
678
|
var RUNTIME_LAUNCHER_MODE = 448;
|
|
@@ -742,7 +752,7 @@ function findPackageVersion(entryFile) {
|
|
|
742
752
|
function readRuntimeManifest(path) {
|
|
743
753
|
try {
|
|
744
754
|
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
745
|
-
if (parsed.schemaVersion !== RUNTIME_SCHEMA_VERSION || typeof parsed.version !== "string" || typeof parsed.nodePath !== "string" || parsed.daemonFile !== "prim-daemon-server" || typeof parsed.daemonSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(parsed.daemonSha256)
|
|
755
|
+
if (parsed.schemaVersion !== RUNTIME_SCHEMA_VERSION || typeof parsed.version !== "string" || typeof parsed.nodePath !== "string" || parsed.daemonFile !== "prim-daemon-server" || typeof parsed.daemonSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(parsed.daemonSha256)) {
|
|
746
756
|
return null;
|
|
747
757
|
}
|
|
748
758
|
return parsed;
|
|
@@ -750,12 +760,12 @@ function readRuntimeManifest(path) {
|
|
|
750
760
|
return null;
|
|
751
761
|
}
|
|
752
762
|
}
|
|
753
|
-
function sameRuntime(current, desired, daemonPath
|
|
754
|
-
if (!current || current.version !== desired.version || current.nodePath !== desired.nodePath || current.daemonSha256 !== desired.daemonSha256
|
|
763
|
+
function sameRuntime(current, desired, daemonPath) {
|
|
764
|
+
if (!current || current.version !== desired.version || current.nodePath !== desired.nodePath || current.daemonSha256 !== desired.daemonSha256) {
|
|
755
765
|
return false;
|
|
756
766
|
}
|
|
757
767
|
try {
|
|
758
|
-
return sha256File(daemonPath) === desired.daemonSha256
|
|
768
|
+
return sha256File(daemonPath) === desired.daemonSha256;
|
|
759
769
|
} catch {
|
|
760
770
|
return false;
|
|
761
771
|
}
|
|
@@ -779,8 +789,6 @@ function stageRuntime(options = {}) {
|
|
|
779
789
|
if (!daemonSource || !existsSync2(daemonSource)) {
|
|
780
790
|
throw new Error("cannot stage runtime: prim-daemon-server bundle is unavailable");
|
|
781
791
|
}
|
|
782
|
-
const configuredStatusline = options.statuslineSource === void 0 ? binFile("prim-statusline") : options.statuslineSource;
|
|
783
|
-
const statuslineSource = configuredStatusline && existsSync2(configuredStatusline) ? configuredStatusline : null;
|
|
784
792
|
const nodePath = resolve(options.nodePath ?? process.execPath);
|
|
785
793
|
const version = options.version ?? findPackageVersion(daemonSource);
|
|
786
794
|
const desired = {
|
|
@@ -789,10 +797,6 @@ function stageRuntime(options = {}) {
|
|
|
789
797
|
nodePath,
|
|
790
798
|
daemonFile: "prim-daemon-server",
|
|
791
799
|
daemonSha256: sha256File(daemonSource),
|
|
792
|
-
...statuslineSource ? {
|
|
793
|
-
statuslineFile: "prim-statusline",
|
|
794
|
-
statuslineSha256: sha256File(statuslineSource)
|
|
795
|
-
} : {},
|
|
796
800
|
stagedAt: (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
|
|
797
801
|
};
|
|
798
802
|
let currentRelease = null;
|
|
@@ -802,19 +806,13 @@ function stageRuntime(options = {}) {
|
|
|
802
806
|
}
|
|
803
807
|
const current = currentRelease ? readRuntimeManifest(join(currentRelease, "manifest.json")) : null;
|
|
804
808
|
const currentDaemon = currentRelease ? join(currentRelease, desired.daemonFile) : "";
|
|
805
|
-
|
|
806
|
-
if (currentRelease && sameRuntime(current, desired, currentDaemon, currentStatusline)) {
|
|
809
|
+
if (currentRelease && sameRuntime(current, desired, currentDaemon)) {
|
|
807
810
|
const manifest = current;
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
RUNTIME_LAUNCHER_MODE
|
|
814
|
-
);
|
|
815
|
-
} else {
|
|
816
|
-
rmSync(paths.statuslineLauncher, { force: true });
|
|
817
|
-
}
|
|
811
|
+
atomicWrite(
|
|
812
|
+
paths.statuslineLauncher,
|
|
813
|
+
statuslineLauncherContent(manifest.version, options.homeDir),
|
|
814
|
+
RUNTIME_LAUNCHER_MODE
|
|
815
|
+
);
|
|
818
816
|
return {
|
|
819
817
|
changed: false,
|
|
820
818
|
manifest,
|
|
@@ -835,14 +833,6 @@ function stageRuntime(options = {}) {
|
|
|
835
833
|
if (sha256File(daemonTarget) !== desired.daemonSha256) {
|
|
836
834
|
throw new Error("cannot stage runtime: daemon bundle changed while it was copied");
|
|
837
835
|
}
|
|
838
|
-
if (statuslineSource && desired.statuslineFile) {
|
|
839
|
-
const statuslineTarget = join(stagingDir, desired.statuslineFile);
|
|
840
|
-
copyFileSync(statuslineSource, statuslineTarget);
|
|
841
|
-
chmodSync(statuslineTarget, RUNTIME_FILE_MODE);
|
|
842
|
-
if (sha256File(statuslineTarget) !== desired.statuslineSha256) {
|
|
843
|
-
throw new Error("cannot stage runtime: statusline bundle changed while it was copied");
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
836
|
const manifestTarget = join(stagingDir, "manifest.json");
|
|
847
837
|
writeFileSync(manifestTarget, `${JSON.stringify(desired, null, 2)}
|
|
848
838
|
`, {
|
|
@@ -850,23 +840,16 @@ function stageRuntime(options = {}) {
|
|
|
850
840
|
mode: RUNTIME_FILE_MODE,
|
|
851
841
|
flag: "wx"
|
|
852
842
|
});
|
|
853
|
-
const releaseName = `release-${createHash2("sha256").update(
|
|
854
|
-
`${version}\0${nodePath}\0${desired.statuslineFile ?? ""}\0${desired.stagedAt}\0${process.pid}\0${Math.random()}`
|
|
855
|
-
).digest("hex").slice(0, 16)}`;
|
|
843
|
+
const releaseName = `release-${createHash2("sha256").update(`${version}\0${nodePath}\0${desired.stagedAt}\0${process.pid}\0${Math.random()}`).digest("hex").slice(0, 16)}`;
|
|
856
844
|
releaseDir = join(paths.releasesDir, releaseName);
|
|
857
845
|
renameSync(stagingDir, releaseDir);
|
|
858
846
|
releaseDir = realpathSync(releaseDir);
|
|
859
847
|
atomicSymlink(join("releases", releaseName), paths.currentLink);
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
RUNTIME_LAUNCHER_MODE
|
|
866
|
-
);
|
|
867
|
-
} else {
|
|
868
|
-
rmSync(paths.statuslineLauncher, { force: true });
|
|
869
|
-
}
|
|
848
|
+
atomicWrite(
|
|
849
|
+
paths.statuslineLauncher,
|
|
850
|
+
statuslineLauncherContent(desired.version, options.homeDir),
|
|
851
|
+
RUNTIME_LAUNCHER_MODE
|
|
852
|
+
);
|
|
870
853
|
} catch (error) {
|
|
871
854
|
rmSync(stagingDir, { recursive: true, force: true });
|
|
872
855
|
throw error;
|
|
@@ -881,9 +864,20 @@ function stageRuntime(options = {}) {
|
|
|
881
864
|
function shellQuote(value) {
|
|
882
865
|
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
883
866
|
}
|
|
884
|
-
function statuslineLauncherContent(
|
|
867
|
+
function statuslineLauncherContent(version, homeDir = homedir()) {
|
|
868
|
+
const socketPath = join(homeDir, ".config", "prim", "sock");
|
|
869
|
+
const fallback = `primitive ${version} (daemon: down)`;
|
|
885
870
|
return `#!/bin/sh
|
|
886
|
-
|
|
871
|
+
response=$(
|
|
872
|
+
printf 'prim-statusline-v1\\000%s\\000%s\\000' "\${PWD:-/}" "\${PRIM_API_URL:-}" |
|
|
873
|
+
/usr/bin/nc -U -w 1 ${shellQuote(socketPath)} 2>/dev/null
|
|
874
|
+
)
|
|
875
|
+
status=$?
|
|
876
|
+
if [ "$status" -eq 0 ] && [ -n "$response" ]; then
|
|
877
|
+
printf '%s' "$response"
|
|
878
|
+
else
|
|
879
|
+
printf '%s' ${shellQuote(fallback)}
|
|
880
|
+
fi
|
|
887
881
|
`;
|
|
888
882
|
}
|
|
889
883
|
function runtimeStatuslineCommand(options = {}) {
|
|
@@ -1719,10 +1713,7 @@ function performInstall(scope, force) {
|
|
|
1719
1713
|
let statuslineCommand = STATUSLINE_COMMAND;
|
|
1720
1714
|
if (!blockedByUserStatusline && process.platform === "darwin" && (!before.statusLine || isPrimStatusLine(before))) {
|
|
1721
1715
|
try {
|
|
1722
|
-
|
|
1723
|
-
if (!runtime.manifest.statuslineFile) {
|
|
1724
|
-
throw new Error("standalone prim-statusline entry is unavailable");
|
|
1725
|
-
}
|
|
1716
|
+
stageRuntime();
|
|
1726
1717
|
statuslineCommand = runtimeStatuslineCommand();
|
|
1727
1718
|
} catch (error) {
|
|
1728
1719
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -2021,10 +2012,74 @@ ${line("project", result.project)}`);
|
|
|
2021
2012
|
}
|
|
2022
2013
|
|
|
2023
2014
|
// src/commands/daemon.ts
|
|
2024
|
-
import { spawn } from "child_process";
|
|
2015
|
+
import { spawn as spawn2 } from "child_process";
|
|
2025
2016
|
import { closeSync as closeSync2, existsSync as existsSync4, mkdirSync as mkdirSync3, openSync as openSync2, readFileSync as readFileSync4, unlinkSync } from "fs";
|
|
2017
|
+
import { homedir as homedir5 } from "os";
|
|
2018
|
+
import { join as join5 } from "path";
|
|
2019
|
+
import { Option } from "commander";
|
|
2020
|
+
|
|
2021
|
+
// src/daemon/latest-bootstrap.ts
|
|
2022
|
+
import { spawn } from "child_process";
|
|
2026
2023
|
import { homedir as homedir4 } from "os";
|
|
2027
2024
|
import { join as join4 } from "path";
|
|
2025
|
+
var LATEST_BOOTSTRAP_TIMEOUT_MS = 45e3;
|
|
2026
|
+
var LATEST_BOOTSTRAP_LOCK_NAME = "daemon-latest.lock";
|
|
2027
|
+
async function runLatestEnsure(options) {
|
|
2028
|
+
const env = {
|
|
2029
|
+
...options.env ?? process.env,
|
|
2030
|
+
npm_config_prefer_online: "true",
|
|
2031
|
+
npm_config_fetch_retries: "1",
|
|
2032
|
+
npm_config_fetch_retry_mintimeout: "1000",
|
|
2033
|
+
npm_config_fetch_retry_maxtimeout: "5000",
|
|
2034
|
+
npm_config_fetch_timeout: "15000"
|
|
2035
|
+
};
|
|
2036
|
+
return await new Promise((resolve5) => {
|
|
2037
|
+
let settled = false;
|
|
2038
|
+
let child;
|
|
2039
|
+
try {
|
|
2040
|
+
child = (options.spawnProcess ?? spawn)(
|
|
2041
|
+
"npx",
|
|
2042
|
+
["--yes", "--prefer-online", "-p", "@primitive.ai/prim@latest", "prim", "daemon", "ensure"],
|
|
2043
|
+
{ env, stdio: "ignore" }
|
|
2044
|
+
);
|
|
2045
|
+
} catch {
|
|
2046
|
+
resolve5(false);
|
|
2047
|
+
return;
|
|
2048
|
+
}
|
|
2049
|
+
const timer = setTimeout(() => {
|
|
2050
|
+
child.kill("SIGTERM");
|
|
2051
|
+
finish(false);
|
|
2052
|
+
}, options.timeoutMs ?? LATEST_BOOTSTRAP_TIMEOUT_MS);
|
|
2053
|
+
const finish = (result) => {
|
|
2054
|
+
if (settled) return;
|
|
2055
|
+
settled = true;
|
|
2056
|
+
clearTimeout(timer);
|
|
2057
|
+
resolve5(result);
|
|
2058
|
+
};
|
|
2059
|
+
timer.unref();
|
|
2060
|
+
child.once("error", () => finish(false));
|
|
2061
|
+
child.once("exit", (code) => finish(code === 0));
|
|
2062
|
+
});
|
|
2063
|
+
}
|
|
2064
|
+
async function runLatestDaemonBootstrap(ensureCurrent, options = {}) {
|
|
2065
|
+
const lockPath = options.lockPath ?? join4(options.homeDir ?? homedir4(), ".config", "prim", LATEST_BOOTSTRAP_LOCK_NAME);
|
|
2066
|
+
try {
|
|
2067
|
+
return await withFileLock(
|
|
2068
|
+
lockPath,
|
|
2069
|
+
async () => {
|
|
2070
|
+
const current = await ensureCurrent();
|
|
2071
|
+
if (current.disabled) return true;
|
|
2072
|
+
return await runLatestEnsure(options);
|
|
2073
|
+
},
|
|
2074
|
+
// A concurrent SessionStart already owns the complete bootstrap. Do not
|
|
2075
|
+
// queue another npm lookup behind it; the short window only permits one
|
|
2076
|
+
// retry after recovering a stale lock.
|
|
2077
|
+
{ timeoutMs: 100, pollMs: 25 }
|
|
2078
|
+
);
|
|
2079
|
+
} catch {
|
|
2080
|
+
return false;
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2028
2083
|
|
|
2029
2084
|
// src/lib/presence.ts
|
|
2030
2085
|
var DECISION_ORIGIN = "https://app.getprimitive.ai";
|
|
@@ -2075,10 +2130,10 @@ function formatTeammatesWithArea(teammates, cap) {
|
|
|
2075
2130
|
|
|
2076
2131
|
// src/commands/daemon.ts
|
|
2077
2132
|
var DAEMON_BIN = "prim-daemon-server";
|
|
2078
|
-
var CONFIG_DIR =
|
|
2079
|
-
var PID_PATH =
|
|
2080
|
-
var SOCK_PATH =
|
|
2081
|
-
var LOG_PATH =
|
|
2133
|
+
var CONFIG_DIR = join5(homedir5(), ".config", "prim");
|
|
2134
|
+
var PID_PATH = join5(CONFIG_DIR, "daemon.pid");
|
|
2135
|
+
var SOCK_PATH = join5(CONFIG_DIR, "sock");
|
|
2136
|
+
var LOG_PATH = join5(CONFIG_DIR, "daemon.log");
|
|
2082
2137
|
var CONFIG_DIR_MODE = 448;
|
|
2083
2138
|
var LOG_FILE_MODE = 384;
|
|
2084
2139
|
var STOP_TIMEOUT_MS = 5e3;
|
|
@@ -2149,13 +2204,13 @@ function sleep(ms) {
|
|
|
2149
2204
|
}
|
|
2150
2205
|
function spawnDaemon(options) {
|
|
2151
2206
|
const file = binFile(DAEMON_BIN);
|
|
2152
|
-
return file ?
|
|
2207
|
+
return file ? spawn2(process.execPath, [file], options) : spawn2(DAEMON_BIN, [], options);
|
|
2153
2208
|
}
|
|
2154
2209
|
function openDaemonLog(configDir = CONFIG_DIR) {
|
|
2155
2210
|
if (!existsSync4(configDir)) {
|
|
2156
2211
|
mkdirSync3(configDir, { recursive: true, mode: CONFIG_DIR_MODE });
|
|
2157
2212
|
}
|
|
2158
|
-
return openSync2(
|
|
2213
|
+
return openSync2(join5(configDir, "daemon.log"), "a", LOG_FILE_MODE);
|
|
2159
2214
|
}
|
|
2160
2215
|
async function waitForReady() {
|
|
2161
2216
|
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
@@ -2667,7 +2722,7 @@ async function daemonEnsure() {
|
|
|
2667
2722
|
process.stderr.write("[prim] daemon remains explicitly disabled\n");
|
|
2668
2723
|
console.log(JSON.stringify({ ensured: false, disabled: true, supervised: false }, null, 2));
|
|
2669
2724
|
}
|
|
2670
|
-
return;
|
|
2725
|
+
return { disabled };
|
|
2671
2726
|
}
|
|
2672
2727
|
const result = await ensureMacDaemon();
|
|
2673
2728
|
if (result.state === "disabled") {
|
|
@@ -2694,6 +2749,7 @@ async function daemonEnsure() {
|
|
|
2694
2749
|
2
|
|
2695
2750
|
)
|
|
2696
2751
|
);
|
|
2752
|
+
return { disabled: result.state === "disabled" };
|
|
2697
2753
|
}
|
|
2698
2754
|
function registerDaemonCommands(program2) {
|
|
2699
2755
|
const daemon = program2.command("daemon").description("Manage the prim companion daemon (latency unlock + presence + broadcast)");
|
|
@@ -2709,13 +2765,17 @@ function registerDaemonCommands(program2) {
|
|
|
2709
2765
|
daemon.command("restart").description("Restart the daemon and clear any explicit disable marker").option("--foreground", "Restart in the foreground").action(async (opts) => {
|
|
2710
2766
|
await daemonRestart(opts);
|
|
2711
2767
|
});
|
|
2712
|
-
daemon.command("ensure").description("Idempotently install, upgrade, and heal the daemon unless explicitly disabled").action(async () => {
|
|
2768
|
+
daemon.command("ensure").description("Idempotently install, upgrade, and heal the daemon unless explicitly disabled").addOption(new Option("--latest-bootstrap").hideHelp()).action(async (opts) => {
|
|
2769
|
+
if (opts.latestBootstrap) {
|
|
2770
|
+
await runLatestDaemonBootstrap(daemonEnsure);
|
|
2771
|
+
return;
|
|
2772
|
+
}
|
|
2713
2773
|
await daemonEnsure();
|
|
2714
2774
|
});
|
|
2715
2775
|
}
|
|
2716
2776
|
|
|
2717
2777
|
// src/commands/decisions.ts
|
|
2718
|
-
import { Option } from "commander";
|
|
2778
|
+
import { Option as Option2 } from "commander";
|
|
2719
2779
|
|
|
2720
2780
|
// src/decisions/cascade-renderer.ts
|
|
2721
2781
|
var DEPENDENTS_INLINE_LIMIT = 5;
|
|
@@ -3517,7 +3577,7 @@ function registerDecisionsCommands(program2) {
|
|
|
3517
3577
|
}
|
|
3518
3578
|
});
|
|
3519
3579
|
decisions.command("create").description("Record a decision directly with its explicit user or agent origin").requiredOption("--intent <text>", "What was decided (required)").addOption(
|
|
3520
|
-
new
|
|
3580
|
+
new Option2(
|
|
3521
3581
|
"--attribution <origin>",
|
|
3522
3582
|
"Who originated the exact choice: user | agent (required)"
|
|
3523
3583
|
).choices(["user", "agent"]).makeOptionMandatory()
|
|
@@ -3848,6 +3908,46 @@ function checkWorkspaceIdentity() {
|
|
|
3848
3908
|
};
|
|
3849
3909
|
}
|
|
3850
3910
|
}
|
|
3911
|
+
function classifyRepositoryBinding(value) {
|
|
3912
|
+
return isValidRepoSyncId(value) ? {
|
|
3913
|
+
name: "repo-binding",
|
|
3914
|
+
status: "ok",
|
|
3915
|
+
detail: "valid local repository binding ready"
|
|
3916
|
+
} : {
|
|
3917
|
+
name: "repo-binding",
|
|
3918
|
+
status: "fail",
|
|
3919
|
+
detail: "missing or invalid local prim.repoSyncId \u2014 run `prim enable`"
|
|
3920
|
+
};
|
|
3921
|
+
}
|
|
3922
|
+
function checkRepositoryBinding() {
|
|
3923
|
+
return classifyRepositoryBinding(repoSyncId(process.cwd()));
|
|
3924
|
+
}
|
|
3925
|
+
function classifyPostCommitHook(inspection) {
|
|
3926
|
+
if (inspection.covered) {
|
|
3927
|
+
return {
|
|
3928
|
+
name: "post-commit",
|
|
3929
|
+
status: "ok",
|
|
3930
|
+
detail: `effective and executable \xB7 ${inspection.kind} \xB7 ${inspection.hookPath}`
|
|
3931
|
+
};
|
|
3932
|
+
}
|
|
3933
|
+
return {
|
|
3934
|
+
name: "post-commit",
|
|
3935
|
+
status: "fail",
|
|
3936
|
+
detail: `${inspection.reason ?? "uncovered"} \xB7 ${inspection.hookPath}`
|
|
3937
|
+
};
|
|
3938
|
+
}
|
|
3939
|
+
function checkPostCommitHook() {
|
|
3940
|
+
try {
|
|
3941
|
+
return classifyPostCommitHook(inspectEffectivePostCommitHook());
|
|
3942
|
+
} catch (error) {
|
|
3943
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
3944
|
+
return {
|
|
3945
|
+
name: "post-commit",
|
|
3946
|
+
status: /not a git repository/iu.test(detail) ? "warn" : "fail",
|
|
3947
|
+
detail: detail.slice(0, 120)
|
|
3948
|
+
};
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3851
3951
|
function checkFeedbackHooks() {
|
|
3852
3952
|
try {
|
|
3853
3953
|
const status = performStatus();
|
|
@@ -3877,6 +3977,9 @@ function parseMovesStatus(value) {
|
|
|
3877
3977
|
if (status.captureState !== "enabled" && status.captureState !== "disabled" || !nullableNumber(status.latestIngestAt) || !nullableNumber(status.latestClassificationAt) || !nullableNumber(status.highWaterMark) || typeof status.pendingSessionCount !== "number" || !Number.isInteger(status.pendingSessionCount) || status.pendingSessionCount < 0 || typeof status.sampled !== "boolean") {
|
|
3878
3978
|
throw new Error("moves status returned an invalid response");
|
|
3879
3979
|
}
|
|
3980
|
+
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)) {
|
|
3981
|
+
throw new Error("moves status returned invalid pending-age fields");
|
|
3982
|
+
}
|
|
3880
3983
|
return status;
|
|
3881
3984
|
}
|
|
3882
3985
|
function classifyMovesStatus(status) {
|
|
@@ -3887,10 +3990,11 @@ function classifyMovesStatus(status) {
|
|
|
3887
3990
|
};
|
|
3888
3991
|
let classification;
|
|
3889
3992
|
if (status.pendingSessionCount > 0) {
|
|
3993
|
+
const oldest = typeof status.oldestPendingAgeMs === "number" ? ` \xB7 oldest ${String(Math.round(status.oldestPendingAgeMs / MS_PER_SECOND))}s` : typeof status.oldestPendingAt === "number" ? ` \xB7 oldest ${String(Math.max(0, Math.round((Date.now() - status.oldestPendingAt) / MS_PER_SECOND)))}s` : "";
|
|
3890
3994
|
classification = {
|
|
3891
3995
|
name: "classification",
|
|
3892
3996
|
status: "warn",
|
|
3893
|
-
detail: `${String(status.pendingSessionCount)} session(s) pending${status.sampled ? " in a bounded sample" : ""}`
|
|
3997
|
+
detail: `${String(status.pendingSessionCount)} session(s) pending${oldest}${status.sampled ? " in a bounded sample" : ""}`
|
|
3894
3998
|
};
|
|
3895
3999
|
} else if (status.sampled) {
|
|
3896
4000
|
classification = {
|
|
@@ -3905,7 +4009,16 @@ function classifyMovesStatus(status) {
|
|
|
3905
4009
|
detail: status.latestClassificationAt === null ? "no sessions awaiting classification" : `caught up \xB7 last ${new Date(status.latestClassificationAt).toISOString()}`
|
|
3906
4010
|
};
|
|
3907
4011
|
}
|
|
3908
|
-
|
|
4012
|
+
const correlation = status.pendingCommitCorrelationCount === void 0 ? void 0 : status.pendingCommitCorrelationCount > 0 ? {
|
|
4013
|
+
name: "commit-correlation",
|
|
4014
|
+
status: "warn",
|
|
4015
|
+
detail: `${String(status.pendingCommitCorrelationCount)} commit(s) awaiting evidence correlation`
|
|
4016
|
+
} : {
|
|
4017
|
+
name: "commit-correlation",
|
|
4018
|
+
status: "ok",
|
|
4019
|
+
detail: "caught up"
|
|
4020
|
+
};
|
|
4021
|
+
return correlation ? [capture, classification, correlation] : [capture, classification];
|
|
3909
4022
|
}
|
|
3910
4023
|
async function checkBackend() {
|
|
3911
4024
|
try {
|
|
@@ -3954,6 +4067,8 @@ async function collectChecks() {
|
|
|
3954
4067
|
checkStranded(),
|
|
3955
4068
|
checkFeedbackHooks(),
|
|
3956
4069
|
checkWorkspaceIdentity(),
|
|
4070
|
+
checkRepositoryBinding(),
|
|
4071
|
+
checkPostCommitHook(),
|
|
3957
4072
|
...backend,
|
|
3958
4073
|
await checkFeedbackCapability()
|
|
3959
4074
|
];
|
|
@@ -3997,8 +4112,8 @@ import {
|
|
|
3997
4112
|
rmSync as rmSync2,
|
|
3998
4113
|
writeFileSync as writeFileSync3
|
|
3999
4114
|
} from "fs";
|
|
4000
|
-
import { homedir as
|
|
4001
|
-
import { dirname as dirname3, join as
|
|
4115
|
+
import { homedir as homedir6 } from "os";
|
|
4116
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
4002
4117
|
import { Document, parseDocument, stringify } from "yaml";
|
|
4003
4118
|
var CAPTURE_BIN3 = "prim-hook";
|
|
4004
4119
|
var GATE_BIN3 = "prim-pre-tool-use";
|
|
@@ -4023,13 +4138,13 @@ var PINNED_CASES = PRIM_BINS3.map((bin) => {
|
|
|
4023
4138
|
;;`;
|
|
4024
4139
|
}).join("\n");
|
|
4025
4140
|
function hermesHome() {
|
|
4026
|
-
return process.env.HERMES_HOME ??
|
|
4141
|
+
return process.env.HERMES_HOME ?? join6(homedir6(), ".hermes");
|
|
4027
4142
|
}
|
|
4028
4143
|
function configPath() {
|
|
4029
|
-
return
|
|
4144
|
+
return join6(hermesHome(), "config.yaml");
|
|
4030
4145
|
}
|
|
4031
4146
|
function shimPath() {
|
|
4032
|
-
return
|
|
4147
|
+
return join6(hermesHome(), "agent-hooks", "prim-shim.sh");
|
|
4033
4148
|
}
|
|
4034
4149
|
var SHIM_SCRIPT = `#!/bin/sh
|
|
4035
4150
|
# prim Hermes hook shim \u2014 managed by \`prim hermes install\`. Hermes runs hooks
|
|
@@ -4331,12 +4446,13 @@ function registerHermesCommands(program2) {
|
|
|
4331
4446
|
// src/commands/hooks.ts
|
|
4332
4447
|
import { execFileSync } from "child_process";
|
|
4333
4448
|
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
4334
|
-
import { homedir as
|
|
4335
|
-
import { dirname as dirname4, join as
|
|
4336
|
-
import { Option as
|
|
4449
|
+
import { homedir as homedir7 } from "os";
|
|
4450
|
+
import { dirname as dirname4, join as join7, resolve as resolve2 } from "path";
|
|
4451
|
+
import { Option as Option3 } from "commander";
|
|
4337
4452
|
var PRE_COMMIT = { hookName: "pre-commit", binName: "prim-pre-commit" };
|
|
4338
4453
|
var POST_COMMIT = { hookName: "post-commit", binName: "prim-post-commit" };
|
|
4339
4454
|
var HOOKS = [PRE_COMMIT, POST_COMMIT];
|
|
4455
|
+
var GIT_TIMEOUT_MS = 1e3;
|
|
4340
4456
|
function blockMarkers(spec) {
|
|
4341
4457
|
return {
|
|
4342
4458
|
start: `# >>> prim ${spec.hookName} hook >>>`,
|
|
@@ -4363,12 +4479,14 @@ ${hookShim(spec.binName)}
|
|
|
4363
4479
|
`;
|
|
4364
4480
|
}
|
|
4365
4481
|
function huskyBlock(spec) {
|
|
4482
|
+
if (spec.hookName === POST_COMMIT.hookName) return postCommitHookBlock();
|
|
4366
4483
|
const { start, end } = blockMarkers(spec);
|
|
4367
4484
|
return `${start}
|
|
4368
4485
|
${hookShim(spec.binName)}
|
|
4369
4486
|
${end}`;
|
|
4370
4487
|
}
|
|
4371
4488
|
function gatedBlock(spec) {
|
|
4489
|
+
if (spec.hookName === POST_COMMIT.hookName) return postCommitHookBlock();
|
|
4372
4490
|
const { start, end } = blockMarkers(spec);
|
|
4373
4491
|
return `${start}
|
|
4374
4492
|
${gatedShim(spec.binName)}
|
|
@@ -4377,6 +4495,21 @@ ${end}`;
|
|
|
4377
4495
|
function mergePrimBlock(hookPath, block, binName) {
|
|
4378
4496
|
if (existsSync7(hookPath)) {
|
|
4379
4497
|
const existing = readFileSync6(hookPath, "utf-8");
|
|
4498
|
+
const start = block.slice(0, block.indexOf("\n"));
|
|
4499
|
+
const end = block.slice(block.lastIndexOf("\n") + 1);
|
|
4500
|
+
const starts = existing.split(start).length - 1;
|
|
4501
|
+
const ends = existing.split(end).length - 1;
|
|
4502
|
+
if (starts !== 0 || ends !== 0) {
|
|
4503
|
+
if (starts !== 1 || ends !== 1 || existing.indexOf(end) < existing.indexOf(start)) {
|
|
4504
|
+
throw new Error(`malformed Prim hook markers in ${hookPath}`);
|
|
4505
|
+
}
|
|
4506
|
+
const from = existing.indexOf(start);
|
|
4507
|
+
const through = existing.indexOf(end) + end.length;
|
|
4508
|
+
const refreshed = existing.slice(0, from) + block + existing.slice(through);
|
|
4509
|
+
if (refreshed === existing) return false;
|
|
4510
|
+
writeFileSync4(hookPath, refreshed, { mode: 493 });
|
|
4511
|
+
return true;
|
|
4512
|
+
}
|
|
4380
4513
|
if (containsPrimHook(existing, binName)) return false;
|
|
4381
4514
|
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
4382
4515
|
writeFileSync4(hookPath, `${existing}${separator}${block}
|
|
@@ -4432,7 +4565,7 @@ function installToHusky(gitRoot, spec = PRE_COMMIT) {
|
|
|
4432
4565
|
}
|
|
4433
4566
|
}
|
|
4434
4567
|
function installToDotGit(gitRoot, spec = PRE_COMMIT) {
|
|
4435
|
-
const hooksDir =
|
|
4568
|
+
const hooksDir = projectHooksDir(gitRoot);
|
|
4436
4569
|
const hookPath = resolve2(hooksDir, spec.hookName);
|
|
4437
4570
|
if (!existsSync7(hooksDir)) {
|
|
4438
4571
|
mkdirSync5(hooksDir, { recursive: true });
|
|
@@ -4450,9 +4583,21 @@ function installToDotGit(gitRoot, spec = PRE_COMMIT) {
|
|
|
4450
4583
|
writeFileSync4(hookPath, dotGitScript(spec), { mode: 493 });
|
|
4451
4584
|
console.log(`Installed ${spec.hookName} hook at ${hookPath}`);
|
|
4452
4585
|
}
|
|
4453
|
-
|
|
4586
|
+
function projectHooksDir(gitRoot) {
|
|
4587
|
+
const commonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
|
|
4588
|
+
cwd: gitRoot,
|
|
4589
|
+
encoding: "utf-8",
|
|
4590
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
4591
|
+
timeout: GIT_TIMEOUT_MS
|
|
4592
|
+
}).trim();
|
|
4593
|
+
if (commonDir === "") {
|
|
4594
|
+
throw new Error("git returned an empty common directory");
|
|
4595
|
+
}
|
|
4596
|
+
return resolve2(gitRoot, commonDir, "hooks");
|
|
4597
|
+
}
|
|
4598
|
+
var PRIM_GIT_HOOKS_DIR = join7(homedir7(), ".config", "prim", "git-hooks");
|
|
4454
4599
|
function expandTilde(p) {
|
|
4455
|
-
return p.startsWith("~/") ?
|
|
4600
|
+
return p.startsWith("~/") ? join7(homedir7(), p.slice(2)) : p;
|
|
4456
4601
|
}
|
|
4457
4602
|
function isOurHooksDir(value) {
|
|
4458
4603
|
return value !== "" && expandTilde(value) === PRIM_GIT_HOOKS_DIR;
|
|
@@ -4461,7 +4606,8 @@ function gitConfigGet(level) {
|
|
|
4461
4606
|
try {
|
|
4462
4607
|
return execFileSync("git", ["config", level, "--get", "core.hooksPath"], {
|
|
4463
4608
|
encoding: "utf-8",
|
|
4464
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
4609
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
4610
|
+
timeout: GIT_TIMEOUT_MS
|
|
4465
4611
|
}).trim();
|
|
4466
4612
|
} catch {
|
|
4467
4613
|
return "";
|
|
@@ -4489,14 +4635,18 @@ var PASSTHROUGH_HOOKS = [
|
|
|
4489
4635
|
];
|
|
4490
4636
|
function globalHookScript(spec) {
|
|
4491
4637
|
const chainExit = spec.hookName === "pre-commit" ? "|| exit $?" : "|| true";
|
|
4638
|
+
const invocation = spec.hookName === POST_COMMIT.hookName ? postCommitHookBlock() : gatedShim(spec.binName);
|
|
4639
|
+
const beforeComments = spec.hookName === POST_COMMIT.hookName ? `${invocation}
|
|
4640
|
+
` : "";
|
|
4641
|
+
const afterComments = spec.hookName === POST_COMMIT.hookName ? "" : `${invocation}
|
|
4642
|
+
`;
|
|
4492
4643
|
return `#!/bin/sh
|
|
4493
|
-
# prim global ${spec.hookName} hook (core.hooksPath) \u2014 managed by prim; do not edit.
|
|
4644
|
+
${beforeComments}# prim global ${spec.hookName} hook (core.hooksPath) \u2014 managed by prim; do not edit.
|
|
4494
4645
|
# Install/uninstall: prim hooks install|uninstall --scope user
|
|
4495
4646
|
# Runs prim only where activated \u2014 'prim enable' (this repo) or
|
|
4496
4647
|
# 'git config --global prim.active true' (every repo). Chains to the repo's own
|
|
4497
4648
|
# hook regardless, so inactive repos are unaffected.
|
|
4498
|
-
${
|
|
4499
|
-
common_dir=$(git rev-parse --git-common-dir 2>/dev/null) || exit 0
|
|
4649
|
+
${afterComments}common_dir=$(git rev-parse --git-common-dir 2>/dev/null) || exit 0
|
|
4500
4650
|
repo_hook="$common_dir/hooks/${spec.hookName}"
|
|
4501
4651
|
if [ -x "$repo_hook" ] && ! grep -q '${PRIM_MANAGED_MARK}' "$repo_hook" 2>/dev/null; then
|
|
4502
4652
|
"$repo_hook" "$@" ${chainExit}
|
|
@@ -4521,18 +4671,29 @@ function writeOwnHooks() {
|
|
|
4521
4671
|
mkdirSync5(PRIM_GIT_HOOKS_DIR, { recursive: true });
|
|
4522
4672
|
}
|
|
4523
4673
|
for (const spec of HOOKS) {
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4674
|
+
const path = resolve2(PRIM_GIT_HOOKS_DIR, spec.hookName);
|
|
4675
|
+
if (spec.hookName === POST_COMMIT.hookName) {
|
|
4676
|
+
ensurePostCommitHookAtPath(path, globalHookScript(spec));
|
|
4677
|
+
} else {
|
|
4678
|
+
writeFileSync4(path, globalHookScript(spec), { mode: 493 });
|
|
4679
|
+
}
|
|
4527
4680
|
}
|
|
4528
4681
|
for (const name of PASSTHROUGH_HOOKS) {
|
|
4529
4682
|
writeFileSync4(resolve2(PRIM_GIT_HOOKS_DIR, name), passThroughScript(name), { mode: 493 });
|
|
4530
4683
|
}
|
|
4531
4684
|
}
|
|
4532
4685
|
function appendPrimBlock(hookPath, spec) {
|
|
4686
|
+
if (spec.hookName === POST_COMMIT.hookName) {
|
|
4687
|
+
ensurePostCommitHookAtPath(hookPath);
|
|
4688
|
+
return;
|
|
4689
|
+
}
|
|
4533
4690
|
mergePrimBlock(hookPath, gatedBlock(spec), spec.binName);
|
|
4534
4691
|
}
|
|
4535
4692
|
function stripPrimBlock(hookPath, spec) {
|
|
4693
|
+
if (spec.hookName === POST_COMMIT.hookName) {
|
|
4694
|
+
uninstallPostCommitHookAtPath(hookPath);
|
|
4695
|
+
return;
|
|
4696
|
+
}
|
|
4536
4697
|
if (!existsSync7(hookPath)) return;
|
|
4537
4698
|
const existing = readFileSync6(hookPath, "utf-8");
|
|
4538
4699
|
const primCreated = existing.includes(PRIM_CREATED_MARK);
|
|
@@ -4564,7 +4725,10 @@ function installGlobalHooks(opts = {}) {
|
|
|
4564
4725
|
);
|
|
4565
4726
|
}
|
|
4566
4727
|
writeOwnHooks();
|
|
4567
|
-
execFileSync("git", ["config", "--global", "core.hooksPath", PRIM_GIT_HOOKS_DIR]
|
|
4728
|
+
execFileSync("git", ["config", "--global", "core.hooksPath", PRIM_GIT_HOOKS_DIR], {
|
|
4729
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
4730
|
+
timeout: GIT_TIMEOUT_MS
|
|
4731
|
+
});
|
|
4568
4732
|
console.log(
|
|
4569
4733
|
`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.`
|
|
4570
4734
|
);
|
|
@@ -4591,7 +4755,10 @@ function uninstallGlobalHooks() {
|
|
|
4591
4755
|
const p = resolve2(PRIM_GIT_HOOKS_DIR, name);
|
|
4592
4756
|
if (existsSync7(p)) unlinkSync2(p);
|
|
4593
4757
|
}
|
|
4594
|
-
execFileSync("git", ["config", "--global", "--unset", "core.hooksPath"]
|
|
4758
|
+
execFileSync("git", ["config", "--global", "--unset", "core.hooksPath"], {
|
|
4759
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
4760
|
+
timeout: GIT_TIMEOUT_MS
|
|
4761
|
+
});
|
|
4595
4762
|
console.log("Removed prim global git hooks and unset core.hooksPath.");
|
|
4596
4763
|
return;
|
|
4597
4764
|
}
|
|
@@ -4606,25 +4773,29 @@ function uninstallGlobalHooks() {
|
|
|
4606
4773
|
console.log("No prim global git hooks found.");
|
|
4607
4774
|
}
|
|
4608
4775
|
function installHooks(gitRoot, target) {
|
|
4609
|
-
for (const spec of HOOKS) {
|
|
4776
|
+
for (const spec of HOOKS.filter((candidate) => candidate !== POST_COMMIT)) {
|
|
4610
4777
|
if (target === "husky") {
|
|
4611
4778
|
installToHusky(gitRoot, spec);
|
|
4612
4779
|
} else {
|
|
4613
4780
|
installToDotGit(gitRoot, spec);
|
|
4614
4781
|
}
|
|
4615
4782
|
}
|
|
4783
|
+
const postCommit = ensureEffectivePostCommitHook(gitRoot);
|
|
4784
|
+
console.log(
|
|
4785
|
+
`${postCommit.changed ? "Installed" : "Refreshed"} effective post-commit hook at ${postCommit.path}.`
|
|
4786
|
+
);
|
|
4616
4787
|
}
|
|
4617
4788
|
function registerHooksCommands(program2) {
|
|
4618
4789
|
const hooks = program2.command("hooks").description("Manage git hooks");
|
|
4619
4790
|
hooks.command("install").description(
|
|
4620
4791
|
"Install the prim git hooks \u2014 pre-commit + post-commit (auto-detects Husky; use --target to override)"
|
|
4621
4792
|
).addOption(
|
|
4622
|
-
new
|
|
4793
|
+
new Option3("--target <where>", "install destination; bypasses Husky detection").choices([
|
|
4623
4794
|
"husky",
|
|
4624
4795
|
"git-hooks"
|
|
4625
4796
|
])
|
|
4626
4797
|
).addOption(
|
|
4627
|
-
new
|
|
4798
|
+
new Option3(
|
|
4628
4799
|
"--scope <scope>",
|
|
4629
4800
|
"project (default, this repo) or user (a global core.hooksPath capturing every repo)"
|
|
4630
4801
|
).choices(["project", "user"])
|
|
@@ -4664,7 +4835,7 @@ function registerHooksCommands(program2) {
|
|
|
4664
4835
|
hooks.command("uninstall").description(
|
|
4665
4836
|
"Remove the prim git hooks (.git/hooks, or the global core.hooksPath with --scope user)"
|
|
4666
4837
|
).addOption(
|
|
4667
|
-
new
|
|
4838
|
+
new Option3(
|
|
4668
4839
|
"--scope <scope>",
|
|
4669
4840
|
"project (default, this repo) or user (global core.hooksPath)"
|
|
4670
4841
|
).choices(["project", "user"])
|
|
@@ -4674,8 +4845,9 @@ function registerHooksCommands(program2) {
|
|
|
4674
4845
|
return;
|
|
4675
4846
|
}
|
|
4676
4847
|
const gitRoot = getGitRoot();
|
|
4677
|
-
|
|
4678
|
-
|
|
4848
|
+
const hooksDir = projectHooksDir(gitRoot);
|
|
4849
|
+
for (const spec of HOOKS.filter((candidate) => candidate !== POST_COMMIT)) {
|
|
4850
|
+
const hookPath = resolve2(hooksDir, spec.hookName);
|
|
4679
4851
|
if (!existsSync7(hookPath)) {
|
|
4680
4852
|
console.log(`No ${spec.hookName} hook found.`);
|
|
4681
4853
|
continue;
|
|
@@ -4687,16 +4859,24 @@ function registerHooksCommands(program2) {
|
|
|
4687
4859
|
console.log(`Left ${spec.hookName} hook at ${hookPath} untouched (not a prim hook).`);
|
|
4688
4860
|
}
|
|
4689
4861
|
}
|
|
4862
|
+
const postCommit = uninstallProjectPostCommitHook(gitRoot);
|
|
4863
|
+
if (!postCommit.changed) {
|
|
4864
|
+
console.log(`No Prim post-commit block found at ${postCommit.path}.`);
|
|
4865
|
+
} else if (postCommit.removedFile) {
|
|
4866
|
+
console.log(`Removed Prim-created post-commit hook at ${postCommit.path}.`);
|
|
4867
|
+
} else {
|
|
4868
|
+
console.log(`Removed the Prim post-commit block from ${postCommit.path}.`);
|
|
4869
|
+
}
|
|
4690
4870
|
});
|
|
4691
4871
|
}
|
|
4692
4872
|
|
|
4693
4873
|
// src/commands/moves.ts
|
|
4694
4874
|
import { existsSync as existsSync8, mkdirSync as mkdirSync6, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
4695
|
-
import { join as
|
|
4875
|
+
import { join as join9 } from "path";
|
|
4696
4876
|
|
|
4697
4877
|
// src/flusher.ts
|
|
4698
4878
|
import { createReadStream, renameSync as renameSync4, unlinkSync as unlinkSync3 } from "fs";
|
|
4699
|
-
import { dirname as dirname5, join as
|
|
4879
|
+
import { dirname as dirname5, join as join8 } from "path";
|
|
4700
4880
|
import { createInterface } from "readline";
|
|
4701
4881
|
var BATCH_SIZE = 500;
|
|
4702
4882
|
var HTTP_TIMEOUT_MS = 1e4;
|
|
@@ -4820,7 +5000,7 @@ async function flushOnce() {
|
|
|
4820
5000
|
return { flushed: total };
|
|
4821
5001
|
}
|
|
4822
5002
|
var flushInFlight;
|
|
4823
|
-
var FLUSH_LOCK_PATH =
|
|
5003
|
+
var FLUSH_LOCK_PATH = join8(dirname5(JOURNAL_DIR), ".flush.lock");
|
|
4824
5004
|
var FLUSH_LOCK_TIMEOUT_MS = 250;
|
|
4825
5005
|
function isFlushLockContended(error) {
|
|
4826
5006
|
return error instanceof Error && error.message.startsWith("timed out waiting for file lock");
|
|
@@ -4937,18 +5117,18 @@ function registerMovesCommands(program2) {
|
|
|
4937
5117
|
}
|
|
4938
5118
|
});
|
|
4939
5119
|
moves.command("bind").description("Pin the current directory to an org via .prim/workspace.json").requiredOption("--orgId <orgId>", "Convex organization id").action((opts) => {
|
|
4940
|
-
const dir =
|
|
5120
|
+
const dir = join9(process.cwd(), ".prim");
|
|
4941
5121
|
if (!existsSync8(dir)) {
|
|
4942
5122
|
mkdirSync6(dir, { recursive: true, mode: DIR_MODE });
|
|
4943
5123
|
}
|
|
4944
|
-
const file =
|
|
5124
|
+
const file = join9(process.cwd(), WORKSPACE_FILE);
|
|
4945
5125
|
writeFileSync5(file, JSON.stringify({ orgId: opts.orgId, boundAt: Date.now() }, null, 2), {
|
|
4946
5126
|
mode: FILE_MODE
|
|
4947
5127
|
});
|
|
4948
5128
|
console.log(`[prim] bound ${process.cwd()} to org ${opts.orgId}`);
|
|
4949
5129
|
});
|
|
4950
5130
|
moves.command("drop").description("Remove the .prim/workspace.json binding from the cwd").action(() => {
|
|
4951
|
-
const file =
|
|
5131
|
+
const file = join9(process.cwd(), WORKSPACE_FILE);
|
|
4952
5132
|
if (!existsSync8(file)) {
|
|
4953
5133
|
console.log("[prim] no workspace binding in cwd");
|
|
4954
5134
|
return;
|
|
@@ -5047,7 +5227,7 @@ import {
|
|
|
5047
5227
|
unlinkSync as unlinkSync5,
|
|
5048
5228
|
writeFileSync as writeFileSync6
|
|
5049
5229
|
} from "fs";
|
|
5050
|
-
import { join as
|
|
5230
|
+
import { join as join10 } from "path";
|
|
5051
5231
|
var DIR_MODE2 = 448;
|
|
5052
5232
|
var FILE_MODE2 = 384;
|
|
5053
5233
|
function ensureDir() {
|
|
@@ -5056,7 +5236,7 @@ function ensureDir() {
|
|
|
5056
5236
|
}
|
|
5057
5237
|
}
|
|
5058
5238
|
function markerPath(sessionId) {
|
|
5059
|
-
return
|
|
5239
|
+
return join10(SESSIONS_DIR, `${sessionId}.json`);
|
|
5060
5240
|
}
|
|
5061
5241
|
function registerSessionCommands(program2) {
|
|
5062
5242
|
const session = program2.command("session").description("Decision Event Pipeline \u2014 session binding markers");
|
|
@@ -5084,7 +5264,7 @@ function registerSessionCommands(program2) {
|
|
|
5084
5264
|
for (const f of files) {
|
|
5085
5265
|
const sessionId = f.replace(/\.json$/, "");
|
|
5086
5266
|
try {
|
|
5087
|
-
const m = JSON.parse(readFileSync7(
|
|
5267
|
+
const m = JSON.parse(readFileSync7(join10(SESSIONS_DIR, f), "utf-8"));
|
|
5088
5268
|
console.log(`${sessionId} org=${m.orgId}`);
|
|
5089
5269
|
} catch {
|
|
5090
5270
|
}
|
|
@@ -5104,7 +5284,7 @@ function registerSessionCommands(program2) {
|
|
|
5104
5284
|
// src/commands/setup.ts
|
|
5105
5285
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
5106
5286
|
import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
|
|
5107
|
-
import { join as
|
|
5287
|
+
import { join as join11 } from "path";
|
|
5108
5288
|
var EXIT_INCOMPLETE = 1;
|
|
5109
5289
|
var EXIT_USAGE3 = 2;
|
|
5110
5290
|
var SESSION_LABELS = {
|
|
@@ -5130,12 +5310,6 @@ function planSetupSteps(opts) {
|
|
|
5130
5310
|
args: ["daemon", "start"],
|
|
5131
5311
|
required: true
|
|
5132
5312
|
});
|
|
5133
|
-
steps.push({
|
|
5134
|
-
key: "health",
|
|
5135
|
-
label: "Capture health",
|
|
5136
|
-
args: ["doctor"],
|
|
5137
|
-
required: true
|
|
5138
|
-
});
|
|
5139
5313
|
} else {
|
|
5140
5314
|
steps.push({
|
|
5141
5315
|
key: "daemon-opt-out",
|
|
@@ -5154,10 +5328,18 @@ function planSetupSteps(opts) {
|
|
|
5154
5328
|
steps.push({ key: "skill", label: "Agent skill", args: skillArgs, required: true });
|
|
5155
5329
|
steps.push({
|
|
5156
5330
|
key: "enable",
|
|
5157
|
-
label: "
|
|
5331
|
+
label: "Activate this repo",
|
|
5158
5332
|
args: ["enable"],
|
|
5159
5333
|
required: true
|
|
5160
5334
|
});
|
|
5335
|
+
if (opts.daemon) {
|
|
5336
|
+
steps.push({
|
|
5337
|
+
key: "health",
|
|
5338
|
+
label: "Capture health",
|
|
5339
|
+
args: ["doctor"],
|
|
5340
|
+
required: true
|
|
5341
|
+
});
|
|
5342
|
+
}
|
|
5161
5343
|
return steps;
|
|
5162
5344
|
}
|
|
5163
5345
|
var CONFLICT_SESSION = "session";
|
|
@@ -5183,7 +5365,7 @@ function detectProjectConflicts(agent, run) {
|
|
|
5183
5365
|
}
|
|
5184
5366
|
try {
|
|
5185
5367
|
const root = gitToplevel();
|
|
5186
|
-
const preCommit = root &&
|
|
5368
|
+
const preCommit = root && join11(root, ".git", "hooks", "pre-commit");
|
|
5187
5369
|
if (preCommit && existsSync10(preCommit) && readFileSync8(preCommit, "utf-8").includes("prim-pre-commit")) {
|
|
5188
5370
|
conflicts.push(CONFLICT_HOOKS);
|
|
5189
5371
|
}
|
|
@@ -5295,7 +5477,10 @@ function registerSetupCommand(program2, dependencies = {}) {
|
|
|
5295
5477
|
note("pre-authorize \xB7 writing prim allow-rule (user scope)\u2026");
|
|
5296
5478
|
results.preauth = run(["claude", "preauth", "--scope", "user"]).code === 0 ? "ok" : "skipped";
|
|
5297
5479
|
}
|
|
5298
|
-
|
|
5480
|
+
const setupSteps = planSetupSteps({ agent, daemon: opts.daemon, scope });
|
|
5481
|
+
for (const step of setupSteps.filter(
|
|
5482
|
+
(candidate) => candidate.key !== "enable" && candidate.key !== "health"
|
|
5483
|
+
)) {
|
|
5299
5484
|
note(`${step.label} \xB7 installing\u2026`);
|
|
5300
5485
|
const { code } = run(step.args);
|
|
5301
5486
|
results[step.key] = code === 0 ? "ok" : step.required ? "failed" : "skipped";
|
|
@@ -5312,6 +5497,18 @@ function registerSetupCommand(program2, dependencies = {}) {
|
|
|
5312
5497
|
);
|
|
5313
5498
|
}
|
|
5314
5499
|
}
|
|
5500
|
+
const enableStep = setupSteps.find((candidate) => candidate.key === "enable");
|
|
5501
|
+
if (enableStep) {
|
|
5502
|
+
note(`${enableStep.label} \xB7 installing\u2026`);
|
|
5503
|
+
const { code } = run(enableStep.args);
|
|
5504
|
+
results[enableStep.key] = code === 0 ? "ok" : enableStep.required ? "failed" : "skipped";
|
|
5505
|
+
}
|
|
5506
|
+
const healthStep = setupSteps.find((candidate) => candidate.key === "health");
|
|
5507
|
+
if (healthStep) {
|
|
5508
|
+
note(`${healthStep.label} \xB7 verifying\u2026`);
|
|
5509
|
+
const { code } = run(healthStep.args);
|
|
5510
|
+
results[healthStep.key] = code === 0 ? "ok" : healthStep.required ? "failed" : "skipped";
|
|
5511
|
+
}
|
|
5315
5512
|
note("welcome");
|
|
5316
5513
|
results.welcome = run(["welcome", "--agent", agent]).code === 0 ? "ok" : "failed";
|
|
5317
5514
|
const failed = Object.entries(results).filter(([, v]) => v === "failed").map(([k]) => k);
|
|
@@ -5327,14 +5524,55 @@ function registerSetupCommand(program2, dependencies = {}) {
|
|
|
5327
5524
|
import { readFileSync as readFileSync9 } from "fs";
|
|
5328
5525
|
import { dirname as dirname6, resolve as resolve3 } from "path";
|
|
5329
5526
|
import { fileURLToPath } from "url";
|
|
5330
|
-
|
|
5527
|
+
|
|
5528
|
+
// src/lib/statusline-render.ts
|
|
5331
5529
|
var STATUSLINE_NAME_CAP = 3;
|
|
5530
|
+
function formatStatusline(version, snapshot, resolveIngestionStatus) {
|
|
5531
|
+
if (!snapshot) {
|
|
5532
|
+
return `primitive ${version} (daemon: down)`;
|
|
5533
|
+
}
|
|
5534
|
+
if (snapshot.healthy === false) {
|
|
5535
|
+
if (snapshot.needsReauth) {
|
|
5536
|
+
return `primitive ${version} (daemon: paused \xB7 run \`prim auth login\`)`;
|
|
5537
|
+
}
|
|
5538
|
+
if (snapshot.ingestion?.healthy === false) {
|
|
5539
|
+
const pending = snapshot.ingestion.pendingCount;
|
|
5540
|
+
const qualifier = snapshot.ingestion.pendingSampled ? "at least " : "";
|
|
5541
|
+
return `primitive ${version} (daemon: degraded \xB7 delivery: stalled${typeof pending === "number" ? ` \xB7 ${qualifier}${String(pending)} pending` : ""})`;
|
|
5542
|
+
}
|
|
5543
|
+
if (snapshot.heartbeat?.healthy === false) {
|
|
5544
|
+
return `primitive ${version} (daemon: degraded \xB7 presence: unavailable)`;
|
|
5545
|
+
}
|
|
5546
|
+
return `primitive ${version} (daemon: starting)`;
|
|
5547
|
+
}
|
|
5548
|
+
const ingestionStatus = resolveIngestionStatus();
|
|
5549
|
+
if (snapshot.envMismatch) {
|
|
5550
|
+
return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 presence: other env)`;
|
|
5551
|
+
}
|
|
5552
|
+
if (snapshot.presenceStale) {
|
|
5553
|
+
return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 presence: stale)`;
|
|
5554
|
+
}
|
|
5555
|
+
let team;
|
|
5556
|
+
if (snapshot.onlineTeammates !== void 0) {
|
|
5557
|
+
team = `team: ${formatTeammatesWithArea(snapshot.onlineTeammates, STATUSLINE_NAME_CAP)}`;
|
|
5558
|
+
} else if (snapshot.onlineNames !== void 0) {
|
|
5559
|
+
team = `team: ${formatTeammates(snapshot.onlineNames, STATUSLINE_NAME_CAP)}`;
|
|
5560
|
+
} else if (typeof snapshot.onlineCount === "number") {
|
|
5561
|
+
team = `team: ${String(snapshot.onlineCount)} online`;
|
|
5562
|
+
} else {
|
|
5563
|
+
team = "team: \u2014";
|
|
5564
|
+
}
|
|
5565
|
+
return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 ${team})`;
|
|
5566
|
+
}
|
|
5567
|
+
|
|
5568
|
+
// src/commands/statusline.ts
|
|
5569
|
+
var STATUSLINE_TIMEOUT_MS = 200;
|
|
5332
5570
|
function readPackageVersion() {
|
|
5333
5571
|
try {
|
|
5334
5572
|
const here = dirname6(fileURLToPath(import.meta.url));
|
|
5335
5573
|
const candidates = [
|
|
5336
|
-
//
|
|
5337
|
-
//
|
|
5574
|
+
// Schema-v2 runtimes staged a manifest beside this standalone bundle.
|
|
5575
|
+
// Keep resolving it while those legacy launchers transition to schema v3.
|
|
5338
5576
|
resolve3(here, "manifest.json"),
|
|
5339
5577
|
resolve3(here, "../../package.json"),
|
|
5340
5578
|
resolve3(here, "../package.json")
|
|
@@ -5369,40 +5607,8 @@ async function renderStatusline() {
|
|
|
5369
5607
|
);
|
|
5370
5608
|
if (!snapshot) {
|
|
5371
5609
|
debug("daemon snapshot missing");
|
|
5372
|
-
return `primitive ${version} (daemon: down)`;
|
|
5373
|
-
}
|
|
5374
|
-
if (snapshot.healthy === false) {
|
|
5375
|
-
if (snapshot.needsReauth) {
|
|
5376
|
-
return `primitive ${version} (daemon: paused \xB7 run \`prim auth login\`)`;
|
|
5377
|
-
}
|
|
5378
|
-
if (snapshot.ingestion?.healthy === false) {
|
|
5379
|
-
const pending = snapshot.ingestion.pendingCount;
|
|
5380
|
-
const qualifier = snapshot.ingestion.pendingSampled ? "at least " : "";
|
|
5381
|
-
return `primitive ${version} (daemon: degraded \xB7 delivery: stalled${typeof pending === "number" ? ` \xB7 ${qualifier}${String(pending)} pending` : ""})`;
|
|
5382
|
-
}
|
|
5383
|
-
if (snapshot.heartbeat?.healthy === false) {
|
|
5384
|
-
return `primitive ${version} (daemon: degraded \xB7 presence: unavailable)`;
|
|
5385
|
-
}
|
|
5386
|
-
return `primitive ${version} (daemon: starting)`;
|
|
5387
|
-
}
|
|
5388
|
-
const ingestionStatus = decisionIngestionStatus(process.cwd());
|
|
5389
|
-
if (snapshot.envMismatch) {
|
|
5390
|
-
return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 presence: other env)`;
|
|
5391
|
-
}
|
|
5392
|
-
if (snapshot.presenceStale) {
|
|
5393
|
-
return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 presence: stale)`;
|
|
5394
5610
|
}
|
|
5395
|
-
|
|
5396
|
-
if (snapshot.onlineTeammates !== void 0) {
|
|
5397
|
-
team = `team: ${formatTeammatesWithArea(snapshot.onlineTeammates, STATUSLINE_NAME_CAP)}`;
|
|
5398
|
-
} else if (snapshot.onlineNames !== void 0) {
|
|
5399
|
-
team = `team: ${formatTeammates(snapshot.onlineNames, STATUSLINE_NAME_CAP)}`;
|
|
5400
|
-
} else if (typeof snapshot.onlineCount === "number") {
|
|
5401
|
-
team = `team: ${String(snapshot.onlineCount)} online`;
|
|
5402
|
-
} else {
|
|
5403
|
-
team = "team: \u2014";
|
|
5404
|
-
}
|
|
5405
|
-
return `primitive ${version} (daemon: live, Decision ingestion ${ingestionStatus} \xB7 ${team})`;
|
|
5611
|
+
return formatStatusline(version, snapshot, () => decisionIngestionStatus(process.cwd()));
|
|
5406
5612
|
}
|
|
5407
5613
|
function registerStatuslineCommands(program2) {
|
|
5408
5614
|
program2.command("statusline").description("Render the Claude Code statusLine for the prim companion daemon").action(async () => {
|