@primitive.ai/prim 0.1.0-alpha.90 → 0.1.0-alpha.92
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 +20 -12
- package/SKILL.md +6 -6
- package/dist/chunk-26P57LAR.js +1491 -0
- package/dist/{chunk-LMRYZL3D.js → chunk-5AFRMOPX.js} +1 -1
- package/dist/{chunk-RO3F6JYV.js → chunk-5GYQ4J3D.js} +2 -2
- package/dist/chunk-5HRR4AOR.js +264 -0
- package/dist/{chunk-3EEFL6NJ.js → chunk-67VNLLBN.js} +261 -1
- package/dist/{chunk-UZPBB7V4.js → chunk-ADUTW66W.js} +60 -48
- package/dist/{chunk-LEMX3IOG.js → chunk-BTNNEH2W.js} +1 -1
- package/dist/{chunk-YWO7O6QG.js → chunk-FBAYCS3X.js} +2 -2
- package/dist/{chunk-R26A7TWY.js → chunk-FKXFUUWJ.js} +1 -1
- package/dist/chunk-H4UNYFUC.js +196 -0
- package/dist/{chunk-CUE2LKLR.js → chunk-KFXF2V5G.js} +1 -1
- package/dist/{chunk-6IWNH776.js → chunk-LFFLHSQO.js} +4 -3
- package/dist/{chunk-C7T7B45R.js → chunk-LGGMJYY4.js} +150 -21
- package/dist/{chunk-MAZWKJGL.js → chunk-M6RIJVRE.js} +42 -232
- package/dist/{chunk-2JRX7ZR4.js → chunk-NAHHFSLH.js} +16 -5
- package/dist/{chunk-FI3LIRCB.js → chunk-OAAP2MY6.js} +2 -2
- package/dist/{chunk-Z7YPFWAZ.js → chunk-PCVGBE5B.js} +10 -268
- package/dist/chunk-WX52U7Z2.js +112 -0
- package/dist/{chunk-PPQYQEIR.js → chunk-ZB4F75SZ.js} +28 -0
- package/dist/daemon/server.js +30 -21
- package/dist/hook-runtime/daemon/server.js +30 -21
- package/dist/hook-runtime/hooks/post-commit.js +20 -20
- package/dist/hook-runtime/hooks/post-rewrite.js +20 -20
- package/dist/hook-runtime/hooks/post-tool-use.js +666 -147
- package/dist/hook-runtime/hooks/pre-tool-use.js +659 -111
- package/dist/hook-runtime/hooks/prim-hook.js +627 -111
- package/dist/hook-runtime/hooks/session-end.js +610 -27
- package/dist/hook-runtime/hooks/session-start.js +1132 -533
- package/dist/hook-runtime/index.js +764 -267
- package/dist/hook-runtime/statusline-main.js +48 -4
- package/dist/hooks/post-commit.js +11 -9
- package/dist/hooks/post-rewrite.js +11 -9
- package/dist/hooks/post-tool-use.js +73 -48
- package/dist/hooks/pre-commit.js +4 -4
- package/dist/hooks/pre-tool-use.js +46 -44
- package/dist/hooks/prim-hook.js +47 -25
- package/dist/hooks/session-end.js +15 -26
- package/dist/hooks/session-start.js +59 -32
- package/dist/index.js +561 -1430
- package/dist/statusline-main.js +48 -4
- package/package.json +1 -1
- package/dist/chunk-PMRDE2DA.js +0 -310
- package/dist/chunk-WB7AUZ4C.js +0 -52
package/dist/index.js
CHANGED
|
@@ -17,18 +17,18 @@ import {
|
|
|
17
17
|
uninstallPostRewriteHookAtPath,
|
|
18
18
|
uninstallProjectPostCommitHook,
|
|
19
19
|
uninstallProjectPostRewriteHook
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-LGGMJYY4.js";
|
|
21
21
|
import {
|
|
22
22
|
requireDurableIngestAcknowledgement
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-FKXFUUWJ.js";
|
|
24
24
|
import {
|
|
25
25
|
checkAffectedDecisions,
|
|
26
26
|
daemonOrDirectGet,
|
|
27
27
|
formatDecisionsWarning
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-5GYQ4J3D.js";
|
|
29
29
|
import {
|
|
30
30
|
fetchFeedbackCapability
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-5AFRMOPX.js";
|
|
32
32
|
import {
|
|
33
33
|
JOURNAL_DIR,
|
|
34
34
|
SESSIONS_DIR,
|
|
@@ -37,24 +37,12 @@ import {
|
|
|
37
37
|
listFlushing,
|
|
38
38
|
pendingJournalStats,
|
|
39
39
|
readMovesFromPath
|
|
40
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-FBAYCS3X.js";
|
|
41
41
|
import {
|
|
42
|
-
STABLE_HOOK_LAUNCHER_NAME,
|
|
43
|
-
binFile,
|
|
44
|
-
commandMatchesBin,
|
|
45
|
-
compareSemver,
|
|
46
|
-
detachedHookShimCommand,
|
|
47
42
|
formatStatusline,
|
|
48
43
|
formatTeammates,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
packageRoot,
|
|
52
|
-
packageVersion,
|
|
53
|
-
pinnedHookCommand,
|
|
54
|
-
pinnedNpxArgs,
|
|
55
|
-
repositoryBindingDiagnosticLabel,
|
|
56
|
-
stableHookCommand
|
|
57
|
-
} from "./chunk-PMRDE2DA.js";
|
|
44
|
+
repositoryBindingDiagnosticLabel
|
|
45
|
+
} from "./chunk-WX52U7Z2.js";
|
|
58
46
|
import {
|
|
59
47
|
bold,
|
|
60
48
|
boundedHealthError,
|
|
@@ -62,13 +50,10 @@ import {
|
|
|
62
50
|
colorForArea,
|
|
63
51
|
dim,
|
|
64
52
|
stripAnsi
|
|
65
|
-
} from "./chunk-
|
|
53
|
+
} from "./chunk-BTNNEH2W.js";
|
|
66
54
|
import {
|
|
67
|
-
canonicalGitRoot,
|
|
68
|
-
canonicalRepositoryPath,
|
|
69
55
|
decisionIngestionStatus,
|
|
70
56
|
fetchAndCacheCollectScope,
|
|
71
|
-
gitToplevel,
|
|
72
57
|
isCliErrorResponse,
|
|
73
58
|
isDecisionIdRequest,
|
|
74
59
|
isDecisionRescopeRequest,
|
|
@@ -84,11 +69,10 @@ import {
|
|
|
84
69
|
repoSyncId,
|
|
85
70
|
repositoryBindingState,
|
|
86
71
|
setRepoActive
|
|
87
|
-
} from "./chunk-
|
|
72
|
+
} from "./chunk-M6RIJVRE.js";
|
|
88
73
|
import {
|
|
89
74
|
CliManagementResponseError,
|
|
90
75
|
HttpError,
|
|
91
|
-
atomicWriteFile,
|
|
92
76
|
clearStoredCredentials,
|
|
93
77
|
commitCredentials,
|
|
94
78
|
getClient,
|
|
@@ -97,34 +81,66 @@ import {
|
|
|
97
81
|
getSiteUrl,
|
|
98
82
|
getTokenExpiresAt,
|
|
99
83
|
isSessionEnded,
|
|
100
|
-
processIsAlive,
|
|
101
84
|
refreshToken,
|
|
102
85
|
setStoredToken,
|
|
103
86
|
stripControlChars,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
87
|
+
terminalSafeLine
|
|
88
|
+
} from "./chunk-PCVGBE5B.js";
|
|
89
|
+
import {
|
|
90
|
+
assertOwnedHookRuntime,
|
|
91
|
+
atomicWrite,
|
|
92
|
+
binFile,
|
|
93
|
+
commandMatchesBin,
|
|
94
|
+
compareSemver,
|
|
95
|
+
ensureRegistration,
|
|
96
|
+
entryHasCommand,
|
|
97
|
+
hookCommandResolutions,
|
|
98
|
+
hookRuntimePaths,
|
|
99
|
+
inspectHookRuntime,
|
|
100
|
+
inspectHookRuntimeResolutions,
|
|
101
|
+
inspectHookRuntimeResolutions2,
|
|
102
|
+
makeRegistration,
|
|
103
|
+
packageVersion,
|
|
104
|
+
performStatus,
|
|
105
|
+
performStatus2,
|
|
106
|
+
pinnedHookCommand,
|
|
107
|
+
pinnedNpxArgs,
|
|
108
|
+
projectRoot,
|
|
109
|
+
readSettings,
|
|
110
|
+
registerClaudeCommands,
|
|
111
|
+
registerCursorCommands,
|
|
112
|
+
removeHookRuntime,
|
|
113
|
+
stableHookCommand,
|
|
114
|
+
stageHookRuntime,
|
|
115
|
+
stripCommand
|
|
116
|
+
} from "./chunk-26P57LAR.js";
|
|
117
|
+
import {
|
|
118
|
+
canonicalGitRoot,
|
|
119
|
+
canonicalRepositoryPath,
|
|
120
|
+
gitToplevel
|
|
121
|
+
} from "./chunk-H4UNYFUC.js";
|
|
110
122
|
import {
|
|
111
123
|
daemonIsLive,
|
|
112
124
|
daemonRequest
|
|
113
|
-
} from "./chunk-
|
|
125
|
+
} from "./chunk-KFXF2V5G.js";
|
|
114
126
|
import {
|
|
115
127
|
REFRESH_TOKEN_PATH,
|
|
116
128
|
TOKEN_FILE_PATH,
|
|
117
129
|
WorkosConnectProtocolError,
|
|
130
|
+
atomicWriteFile,
|
|
118
131
|
discoverWorkosConnect,
|
|
119
132
|
pollWorkosDeviceAuthorization,
|
|
120
133
|
primConfigDirectory,
|
|
134
|
+
processIsAlive,
|
|
121
135
|
requestWorkosDeviceAuthorization,
|
|
122
|
-
resolveAuthCredential
|
|
123
|
-
|
|
136
|
+
resolveAuthCredential,
|
|
137
|
+
syncDirectory,
|
|
138
|
+
withFileLock
|
|
139
|
+
} from "./chunk-67VNLLBN.js";
|
|
124
140
|
|
|
125
141
|
// src/index.ts
|
|
126
|
-
import { readFileSync as
|
|
127
|
-
import { dirname as
|
|
142
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
143
|
+
import { dirname as dirname6, resolve as resolve4 } from "path";
|
|
128
144
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
129
145
|
import { Command } from "commander";
|
|
130
146
|
import updateNotifier from "update-notifier";
|
|
@@ -1007,8 +1023,8 @@ ${verificationUrl}`);
|
|
|
1007
1023
|
const state = base64url(randomBytes2(16));
|
|
1008
1024
|
const redirectUri = `http://${LOCALHOST}:${CALLBACK_PORT}/callback`;
|
|
1009
1025
|
let settle;
|
|
1010
|
-
const outcome = new Promise((
|
|
1011
|
-
settle =
|
|
1026
|
+
const outcome = new Promise((resolve5) => {
|
|
1027
|
+
settle = resolve5;
|
|
1012
1028
|
});
|
|
1013
1029
|
const server = createServer((req, res) => {
|
|
1014
1030
|
const url = new URL(req.url ?? "/", `http://${LOCALHOST}`);
|
|
@@ -1022,12 +1038,12 @@ ${verificationUrl}`);
|
|
|
1022
1038
|
res.end(page.html, () => settle(page.result));
|
|
1023
1039
|
});
|
|
1024
1040
|
try {
|
|
1025
|
-
await new Promise((
|
|
1041
|
+
await new Promise((resolve5, reject) => {
|
|
1026
1042
|
const onError = (err) => reject(err);
|
|
1027
1043
|
server.once("error", onError);
|
|
1028
1044
|
server.listen(CALLBACK_PORT, LOCALHOST, () => {
|
|
1029
1045
|
server.removeListener("error", onError);
|
|
1030
|
-
|
|
1046
|
+
resolve5();
|
|
1031
1047
|
});
|
|
1032
1048
|
});
|
|
1033
1049
|
} catch (err) {
|
|
@@ -1908,1041 +1924,84 @@ async function applyActivation(active, globals = {}) {
|
|
|
1908
1924
|
if (active) {
|
|
1909
1925
|
refreshOwnedGlobalHooks();
|
|
1910
1926
|
postCommitHook = ensureEffectivePostCommitHook(root).path;
|
|
1911
|
-
try {
|
|
1912
|
-
postRewriteHook = ensureEffectivePostRewriteHook(root).path;
|
|
1913
|
-
} catch (error) {
|
|
1914
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
1915
|
-
process.stderr.write(`[prim] post-rewrite hook coverage is degraded: ${detail}
|
|
1916
|
-
`);
|
|
1917
|
-
}
|
|
1918
|
-
phase = "GitHub repo connection";
|
|
1919
|
-
binding = await bindRepository(root);
|
|
1920
|
-
}
|
|
1921
|
-
if (active && binding?.status === "unbound") {
|
|
1922
|
-
const connected = await maybeConnectRepository(root, globals);
|
|
1923
|
-
if (!connected) {
|
|
1924
|
-
process.stderr.write(`[prim] ${GITHUB_CONNECTION_REQUIRED}
|
|
1925
|
-
`);
|
|
1926
|
-
printJson({
|
|
1927
|
-
active: false,
|
|
1928
|
-
repo: root,
|
|
1929
|
-
bindingStatus: binding.status,
|
|
1930
|
-
repositoryFullName: binding.repositoryFullName,
|
|
1931
|
-
...postCommitHook ? { postCommitHook } : {},
|
|
1932
|
-
...postRewriteHook ? { postRewriteHook } : {}
|
|
1933
|
-
});
|
|
1934
|
-
process.exitCode = 1;
|
|
1935
|
-
return;
|
|
1936
|
-
}
|
|
1937
|
-
binding = connected;
|
|
1938
|
-
}
|
|
1939
|
-
if (active) {
|
|
1940
|
-
try {
|
|
1941
|
-
await fetchAndCacheCollectScope(root);
|
|
1942
|
-
} catch {
|
|
1943
|
-
}
|
|
1944
|
-
}
|
|
1945
|
-
phase = "local activation";
|
|
1946
|
-
setRepoActive(root, active);
|
|
1947
|
-
await daemonRequest("statusline_invalidate", {}, { timeoutMs: 250 });
|
|
1948
|
-
process.stderr.write(`[prim] prim ${active ? "enabled" : "disabled"} in ${root}
|
|
1949
|
-
`);
|
|
1950
|
-
printJson({
|
|
1951
|
-
active,
|
|
1952
|
-
repo: root,
|
|
1953
|
-
...binding ? {
|
|
1954
|
-
bindingStatus: binding.status,
|
|
1955
|
-
repositoryFullName: binding.repositoryFullName,
|
|
1956
|
-
...binding.status === "connected" ? { repoSyncId: binding.repoSyncId } : {}
|
|
1957
|
-
} : {},
|
|
1958
|
-
...postCommitHook ? { postCommitHook } : {},
|
|
1959
|
-
...postRewriteHook ? { postRewriteHook } : {}
|
|
1960
|
-
});
|
|
1961
|
-
} catch (err) {
|
|
1962
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
1963
|
-
process.stderr.write(
|
|
1964
|
-
active ? `[prim] failed to enable prim during ${phase}: ${detail}
|
|
1965
|
-
` : `[prim] failed to disable prim: ${detail}
|
|
1966
|
-
`
|
|
1967
|
-
);
|
|
1968
|
-
process.exit(1);
|
|
1969
|
-
}
|
|
1970
|
-
}
|
|
1971
|
-
function registerActivationCommands(program2) {
|
|
1972
|
-
program2.command("enable").description("Activate prim's hooks in this repo (git config prim.active=true)").action((_opts, command) => applyActivation(true, command.optsWithGlobals()));
|
|
1973
|
-
program2.command("disable").description("Mute prim's hooks in this repo (git config prim.active=false)").action(
|
|
1974
|
-
(_opts, command) => applyActivation(false, command.optsWithGlobals())
|
|
1975
|
-
);
|
|
1976
|
-
}
|
|
1977
|
-
|
|
1978
|
-
// src/commands/claude-install.ts
|
|
1979
|
-
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
1980
|
-
import { homedir as homedir2 } from "os";
|
|
1981
|
-
import { join as join3 } from "path";
|
|
1982
|
-
|
|
1983
|
-
// src/lib/hook-runtime.ts
|
|
1984
|
-
import { createHash as createHash2, randomBytes as randomBytes3 } from "crypto";
|
|
1985
|
-
import {
|
|
1986
|
-
constants,
|
|
1987
|
-
accessSync,
|
|
1988
|
-
chmodSync,
|
|
1989
|
-
copyFileSync,
|
|
1990
|
-
existsSync as existsSync3,
|
|
1991
|
-
lstatSync as lstatSync2,
|
|
1992
|
-
mkdirSync as mkdirSync2,
|
|
1993
|
-
mkdtempSync,
|
|
1994
|
-
readFileSync as readFileSync3,
|
|
1995
|
-
readdirSync as readdirSync2,
|
|
1996
|
-
realpathSync,
|
|
1997
|
-
renameSync,
|
|
1998
|
-
rmSync,
|
|
1999
|
-
statSync,
|
|
2000
|
-
utimesSync,
|
|
2001
|
-
writeFileSync as writeFileSync2
|
|
2002
|
-
} from "fs";
|
|
2003
|
-
import { dirname as dirname2, isAbsolute, join as join2, resolve as resolve2 } from "path";
|
|
2004
|
-
var HOOK_RUNTIME_SCHEMA_VERSION = 1;
|
|
2005
|
-
var DIRECTORY_MODE = 448;
|
|
2006
|
-
var DATA_FILE_MODE = 384;
|
|
2007
|
-
var LAUNCHER_MODE = 448;
|
|
2008
|
-
var RELEASE_PREFIX = "release-";
|
|
2009
|
-
var RELEASE_NAME_RE = /^release-[0-9a-f]{64}$/u;
|
|
2010
|
-
var MAX_MANIFEST_BYTES = 64 * 1024;
|
|
2011
|
-
var MAX_RELEASES = 8;
|
|
2012
|
-
var MAX_RELEASE_DELETIONS_PER_STAGE = 8;
|
|
2013
|
-
var RETAIN_PREDECESSORS = 2;
|
|
2014
|
-
var RELEASE_GRACE_MS = 10 * 60 * 1e3;
|
|
2015
|
-
var HOOK_RUNTIME_ENTRIES = {
|
|
2016
|
-
// SessionStart self-healing resolves this exact CLI entry to run
|
|
2017
|
-
// `daemon ensure`; it must travel with the immutable hook release.
|
|
2018
|
-
prim: "dist/index.js",
|
|
2019
|
-
"prim-hook": "dist/hooks/prim-hook.js",
|
|
2020
|
-
"prim-pre-commit": "dist/hooks/pre-commit.js",
|
|
2021
|
-
"prim-post-commit": "dist/hooks/post-commit.js",
|
|
2022
|
-
"prim-post-rewrite": "dist/hooks/post-rewrite.js",
|
|
2023
|
-
"prim-pre-tool-use": "dist/hooks/pre-tool-use.js",
|
|
2024
|
-
"prim-post-tool-use": "dist/hooks/post-tool-use.js",
|
|
2025
|
-
"prim-session-start": "dist/hooks/session-start.js",
|
|
2026
|
-
"prim-session-end": "dist/hooks/session-end.js",
|
|
2027
|
-
"prim-daemon-server": "dist/daemon/server.js",
|
|
2028
|
-
"prim-statusline": "dist/statusline-main.js"
|
|
2029
|
-
};
|
|
2030
|
-
var SOURCE_ENTRIES = Object.fromEntries(
|
|
2031
|
-
Object.entries(HOOK_RUNTIME_ENTRIES).map(([bin, target]) => [
|
|
2032
|
-
bin,
|
|
2033
|
-
target.replace(/^dist\//u, "")
|
|
2034
|
-
])
|
|
2035
|
-
);
|
|
2036
|
-
function hookRuntimePaths(options = {}) {
|
|
2037
|
-
const env = options.env ?? process.env;
|
|
2038
|
-
const configDir = primConfigDirectory({
|
|
2039
|
-
...options,
|
|
2040
|
-
env,
|
|
2041
|
-
homeDir: options.homeDir ?? env.HOME ?? ""
|
|
2042
|
-
});
|
|
2043
|
-
const runtimeDir = join2(configDir, "hook-runtime");
|
|
2044
|
-
return {
|
|
2045
|
-
configDir,
|
|
2046
|
-
launcher: join2(configDir, STABLE_HOOK_LAUNCHER_NAME),
|
|
2047
|
-
runtimeDir,
|
|
2048
|
-
releasesDir: join2(runtimeDir, "releases"),
|
|
2049
|
-
current: join2(runtimeDir, "current"),
|
|
2050
|
-
selectionLock: join2(runtimeDir, "selection.lock")
|
|
2051
|
-
};
|
|
2052
|
-
}
|
|
2053
|
-
function sha256(path) {
|
|
2054
|
-
return createHash2("sha256").update(readFileSync3(path)).digest("hex");
|
|
2055
|
-
}
|
|
2056
|
-
function exactManifest(manifest) {
|
|
2057
|
-
return `${JSON.stringify(manifest, null, 2)}
|
|
2058
|
-
`;
|
|
2059
|
-
}
|
|
2060
|
-
function exactKeys(value, expected) {
|
|
2061
|
-
const actual = Object.keys(value).sort();
|
|
2062
|
-
const wanted = [...expected].sort();
|
|
2063
|
-
return actual.length === wanted.length && actual.every((key, index) => key === wanted[index]);
|
|
2064
|
-
}
|
|
2065
|
-
function releaseId(manifest) {
|
|
2066
|
-
return createHash2("sha256").update(exactManifest(manifest)).digest("hex");
|
|
2067
|
-
}
|
|
2068
|
-
function runtimePackageJson(manifest) {
|
|
2069
|
-
return `${JSON.stringify(
|
|
2070
|
-
{
|
|
2071
|
-
name: "@primitive.ai/prim",
|
|
2072
|
-
version: manifest.version,
|
|
2073
|
-
type: "module",
|
|
2074
|
-
bin: HOOK_RUNTIME_ENTRIES
|
|
2075
|
-
},
|
|
2076
|
-
null,
|
|
2077
|
-
2
|
|
2078
|
-
)}
|
|
2079
|
-
`;
|
|
2080
|
-
}
|
|
2081
|
-
function validateRelease(releaseDir, manifest) {
|
|
2082
|
-
try {
|
|
2083
|
-
if (!directReleaseLayout(releaseDir)) return false;
|
|
2084
|
-
if (readFileSync3(join2(releaseDir, "manifest.json"), "utf8") !== exactManifest(manifest)) {
|
|
2085
|
-
return false;
|
|
2086
|
-
}
|
|
2087
|
-
if (readFileSync3(join2(releaseDir, "package.json"), "utf8") !== runtimePackageJson(manifest)) {
|
|
2088
|
-
return false;
|
|
2089
|
-
}
|
|
2090
|
-
if (readFileSync3(join2(releaseDir, "node"), "utf8") !== `${manifest.nodePath}
|
|
2091
|
-
`) {
|
|
2092
|
-
return false;
|
|
2093
|
-
}
|
|
2094
|
-
for (const [bin, relativePath] of Object.entries(HOOK_RUNTIME_ENTRIES)) {
|
|
2095
|
-
const target = join2(releaseDir, relativePath);
|
|
2096
|
-
if (sha256(target) !== manifest.files[bin]) return false;
|
|
2097
|
-
}
|
|
2098
|
-
return true;
|
|
2099
|
-
} catch {
|
|
2100
|
-
return false;
|
|
2101
|
-
}
|
|
2102
|
-
}
|
|
2103
|
-
function directReleaseLayout(releaseDir) {
|
|
2104
|
-
try {
|
|
2105
|
-
const directories = /* @__PURE__ */ new Set([releaseDir]);
|
|
2106
|
-
for (const relativePath of Object.values(HOOK_RUNTIME_ENTRIES)) {
|
|
2107
|
-
for (let directory = dirname2(relativePath); directory !== "."; directory = dirname2(directory)) {
|
|
2108
|
-
directories.add(join2(releaseDir, directory));
|
|
2109
|
-
}
|
|
2110
|
-
}
|
|
2111
|
-
if (![...directories].every((directory) => lstatSync2(directory).isDirectory())) return false;
|
|
2112
|
-
const files = ["manifest.json", "package.json", "node", ...Object.values(HOOK_RUNTIME_ENTRIES)];
|
|
2113
|
-
return files.every((relativePath) => lstatSync2(join2(releaseDir, relativePath)).isFile());
|
|
2114
|
-
} catch {
|
|
2115
|
-
return false;
|
|
2116
|
-
}
|
|
2117
|
-
}
|
|
2118
|
-
function readReleaseManifest(releaseDir, releaseName) {
|
|
2119
|
-
try {
|
|
2120
|
-
const manifestPath = join2(releaseDir, "manifest.json");
|
|
2121
|
-
const manifestStat = statSync(manifestPath);
|
|
2122
|
-
if (!manifestStat.isFile() || manifestStat.size > MAX_MANIFEST_BYTES) return null;
|
|
2123
|
-
const raw = readFileSync3(manifestPath, "utf8");
|
|
2124
|
-
const parsed = JSON.parse(raw);
|
|
2125
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
2126
|
-
if (!exactKeys(parsed, ["schemaVersion", "version", "nodePath", "files"])) return null;
|
|
2127
|
-
const candidate = parsed;
|
|
2128
|
-
if (candidate.schemaVersion !== HOOK_RUNTIME_SCHEMA_VERSION || typeof candidate.version !== "string" || compareSemver(candidate.version, candidate.version) !== 0 || typeof candidate.nodePath !== "string" || !isAbsolute(candidate.nodePath) || /[\r\n]/u.test(candidate.nodePath) || !candidate.files || typeof candidate.files !== "object" || Array.isArray(candidate.files) || !exactKeys(candidate.files, Object.keys(HOOK_RUNTIME_ENTRIES)) || Object.values(candidate.files).some(
|
|
2129
|
-
(digest) => typeof digest !== "string" || !/^[0-9a-f]{64}$/u.test(digest)
|
|
2130
|
-
)) {
|
|
2131
|
-
return null;
|
|
2132
|
-
}
|
|
2133
|
-
const manifest = candidate;
|
|
2134
|
-
if (raw !== exactManifest(manifest) || releaseName !== `${RELEASE_PREFIX}${releaseId(manifest)}` || !validateRelease(releaseDir, manifest)) {
|
|
2135
|
-
return null;
|
|
2136
|
-
}
|
|
2137
|
-
return manifest;
|
|
2138
|
-
} catch {
|
|
2139
|
-
return null;
|
|
2140
|
-
}
|
|
2141
|
-
}
|
|
2142
|
-
function readSelectedRelease(paths) {
|
|
2143
|
-
let raw;
|
|
2144
|
-
try {
|
|
2145
|
-
raw = readFileSync3(paths.current, "utf8");
|
|
2146
|
-
} catch (error) {
|
|
2147
|
-
if (error.code === "ENOENT") return null;
|
|
2148
|
-
throw error;
|
|
2149
|
-
}
|
|
2150
|
-
const name = raw.endsWith("\n") ? raw.slice(0, -1) : "";
|
|
2151
|
-
if (`${name}
|
|
2152
|
-
` !== raw || !RELEASE_NAME_RE.test(name)) {
|
|
2153
|
-
throw new Error("cannot stage hook runtime: selected release is malformed");
|
|
2154
|
-
}
|
|
2155
|
-
const dir = join2(paths.releasesDir, name);
|
|
2156
|
-
const manifest = readReleaseManifest(dir, name);
|
|
2157
|
-
if (!manifest) {
|
|
2158
|
-
throw new Error("cannot stage hook runtime: immutable release is corrupt");
|
|
2159
|
-
}
|
|
2160
|
-
return { name, dir, manifest };
|
|
2161
|
-
}
|
|
2162
|
-
function releaseEntries(paths) {
|
|
2163
|
-
return readdirSync2(paths.releasesDir, { withFileTypes: true }).flatMap((entry) => {
|
|
2164
|
-
if (!entry.isDirectory() || !RELEASE_NAME_RE.test(entry.name)) return [];
|
|
2165
|
-
const dir = join2(paths.releasesDir, entry.name);
|
|
2166
|
-
try {
|
|
2167
|
-
return [{ name: entry.name, dir, mtimeMs: statSync(dir).mtimeMs }];
|
|
2168
|
-
} catch {
|
|
2169
|
-
return [];
|
|
2170
|
-
}
|
|
2171
|
-
});
|
|
2172
|
-
}
|
|
2173
|
-
function pruneReleases(paths, protectedNames, reserveSlots, now) {
|
|
2174
|
-
const entries = releaseEntries(paths).sort(
|
|
2175
|
-
(left, right) => right.mtimeMs - left.mtimeMs || left.name.localeCompare(right.name)
|
|
2176
|
-
);
|
|
2177
|
-
const protectedWithPredecessors = new Set(protectedNames);
|
|
2178
|
-
for (const entry of entries) {
|
|
2179
|
-
if (protectedWithPredecessors.size >= protectedNames.size + RETAIN_PREDECESSORS) break;
|
|
2180
|
-
if (!protectedWithPredecessors.has(entry.name)) protectedWithPredecessors.add(entry.name);
|
|
2181
|
-
}
|
|
2182
|
-
const candidates = entries.filter(
|
|
2183
|
-
(entry) => !protectedWithPredecessors.has(entry.name) && now - entry.mtimeMs >= RELEASE_GRACE_MS
|
|
2184
|
-
).sort((left, right) => left.mtimeMs - right.mtimeMs || left.name.localeCompare(right.name));
|
|
2185
|
-
let remaining = entries.length;
|
|
2186
|
-
let deleted = 0;
|
|
2187
|
-
for (const entry of candidates) {
|
|
2188
|
-
if (remaining + reserveSlots <= MAX_RELEASES || deleted >= MAX_RELEASE_DELETIONS_PER_STAGE) {
|
|
2189
|
-
break;
|
|
2190
|
-
}
|
|
2191
|
-
rmSync(entry.dir, { recursive: true, force: true });
|
|
2192
|
-
remaining -= 1;
|
|
2193
|
-
deleted += 1;
|
|
2194
|
-
}
|
|
2195
|
-
if (remaining + reserveSlots > MAX_RELEASES) {
|
|
2196
|
-
throw new Error(
|
|
2197
|
-
"cannot stage hook runtime: release retention is full while recent runtimes are protected"
|
|
2198
|
-
);
|
|
2199
|
-
}
|
|
2200
|
-
}
|
|
2201
|
-
function sourceManifest(sourceDir, version, nodePath) {
|
|
2202
|
-
const files = {};
|
|
2203
|
-
for (const [bin, relativePath] of Object.entries(SOURCE_ENTRIES)) {
|
|
2204
|
-
const source = join2(sourceDir, relativePath);
|
|
2205
|
-
if (!existsSync3(source) || !statSync(source).isFile()) {
|
|
2206
|
-
throw new Error(`cannot stage hook runtime: missing self-contained ${bin} bundle`);
|
|
2207
|
-
}
|
|
2208
|
-
files[bin] = sha256(source);
|
|
2209
|
-
}
|
|
2210
|
-
return { schemaVersion: HOOK_RUNTIME_SCHEMA_VERSION, version, nodePath, files };
|
|
2211
|
-
}
|
|
2212
|
-
var STABLE_HOOK_LAUNCHER_CONTENT = `#!/bin/sh
|
|
2213
|
-
set -eu
|
|
2214
|
-
case "$0" in */*) prim_launcher_dir=\${0%/*} ;; *) exit 78 ;; esac
|
|
2215
|
-
IFS= read -r prim_release_name < "$prim_launcher_dir/hook-runtime/current" || exit 69
|
|
2216
|
-
case "$prim_release_name" in ${RELEASE_PREFIX}*) ;; *) exit 69 ;; esac
|
|
2217
|
-
prim_release_id=\${prim_release_name#${RELEASE_PREFIX}}
|
|
2218
|
-
case "$prim_release_id" in *[!0-9a-f]*|"") exit 69 ;; esac
|
|
2219
|
-
[ "\${#prim_release_id}" -eq 64 ] || exit 69
|
|
2220
|
-
prim_release="$prim_launcher_dir/hook-runtime/releases/$prim_release_name"
|
|
2221
|
-
case "\${1-}" in
|
|
2222
|
-
${Object.entries(HOOK_RUNTIME_ENTRIES).map(([bin, entry]) => ` ${bin}) prim_entry="$prim_release/${entry}" ;;`).join("\n")}
|
|
2223
|
-
*) exit 64 ;;
|
|
2224
|
-
esac
|
|
2225
|
-
shift
|
|
2226
|
-
IFS= read -r prim_node < "$prim_release/node" || exit 69
|
|
2227
|
-
case "$prim_node" in /*) ;; *) exit 69 ;; esac
|
|
2228
|
-
[ -x "$prim_node" ] && [ -f "$prim_entry" ] || exit 69
|
|
2229
|
-
exec "$prim_node" "$prim_entry" "$@"
|
|
2230
|
-
`;
|
|
2231
|
-
function privateDirectory(path) {
|
|
2232
|
-
const stat = lstatIfPresent2(path);
|
|
2233
|
-
if (!stat?.isDirectory()) return false;
|
|
2234
|
-
if (process.platform === "win32") return true;
|
|
2235
|
-
const uid = process.getuid?.();
|
|
2236
|
-
return (Number(stat.mode) & 511) === DIRECTORY_MODE && (uid === void 0 || stat.uid === uid);
|
|
2237
|
-
}
|
|
2238
|
-
function privateFile(path, mode) {
|
|
2239
|
-
const stat = lstatIfPresent2(path);
|
|
2240
|
-
if (!stat?.isFile()) return false;
|
|
2241
|
-
if (process.platform === "win32") return true;
|
|
2242
|
-
const uid = process.getuid?.();
|
|
2243
|
-
return (Number(stat.mode) & 511) === mode && (uid === void 0 || stat.uid === uid);
|
|
2244
|
-
}
|
|
2245
|
-
function hasSafeRuntimeLayout(paths, selected) {
|
|
2246
|
-
const releaseDirectories = /* @__PURE__ */ new Set([selected.dir]);
|
|
2247
|
-
for (const relativePath of Object.values(HOOK_RUNTIME_ENTRIES)) {
|
|
2248
|
-
for (let directory = dirname2(relativePath); directory !== "."; directory = dirname2(directory)) {
|
|
2249
|
-
releaseDirectories.add(join2(selected.dir, directory));
|
|
2250
|
-
}
|
|
2251
|
-
}
|
|
2252
|
-
if (!privateDirectory(paths.configDir) || !privateDirectory(paths.runtimeDir) || !privateDirectory(paths.releasesDir) || ![...releaseDirectories].every(privateDirectory) || !privateFile(paths.launcher, LAUNCHER_MODE) || !privateFile(paths.current, DATA_FILE_MODE) || !privateFile(join2(selected.dir, "manifest.json"), DATA_FILE_MODE) || !privateFile(join2(selected.dir, "package.json"), DATA_FILE_MODE) || !privateFile(join2(selected.dir, "node"), DATA_FILE_MODE)) {
|
|
2253
|
-
return false;
|
|
2254
|
-
}
|
|
2255
|
-
return Object.values(HOOK_RUNTIME_ENTRIES).every(
|
|
2256
|
-
(relativePath) => privateFile(join2(selected.dir, relativePath), DATA_FILE_MODE)
|
|
2257
|
-
);
|
|
2258
|
-
}
|
|
2259
|
-
function recordedNodeMatchesCurrentProcess(nodePath) {
|
|
2260
|
-
try {
|
|
2261
|
-
if (!statSync(nodePath).isFile()) return false;
|
|
2262
|
-
accessSync(nodePath, constants.X_OK);
|
|
2263
|
-
return realpathSync(nodePath) === realpathSync(process.execPath);
|
|
2264
|
-
} catch {
|
|
2265
|
-
return false;
|
|
2266
|
-
}
|
|
2267
|
-
}
|
|
2268
|
-
function inspectHookRuntime(options = {}) {
|
|
2269
|
-
let paths;
|
|
2270
|
-
try {
|
|
2271
|
-
paths = hookRuntimePaths(options);
|
|
2272
|
-
} catch {
|
|
2273
|
-
return { state: "invalid" };
|
|
2274
|
-
}
|
|
2275
|
-
const launcher = lstatIfPresent2(paths.launcher);
|
|
2276
|
-
const selector = lstatIfPresent2(paths.current);
|
|
2277
|
-
if (!(launcher || selector)) return { state: "missing" };
|
|
2278
|
-
if (!(launcher && selector)) return { state: "invalid" };
|
|
2279
|
-
try {
|
|
2280
|
-
if (readFileSync3(paths.launcher, "utf8") !== STABLE_HOOK_LAUNCHER_CONTENT) {
|
|
2281
|
-
return { state: "invalid" };
|
|
2282
|
-
}
|
|
2283
|
-
const selected = readSelectedRelease(paths);
|
|
2284
|
-
if (!selected) return { state: "invalid" };
|
|
2285
|
-
assertOwnedHookRuntime(paths);
|
|
2286
|
-
if (!hasSafeRuntimeLayout(paths, selected)) return { state: "invalid" };
|
|
2287
|
-
return recordedNodeMatchesCurrentProcess(selected.manifest.nodePath) ? { state: "ready", version: selected.manifest.version } : { state: "invalid" };
|
|
2288
|
-
} catch {
|
|
2289
|
-
return { state: "invalid" };
|
|
2290
|
-
}
|
|
2291
|
-
}
|
|
2292
|
-
function writeRelease(sourceDir, releaseDir, manifest, releasesDir) {
|
|
2293
|
-
const stagingDir = mkdtempSync(join2(releasesDir, ".stage-"));
|
|
2294
|
-
chmodSync(stagingDir, DIRECTORY_MODE);
|
|
2295
|
-
try {
|
|
2296
|
-
const directories = /* @__PURE__ */ new Set([stagingDir]);
|
|
2297
|
-
const syncStagedFile = (path) => {
|
|
2298
|
-
syncFile(path);
|
|
2299
|
-
for (let directory = dirname2(path); ; directory = dirname2(directory)) {
|
|
2300
|
-
directories.add(directory);
|
|
2301
|
-
if (directory === stagingDir) break;
|
|
2302
|
-
}
|
|
2303
|
-
};
|
|
2304
|
-
for (const [bin, targetRelative] of Object.entries(HOOK_RUNTIME_ENTRIES)) {
|
|
2305
|
-
const source = join2(sourceDir, SOURCE_ENTRIES[bin]);
|
|
2306
|
-
const target = join2(stagingDir, targetRelative);
|
|
2307
|
-
mkdirSync2(dirname2(target), { recursive: true, mode: DIRECTORY_MODE });
|
|
2308
|
-
chmodSync(dirname2(target), DIRECTORY_MODE);
|
|
2309
|
-
copyFileSync(source, target);
|
|
2310
|
-
chmodSync(target, DATA_FILE_MODE);
|
|
2311
|
-
if (sha256(target) !== manifest.files[bin]) {
|
|
2312
|
-
throw new Error(`cannot stage hook runtime: ${bin} changed while it was copied`);
|
|
2313
|
-
}
|
|
2314
|
-
syncStagedFile(target);
|
|
2315
|
-
}
|
|
2316
|
-
const nodePath = join2(stagingDir, "node");
|
|
2317
|
-
writeFileSync2(nodePath, `${manifest.nodePath}
|
|
2318
|
-
`, {
|
|
2319
|
-
encoding: "utf8",
|
|
2320
|
-
flag: "wx",
|
|
2321
|
-
mode: DATA_FILE_MODE
|
|
2322
|
-
});
|
|
2323
|
-
syncStagedFile(nodePath);
|
|
2324
|
-
const packageJsonPath = join2(stagingDir, "package.json");
|
|
2325
|
-
writeFileSync2(packageJsonPath, runtimePackageJson(manifest), {
|
|
2326
|
-
encoding: "utf8",
|
|
2327
|
-
flag: "wx",
|
|
2328
|
-
mode: DATA_FILE_MODE
|
|
2329
|
-
});
|
|
2330
|
-
syncStagedFile(packageJsonPath);
|
|
2331
|
-
const manifestPath = join2(stagingDir, "manifest.json");
|
|
2332
|
-
writeFileSync2(manifestPath, exactManifest(manifest), {
|
|
2333
|
-
encoding: "utf8",
|
|
2334
|
-
flag: "wx",
|
|
2335
|
-
mode: DATA_FILE_MODE
|
|
2336
|
-
});
|
|
2337
|
-
syncStagedFile(manifestPath);
|
|
2338
|
-
for (const directory of [...directories].sort((left, right) => right.length - left.length)) {
|
|
2339
|
-
syncDirectory(directory);
|
|
2340
|
-
}
|
|
2341
|
-
try {
|
|
2342
|
-
renameSync(stagingDir, releaseDir);
|
|
2343
|
-
} catch (error) {
|
|
2344
|
-
const code = error.code;
|
|
2345
|
-
if (code !== "EEXIST" && code !== "ENOTEMPTY" || !validateRelease(releaseDir, manifest)) {
|
|
2346
|
-
throw error;
|
|
2347
|
-
}
|
|
2348
|
-
}
|
|
2349
|
-
} finally {
|
|
2350
|
-
rmSync(stagingDir, { recursive: true, force: true });
|
|
2351
|
-
}
|
|
2352
|
-
}
|
|
2353
|
-
function stageHookRuntime(options = {}) {
|
|
2354
|
-
const root = packageRoot();
|
|
2355
|
-
const sourceDir = options.sourceDir ?? (root ? join2(root, "dist", "hook-runtime") : null);
|
|
2356
|
-
const version = options.version ?? packageVersion();
|
|
2357
|
-
const nodePath = resolve2(options.nodePath ?? process.execPath);
|
|
2358
|
-
if (!sourceDir || !version || compareSemver(version, version) !== 0 || !isAbsolute(nodePath) || /[\r\n]/u.test(nodePath)) {
|
|
2359
|
-
throw new Error("cannot stage hook runtime: package identity is unavailable");
|
|
2360
|
-
}
|
|
2361
|
-
const manifest = sourceManifest(sourceDir, version, nodePath);
|
|
2362
|
-
const paths = hookRuntimePaths(options);
|
|
2363
|
-
const releaseName = `${RELEASE_PREFIX}${releaseId(manifest)}`;
|
|
2364
|
-
const releaseDir = join2(paths.releasesDir, releaseName);
|
|
2365
|
-
mkdirSync2(paths.releasesDir, { recursive: true, mode: DIRECTORY_MODE });
|
|
2366
|
-
chmodSync(paths.configDir, DIRECTORY_MODE);
|
|
2367
|
-
chmodSync(paths.runtimeDir, DIRECTORY_MODE);
|
|
2368
|
-
chmodSync(paths.releasesDir, DIRECTORY_MODE);
|
|
2369
|
-
return withFileLockSync(paths.selectionLock, () => {
|
|
2370
|
-
const now = Date.now();
|
|
2371
|
-
const selected = readSelectedRelease(paths);
|
|
2372
|
-
if (selected) utimesSync(selected.dir, now / 1e3, now / 1e3);
|
|
2373
|
-
const precedence = selected ? compareSemver(selected.manifest.version, manifest.version) : -1;
|
|
2374
|
-
if (precedence === void 0) {
|
|
2375
|
-
throw new Error("cannot stage hook runtime: selected release version is malformed");
|
|
2376
|
-
}
|
|
2377
|
-
const selectRequested = !selected || precedence < 0;
|
|
2378
|
-
const requestedExists = existsSync3(releaseDir);
|
|
2379
|
-
const protectedBefore = new Set(selected ? [selected.name] : []);
|
|
2380
|
-
if (selectRequested && requestedExists) protectedBefore.add(releaseName);
|
|
2381
|
-
pruneReleases(paths, protectedBefore, selectRequested && !requestedExists ? 1 : 0, now);
|
|
2382
|
-
if (selectRequested && !validateRelease(releaseDir, manifest)) {
|
|
2383
|
-
if (requestedExists) {
|
|
2384
|
-
throw new Error("cannot stage hook runtime: immutable release is corrupt");
|
|
2385
|
-
}
|
|
2386
|
-
writeRelease(sourceDir, releaseDir, manifest, paths.releasesDir);
|
|
2387
|
-
}
|
|
2388
|
-
const finalRelease = selectRequested ? { name: releaseName, dir: releaseDir, manifest } : selected;
|
|
2389
|
-
const launcherCurrent = (() => {
|
|
2390
|
-
try {
|
|
2391
|
-
return readFileSync3(paths.launcher, "utf8") === STABLE_HOOK_LAUNCHER_CONTENT;
|
|
2392
|
-
} catch {
|
|
2393
|
-
return false;
|
|
2394
|
-
}
|
|
2395
|
-
})();
|
|
2396
|
-
const selectorCurrent = selected?.name === finalRelease.name;
|
|
2397
|
-
if (!launcherCurrent) {
|
|
2398
|
-
atomicWriteFile(paths.launcher, STABLE_HOOK_LAUNCHER_CONTENT, {
|
|
2399
|
-
ensureParent: true,
|
|
2400
|
-
mode: LAUNCHER_MODE
|
|
2401
|
-
});
|
|
2402
|
-
}
|
|
2403
|
-
if (!selectorCurrent) {
|
|
2404
|
-
syncDirectory(paths.releasesDir);
|
|
2405
|
-
atomicWriteFile(paths.current, `${finalRelease.name}
|
|
2406
|
-
`, {
|
|
2407
|
-
ensureParent: true,
|
|
2408
|
-
mode: DATA_FILE_MODE
|
|
2409
|
-
});
|
|
2410
|
-
}
|
|
2411
|
-
utimesSync(finalRelease.dir, now / 1e3, now / 1e3);
|
|
2412
|
-
pruneReleases(
|
|
2413
|
-
paths,
|
|
2414
|
-
/* @__PURE__ */ new Set([finalRelease.name, ...selected ? [selected.name] : []]),
|
|
2415
|
-
0,
|
|
2416
|
-
now
|
|
2417
|
-
);
|
|
2418
|
-
return {
|
|
2419
|
-
changed: !launcherCurrent || !selectorCurrent,
|
|
2420
|
-
releaseDir: finalRelease.dir,
|
|
2421
|
-
manifest: finalRelease.manifest,
|
|
2422
|
-
paths
|
|
2423
|
-
};
|
|
2424
|
-
});
|
|
2425
|
-
}
|
|
2426
|
-
function lstatIfPresent2(path) {
|
|
2427
|
-
try {
|
|
2428
|
-
return lstatSync2(path);
|
|
2429
|
-
} catch (error) {
|
|
2430
|
-
const code = error.code;
|
|
2431
|
-
if (code === "ENOENT" || code === "ENOTDIR") return void 0;
|
|
2432
|
-
throw error;
|
|
2433
|
-
}
|
|
2434
|
-
}
|
|
2435
|
-
function assertOwnedHookRuntime(paths) {
|
|
2436
|
-
const launcher = lstatIfPresent2(paths.launcher);
|
|
2437
|
-
if (launcher) {
|
|
2438
|
-
if (!launcher.isFile() || readFileSync3(paths.launcher, "utf8") !== STABLE_HOOK_LAUNCHER_CONTENT) {
|
|
2439
|
-
throw new Error(`refusing to remove unrecognized hook launcher at ${paths.launcher}`);
|
|
2440
|
-
}
|
|
2441
|
-
}
|
|
2442
|
-
const runtime = lstatIfPresent2(paths.runtimeDir);
|
|
2443
|
-
if (!runtime) return;
|
|
2444
|
-
if (!runtime.isDirectory()) {
|
|
2445
|
-
throw new Error(`refusing to remove non-directory hook runtime at ${paths.runtimeDir}`);
|
|
2446
|
-
}
|
|
2447
|
-
const allowed = /* @__PURE__ */ new Set(["current", "releases", "selection.lock"]);
|
|
2448
|
-
const unexpected = readdirSync2(paths.runtimeDir).filter((entry) => !allowed.has(entry));
|
|
2449
|
-
if (unexpected.length > 0) {
|
|
2450
|
-
throw new Error(
|
|
2451
|
-
`refusing to remove hook runtime with unrecognized entries: ${unexpected.sort().join(", ")}`
|
|
2452
|
-
);
|
|
2453
|
-
}
|
|
2454
|
-
const current = lstatIfPresent2(paths.current);
|
|
2455
|
-
const releases = lstatIfPresent2(paths.releasesDir);
|
|
2456
|
-
if (current) {
|
|
2457
|
-
if (!current.isFile() || !releases || !readSelectedRelease(paths)) {
|
|
2458
|
-
throw new Error(`refusing to remove malformed hook runtime selection at ${paths.current}`);
|
|
2459
|
-
}
|
|
2460
|
-
}
|
|
2461
|
-
if (!releases) return;
|
|
2462
|
-
if (!releases.isDirectory()) {
|
|
2463
|
-
throw new Error(`refusing to remove non-directory hook releases at ${paths.releasesDir}`);
|
|
2464
|
-
}
|
|
2465
|
-
for (const entry of readdirSync2(paths.releasesDir, { withFileTypes: true })) {
|
|
2466
|
-
const releaseDir = join2(paths.releasesDir, entry.name);
|
|
2467
|
-
if (!entry.isDirectory() || !RELEASE_NAME_RE.test(entry.name) || !readReleaseManifest(releaseDir, entry.name)) {
|
|
2468
|
-
throw new Error(`refusing to remove unrecognized hook release ${releaseDir}`);
|
|
2469
|
-
}
|
|
2470
|
-
}
|
|
2471
|
-
}
|
|
2472
|
-
function removeHookRuntime(options = {}) {
|
|
2473
|
-
const paths = hookRuntimePaths(options);
|
|
2474
|
-
const hadLauncher = lstatIfPresent2(paths.launcher) !== void 0;
|
|
2475
|
-
const hadRuntime = lstatIfPresent2(paths.runtimeDir) !== void 0;
|
|
2476
|
-
if (!hadLauncher && !hadRuntime) return { changed: false, paths };
|
|
2477
|
-
assertOwnedHookRuntime(paths);
|
|
2478
|
-
if (!hadRuntime) {
|
|
2479
|
-
rmSync(paths.launcher, { force: true });
|
|
2480
|
-
return { changed: hadLauncher, paths };
|
|
2481
|
-
}
|
|
2482
|
-
withFileLockSync(paths.selectionLock, () => {
|
|
2483
|
-
assertOwnedHookRuntime(paths);
|
|
2484
|
-
if (lstatIfPresent2(paths.launcher)) rmSync(paths.launcher, { force: true });
|
|
2485
|
-
const quarantined = `${paths.runtimeDir}.uninstall-${String(process.pid)}-${randomBytes3(8).toString("hex")}`;
|
|
2486
|
-
renameSync(paths.runtimeDir, quarantined);
|
|
2487
|
-
rmSync(quarantined, { recursive: true, force: true });
|
|
2488
|
-
});
|
|
2489
|
-
return { changed: true, paths };
|
|
2490
|
-
}
|
|
2491
|
-
|
|
2492
|
-
// src/commands/claude-install.ts
|
|
2493
|
-
var CAPTURE_BIN = "prim-hook";
|
|
2494
|
-
var GATE_BIN = "prim-pre-tool-use";
|
|
2495
|
-
var POST_TOOL_USE_BIN = "prim-post-tool-use";
|
|
2496
|
-
var SESSION_START_BIN = "prim-session-start";
|
|
2497
|
-
var SESSION_END_BIN = "prim-session-end";
|
|
2498
|
-
var STATUSLINE_BIN = "prim-statusline";
|
|
2499
|
-
var STATUSLINE_ARGS = "";
|
|
2500
|
-
var STATUSLINE_COMMAND = stableHookCommand(STATUSLINE_BIN, STATUSLINE_ARGS);
|
|
2501
|
-
var STATUSLINE_REFRESH_SECONDS = 5;
|
|
2502
|
-
var PRIM_BINS = [
|
|
2503
|
-
CAPTURE_BIN,
|
|
2504
|
-
GATE_BIN,
|
|
2505
|
-
POST_TOOL_USE_BIN,
|
|
2506
|
-
SESSION_START_BIN,
|
|
2507
|
-
SESSION_END_BIN
|
|
2508
|
-
];
|
|
2509
|
-
var JSON_INDENT = 2;
|
|
2510
|
-
var USER_SCOPE_PATH = join3(homedir2(), ".claude", "settings.json");
|
|
2511
|
-
function projectRoot() {
|
|
2512
|
-
return gitToplevel() ?? process.cwd();
|
|
2513
|
-
}
|
|
2514
|
-
var projectScopePath = () => join3(projectRoot(), ".claude", "settings.json");
|
|
2515
|
-
var CAPTURE_EVENTS = [
|
|
2516
|
-
"SessionStart",
|
|
2517
|
-
"UserPromptSubmit",
|
|
2518
|
-
"PreToolUse",
|
|
2519
|
-
"PostToolUse",
|
|
2520
|
-
"PostToolUseFailure",
|
|
2521
|
-
"Stop",
|
|
2522
|
-
"SessionEnd",
|
|
2523
|
-
"SubagentStop"
|
|
2524
|
-
];
|
|
2525
|
-
function makeRegistration(event, matcher, bin, args = "") {
|
|
2526
|
-
return {
|
|
2527
|
-
event,
|
|
2528
|
-
matcher,
|
|
2529
|
-
bin,
|
|
2530
|
-
command: stableHookCommand(bin, args)
|
|
2531
|
-
};
|
|
2532
|
-
}
|
|
2533
|
-
function makeDetachedRegistration(event, matcher, bin, args = "") {
|
|
2534
|
-
return { event, matcher, bin, command: detachedHookShimCommand(bin, args) };
|
|
2535
|
-
}
|
|
2536
|
-
var REGISTRATIONS = [
|
|
2537
|
-
...CAPTURE_EVENTS.map(
|
|
2538
|
-
(event) => event === "SessionEnd" ? makeDetachedRegistration(event, "*", CAPTURE_BIN) : makeRegistration(event, "*", CAPTURE_BIN)
|
|
2539
|
-
),
|
|
2540
|
-
makeRegistration("PreToolUse", "Edit|Write|MultiEdit|NotebookEdit|Bash", GATE_BIN),
|
|
2541
|
-
makeRegistration("PostToolUse", "Edit|Write|MultiEdit|NotebookEdit|Bash", POST_TOOL_USE_BIN),
|
|
2542
|
-
makeRegistration(
|
|
2543
|
-
"PostToolUseFailure",
|
|
2544
|
-
"Edit|Write|MultiEdit|NotebookEdit|Bash",
|
|
2545
|
-
POST_TOOL_USE_BIN
|
|
2546
|
-
),
|
|
2547
|
-
makeRegistration("SessionStart", "*", SESSION_START_BIN),
|
|
2548
|
-
makeDetachedRegistration("SessionEnd", "*", SESSION_END_BIN)
|
|
2549
|
-
];
|
|
2550
|
-
var PRIM_PERMISSION_RULE = "Bash(npx --yes @primitive.ai/prim*)";
|
|
2551
|
-
var LEGACY_PRIM_PERMISSION_RULES = [
|
|
2552
|
-
"Bash(npx --yes @primitive.ai/prim@latest:*)",
|
|
2553
|
-
"Bash(npx --yes @primitive.ai/prim:*)"
|
|
2554
|
-
];
|
|
2555
|
-
var ALL_PRIM_PERMISSION_RULES = /* @__PURE__ */ new Set([
|
|
2556
|
-
PRIM_PERMISSION_RULE,
|
|
2557
|
-
...LEGACY_PRIM_PERMISSION_RULES
|
|
2558
|
-
]);
|
|
2559
|
-
var PRIM_AUTOMODE_TRUST = "Primitive CLI (prim): the @primitive.ai/prim npm package \u2014 run via `npx --yes @primitive.ai/prim \u2026` \u2014 and its API at api.getprimitive.ai are trusted developer tooling this user installed for passive decision capture, conflict checks, and team presence. Running prim and its routine API/daemon calls is expected, not untrusted code execution or data exfiltration.";
|
|
2560
|
-
var AUTOMODE_DEFAULTS = "$defaults";
|
|
2561
|
-
function settingsPathFor(scope) {
|
|
2562
|
-
return scope === "user" ? USER_SCOPE_PATH : projectScopePath();
|
|
2563
|
-
}
|
|
2564
|
-
function readSettings(path) {
|
|
2565
|
-
if (!existsSync4(path)) {
|
|
2566
|
-
return {};
|
|
2567
|
-
}
|
|
2568
|
-
const raw = readFileSync4(path, "utf-8");
|
|
2569
|
-
try {
|
|
2570
|
-
return JSON.parse(raw);
|
|
2571
|
-
} catch (err) {
|
|
2572
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
2573
|
-
throw new Error(`${path} is not valid JSON: ${detail}`);
|
|
2574
|
-
}
|
|
2575
|
-
}
|
|
2576
|
-
function entryHasCommand(entry, bin) {
|
|
2577
|
-
return entry.hooks?.some((h) => commandMatchesBin(h.command, bin)) ?? false;
|
|
2578
|
-
}
|
|
2579
|
-
function ownedRegistrationEntries(settings) {
|
|
2580
|
-
return Object.entries(settings.hooks ?? {}).flatMap(
|
|
2581
|
-
([event, entries]) => (entries ?? []).filter((entry) => PRIM_BINS.some((bin) => entryHasCommand(entry, bin))).map((entry) => ({ event, entry }))
|
|
2582
|
-
);
|
|
2583
|
-
}
|
|
2584
|
-
function hasAnyHookRegistration(settings) {
|
|
2585
|
-
return ownedRegistrationEntries(settings).length > 0;
|
|
2586
|
-
}
|
|
2587
|
-
function entryMatchesRegistration(owned, registration) {
|
|
2588
|
-
return owned.event === registration.event && owned.entry.matcher === registration.matcher && owned.entry.hooks?.length === 1 && owned.entry.hooks[0].type === "command" && owned.entry.hooks[0].command === registration.command;
|
|
2589
|
-
}
|
|
2590
|
-
function hasCompleteHookRegistration(settings) {
|
|
2591
|
-
const owned = ownedRegistrationEntries(settings);
|
|
2592
|
-
return owned.length === REGISTRATIONS.length && REGISTRATIONS.every(
|
|
2593
|
-
(registration) => owned.filter((entry) => entryMatchesRegistration(entry, registration)).length === 1
|
|
2594
|
-
);
|
|
2595
|
-
}
|
|
2596
|
-
function canonicalEntry(reg) {
|
|
2597
|
-
return { matcher: reg.matcher, hooks: [{ type: "command", command: reg.command }] };
|
|
2598
|
-
}
|
|
2599
|
-
function stripCommand(list, bin) {
|
|
2600
|
-
const out = [];
|
|
2601
|
-
for (const e of list) {
|
|
2602
|
-
const hooks = (e.hooks ?? []).filter((h) => !commandMatchesBin(h.command, bin));
|
|
2603
|
-
if (hooks.length > 0) {
|
|
2604
|
-
out.push({ ...e, hooks });
|
|
2605
|
-
}
|
|
2606
|
-
}
|
|
2607
|
-
return out;
|
|
2608
|
-
}
|
|
2609
|
-
function ensureRegistration(list, reg, force) {
|
|
2610
|
-
const isCanonical = (e) => e.matcher === reg.matcher && e.hooks?.length === 1 && e.hooks[0].command === reg.command;
|
|
2611
|
-
const hasStray = (e) => !isCanonical(e) && (e.hooks ?? []).some((h) => commandMatchesBin(h.command, reg.bin));
|
|
2612
|
-
if (list.some(isCanonical) && !list.some(hasStray) && !force) {
|
|
2613
|
-
return list;
|
|
2614
|
-
}
|
|
2615
|
-
return [...stripCommand(list, reg.bin), canonicalEntry(reg)];
|
|
2616
|
-
}
|
|
2617
|
-
var LEGACY_STATUSLINE_COMMAND = "prim statusline";
|
|
2618
|
-
var RUNTIME_STATUSLINE_SUFFIXES = [
|
|
2619
|
-
"/prim/runtime/prim-statusline",
|
|
2620
|
-
// One pre-launcher build persisted the JS entry directly. Keep recognizing
|
|
2621
|
-
// it so reinstall migrates and uninstall removes that older staged form.
|
|
2622
|
-
"/prim/runtime/current/prim-statusline"
|
|
2623
|
-
];
|
|
2624
|
-
function isPrimStatusLine(settings) {
|
|
2625
|
-
const s = settings.statusLine;
|
|
2626
|
-
if (s?.type !== "command") {
|
|
2627
|
-
return false;
|
|
2628
|
-
}
|
|
2629
|
-
const c = (s.command ?? "").trim();
|
|
2630
|
-
return c === LEGACY_STATUSLINE_COMMAND || c.includes("@primitive.ai/prim") && c.includes("statusline") || c.includes("prim-hook-launcher-v1") && commandMatchesBin(c, STATUSLINE_BIN) || RUNTIME_STATUSLINE_SUFFIXES.some((suffix) => c.includes(suffix));
|
|
2631
|
-
}
|
|
2632
|
-
function hookRuntimeResolutions(settings) {
|
|
2633
|
-
const resolutions = hookCommandResolutions(
|
|
2634
|
-
Object.values(settings.hooks ?? {}).flatMap(
|
|
2635
|
-
(entries) => (entries ?? []).flatMap((entry) => (entry.hooks ?? []).map((hook) => hook.command))
|
|
2636
|
-
),
|
|
2637
|
-
PRIM_BINS
|
|
2638
|
-
);
|
|
2639
|
-
if (!isPrimStatusLine(settings)) return resolutions;
|
|
2640
|
-
const statusline = hookCommandResolution(settings.statusLine?.command, STATUSLINE_BIN);
|
|
2641
|
-
resolutions.push(statusline ?? { kind: "legacy_path" });
|
|
2642
|
-
return resolutions;
|
|
2643
|
-
}
|
|
2644
|
-
function inspectHookRuntimeResolutions() {
|
|
2645
|
-
return [
|
|
2646
|
-
...hookRuntimeResolutions(readSettings(USER_SCOPE_PATH)),
|
|
2647
|
-
...hookRuntimeResolutions(readSettings(projectScopePath()))
|
|
2648
|
-
];
|
|
2649
|
-
}
|
|
2650
|
-
function applyStatusLine(settings, command = STATUSLINE_COMMAND) {
|
|
2651
|
-
if (command === false) {
|
|
2652
|
-
return isPrimStatusLine(settings) ? { ...settings, statusLine: void 0 } : settings;
|
|
2653
|
-
}
|
|
2654
|
-
if (settings.statusLine && !isPrimStatusLine(settings)) {
|
|
2655
|
-
return settings;
|
|
2656
|
-
}
|
|
2657
|
-
return {
|
|
2658
|
-
...settings,
|
|
2659
|
-
statusLine: {
|
|
2660
|
-
type: "command",
|
|
2661
|
-
command,
|
|
2662
|
-
refreshInterval: STATUSLINE_REFRESH_SECONDS
|
|
2663
|
-
}
|
|
2664
|
-
};
|
|
2665
|
-
}
|
|
2666
|
-
function applyPermissions(settings) {
|
|
2667
|
-
const permissions = settings.permissions ?? {};
|
|
2668
|
-
const allow = permissions.allow ?? [];
|
|
2669
|
-
const hasCanonical = allow.includes(PRIM_PERMISSION_RULE);
|
|
2670
|
-
const hasLegacy = allow.some((rule) => LEGACY_PRIM_PERMISSION_RULES.includes(rule));
|
|
2671
|
-
if (hasCanonical && !hasLegacy) {
|
|
2672
|
-
return settings;
|
|
2673
|
-
}
|
|
2674
|
-
const withoutPrim = allow.filter((rule) => !ALL_PRIM_PERMISSION_RULES.has(rule));
|
|
2675
|
-
return {
|
|
2676
|
-
...settings,
|
|
2677
|
-
permissions: { ...permissions, allow: [...withoutPrim, PRIM_PERMISSION_RULE] }
|
|
2678
|
-
};
|
|
2679
|
-
}
|
|
2680
|
-
function removePrimPermission(settings) {
|
|
2681
|
-
const permissions = settings.permissions;
|
|
2682
|
-
if (!permissions?.allow?.some((rule) => ALL_PRIM_PERMISSION_RULES.has(rule))) {
|
|
2683
|
-
return settings;
|
|
2684
|
-
}
|
|
2685
|
-
const allow = permissions.allow.filter((rule) => !ALL_PRIM_PERMISSION_RULES.has(rule));
|
|
2686
|
-
const nextPermissions = {
|
|
2687
|
-
...permissions,
|
|
2688
|
-
allow: allow.length > 0 ? allow : void 0
|
|
2689
|
-
};
|
|
2690
|
-
const hasOtherPerms = Object.values(nextPermissions).some((v) => v !== void 0);
|
|
2691
|
-
return { ...settings, permissions: hasOtherPerms ? nextPermissions : void 0 };
|
|
2692
|
-
}
|
|
2693
|
-
function applyAutoModeTrust(settings) {
|
|
2694
|
-
const autoMode = settings.autoMode ?? {};
|
|
2695
|
-
const env = autoMode.environment;
|
|
2696
|
-
if (env?.includes(PRIM_AUTOMODE_TRUST)) {
|
|
2697
|
-
return settings;
|
|
2698
|
-
}
|
|
2699
|
-
const nextEnv = env ? [...env, PRIM_AUTOMODE_TRUST] : [AUTOMODE_DEFAULTS, PRIM_AUTOMODE_TRUST];
|
|
2700
|
-
return { ...settings, autoMode: { ...autoMode, environment: nextEnv } };
|
|
2701
|
-
}
|
|
2702
|
-
function removeAutoModeTrust(settings) {
|
|
2703
|
-
const autoMode = settings.autoMode;
|
|
2704
|
-
if (!autoMode?.environment?.includes(PRIM_AUTOMODE_TRUST)) {
|
|
2705
|
-
return settings;
|
|
2706
|
-
}
|
|
2707
|
-
const env = autoMode.environment.filter((e) => e !== PRIM_AUTOMODE_TRUST);
|
|
2708
|
-
const onlyDefaultsLeft = env.length === 0 || env.length === 1 && env[0] === AUTOMODE_DEFAULTS;
|
|
2709
|
-
const nextAutoMode = {
|
|
2710
|
-
...autoMode,
|
|
2711
|
-
environment: onlyDefaultsLeft ? void 0 : env
|
|
2712
|
-
};
|
|
2713
|
-
const hasOther = Object.values(nextAutoMode).some((v) => v !== void 0);
|
|
2714
|
-
return { ...settings, autoMode: hasOther ? nextAutoMode : void 0 };
|
|
2715
|
-
}
|
|
2716
|
-
function applyInstall(settings, options = {}) {
|
|
2717
|
-
const hooks = { ...settings.hooks ?? {} };
|
|
2718
|
-
for (const reg of REGISTRATIONS) {
|
|
2719
|
-
hooks[reg.event] = ensureRegistration(hooks[reg.event] ?? [], reg, options.force ?? false);
|
|
2720
|
-
}
|
|
2721
|
-
return applyPermissions(
|
|
2722
|
-
applyStatusLine(
|
|
2723
|
-
{ ...settings, hooks },
|
|
2724
|
-
options.installStatusline === false ? false : options.statuslineCommand ?? STATUSLINE_COMMAND
|
|
2725
|
-
)
|
|
2726
|
-
);
|
|
2727
|
-
}
|
|
2728
|
-
function applyUninstall(settings) {
|
|
2729
|
-
const source = settings.hooks ?? {};
|
|
2730
|
-
const hooks = {};
|
|
2731
|
-
for (const event of Object.keys(source)) {
|
|
2732
|
-
let list = source[event] ?? [];
|
|
2733
|
-
for (const bin of PRIM_BINS) {
|
|
2734
|
-
list = stripCommand(list, bin);
|
|
2735
|
-
}
|
|
2736
|
-
if (list.length > 0) {
|
|
2737
|
-
hooks[event] = list;
|
|
2738
|
-
}
|
|
2739
|
-
}
|
|
2740
|
-
const next = { ...settings, hooks };
|
|
2741
|
-
if (isPrimStatusLine(next)) {
|
|
2742
|
-
next.statusLine = void 0;
|
|
2743
|
-
}
|
|
2744
|
-
return removeAutoModeTrust(removePrimPermission(next));
|
|
2745
|
-
}
|
|
2746
|
-
function captureInstalled(settings) {
|
|
2747
|
-
return CAPTURE_EVENTS.some(
|
|
2748
|
-
(event) => (settings.hooks?.[event] ?? []).some((e) => entryHasCommand(e, CAPTURE_BIN))
|
|
2749
|
-
);
|
|
2750
|
-
}
|
|
2751
|
-
function feedbackInstalled(settings) {
|
|
2752
|
-
const stopCapture = (settings.hooks?.Stop ?? []).some(
|
|
2753
|
-
(entry) => entryHasCommand(entry, CAPTURE_BIN)
|
|
2754
|
-
);
|
|
2755
|
-
const sessionStart = (settings.hooks?.SessionStart ?? []).some(
|
|
2756
|
-
(entry) => entryHasCommand(entry, SESSION_START_BIN)
|
|
2757
|
-
);
|
|
2758
|
-
return stopCapture && sessionStart;
|
|
2759
|
-
}
|
|
2760
|
-
function statuslineInstalled(settings) {
|
|
2761
|
-
return isPrimStatusLine(settings);
|
|
2762
|
-
}
|
|
2763
|
-
function statuslineState(settings) {
|
|
2764
|
-
if (isPrimStatusLine(settings)) {
|
|
2765
|
-
return "primitive";
|
|
2766
|
-
}
|
|
2767
|
-
return settings.statusLine ? "custom" : "empty";
|
|
2768
|
-
}
|
|
2769
|
-
function isGateInstalled(settings) {
|
|
2770
|
-
return (settings.hooks?.PreToolUse ?? []).some((e) => entryHasCommand(e, GATE_BIN));
|
|
2771
|
-
}
|
|
2772
|
-
function atomicWrite(path, content) {
|
|
2773
|
-
atomicWriteFile(path, `${JSON.stringify(content, null, JSON_INDENT)}
|
|
2774
|
-
`, {
|
|
2775
|
-
ensureParent: true
|
|
2776
|
-
});
|
|
2777
|
-
}
|
|
2778
|
-
function userStatuslineBlocksProjectInstall(scope, userSettings) {
|
|
2779
|
-
return scope === "project" && statuslineState(userSettings) === "custom";
|
|
2780
|
-
}
|
|
2781
|
-
function performInstall(scope, force) {
|
|
2782
|
-
stageHookRuntime();
|
|
2783
|
-
const path = settingsPathFor(scope);
|
|
2784
|
-
const before = readSettings(path);
|
|
2785
|
-
const blockedByUserStatusline = userStatuslineBlocksProjectInstall(
|
|
2786
|
-
scope,
|
|
2787
|
-
scope === "project" ? readSettings(USER_SCOPE_PATH) : {}
|
|
2788
|
-
);
|
|
2789
|
-
const after = applyInstall(before, {
|
|
2790
|
-
force,
|
|
2791
|
-
statuslineCommand: STATUSLINE_COMMAND,
|
|
2792
|
-
installStatusline: !blockedByUserStatusline
|
|
2793
|
-
});
|
|
2794
|
-
const changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
2795
|
-
if (changed) {
|
|
2796
|
-
atomicWrite(path, after);
|
|
2797
|
-
}
|
|
2798
|
-
return {
|
|
2799
|
-
scope,
|
|
2800
|
-
path,
|
|
2801
|
-
gate: isGateInstalled(after),
|
|
2802
|
-
capture: captureInstalled(after),
|
|
2803
|
-
feedback: feedbackInstalled(after),
|
|
2804
|
-
statusline: statuslineInstalled(after),
|
|
2805
|
-
statuslineState: statuslineState(after),
|
|
2806
|
-
changed
|
|
2807
|
-
};
|
|
2808
|
-
}
|
|
2809
|
-
function performUninstall(scope) {
|
|
2810
|
-
const path = settingsPathFor(scope);
|
|
2811
|
-
const before = readSettings(path);
|
|
2812
|
-
const after = applyUninstall(before);
|
|
2813
|
-
const changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
2814
|
-
if (changed) {
|
|
2815
|
-
atomicWrite(path, after);
|
|
2816
|
-
}
|
|
2817
|
-
return {
|
|
2818
|
-
scope,
|
|
2819
|
-
path,
|
|
2820
|
-
gate: isGateInstalled(after),
|
|
2821
|
-
capture: captureInstalled(after),
|
|
2822
|
-
feedback: feedbackInstalled(after),
|
|
2823
|
-
statusline: statuslineInstalled(after),
|
|
2824
|
-
statuslineState: statuslineState(after),
|
|
2825
|
-
changed
|
|
2826
|
-
};
|
|
2827
|
-
}
|
|
2828
|
-
function performPermissionInstall(scope) {
|
|
2829
|
-
const path = settingsPathFor(scope);
|
|
2830
|
-
const before = readSettings(path);
|
|
2831
|
-
const after = applyAutoModeTrust(applyPermissions(before));
|
|
2832
|
-
const changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
2833
|
-
if (changed) {
|
|
2834
|
-
atomicWrite(path, after);
|
|
2835
|
-
}
|
|
2836
|
-
return {
|
|
2837
|
-
scope,
|
|
2838
|
-
path,
|
|
2839
|
-
allowed: (after.permissions?.allow ?? []).includes(PRIM_PERMISSION_RULE),
|
|
2840
|
-
autoModeTrusted: (after.autoMode?.environment ?? []).includes(PRIM_AUTOMODE_TRUST),
|
|
2841
|
-
changed
|
|
2842
|
-
};
|
|
2843
|
-
}
|
|
2844
|
-
function performStatus() {
|
|
2845
|
-
const statusFor = (path) => {
|
|
2846
|
-
const settings = readSettings(path);
|
|
2847
|
-
return {
|
|
2848
|
-
path,
|
|
2849
|
-
present: hasAnyHookRegistration(settings) || statuslineInstalled(settings),
|
|
2850
|
-
gate: isGateInstalled(settings),
|
|
2851
|
-
capture: captureInstalled(settings),
|
|
2852
|
-
feedback: feedbackInstalled(settings),
|
|
2853
|
-
complete: hasCompleteHookRegistration(settings),
|
|
2854
|
-
statusline: statuslineInstalled(settings),
|
|
2855
|
-
statuslineState: statuslineState(settings)
|
|
2856
|
-
};
|
|
2857
|
-
};
|
|
2858
|
-
return { user: statusFor(USER_SCOPE_PATH), project: statusFor(projectScopePath()) };
|
|
2859
|
-
}
|
|
2860
|
-
function resolveScope(input) {
|
|
2861
|
-
if (input === void 0 || input === "project") {
|
|
2862
|
-
return "project";
|
|
2863
|
-
}
|
|
2864
|
-
if (input === "user") {
|
|
2865
|
-
return "user";
|
|
2866
|
-
}
|
|
2867
|
-
console.error(`[prim] unknown --scope "${input}" (expected: user or project)`);
|
|
2868
|
-
process.exit(1);
|
|
2869
|
-
}
|
|
2870
|
-
function registerClaudeCommands(program2) {
|
|
2871
|
-
const claude = program2.command("claude").description("Manage the prim Claude Code integration (capture, gate, ingest, presence)");
|
|
2872
|
-
claude.command("install").description("Register the prim hooks + statusline in Claude Code's settings.json").option(
|
|
2873
|
-
"--scope <scope>",
|
|
2874
|
-
"project (default, the repo's .claude/settings.json) or user (~/.claude/settings.json)"
|
|
2875
|
-
).option("--force", "Replace any drifted prim hook entries").action((opts) => {
|
|
2876
|
-
const scope = resolveScope(opts.scope);
|
|
2877
|
-
const result = performInstall(scope, opts.force ?? false);
|
|
2878
|
-
if (result.changed) {
|
|
2879
|
-
console.error(
|
|
2880
|
-
`[prim] Claude Code integration installed (${scope} scope) at ${result.path}`
|
|
2881
|
-
);
|
|
2882
|
-
} else {
|
|
2883
|
-
console.error(
|
|
2884
|
-
`[prim] Claude Code integration already present at ${result.path} (no changes)`
|
|
2885
|
-
);
|
|
1927
|
+
try {
|
|
1928
|
+
postRewriteHook = ensureEffectivePostRewriteHook(root).path;
|
|
1929
|
+
} catch (error) {
|
|
1930
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1931
|
+
process.stderr.write(`[prim] post-rewrite hook coverage is degraded: ${detail}
|
|
1932
|
+
`);
|
|
1933
|
+
}
|
|
1934
|
+
phase = "GitHub repo connection";
|
|
1935
|
+
binding = await bindRepository(root);
|
|
2886
1936
|
}
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
1937
|
+
if (active && binding?.status === "unbound") {
|
|
1938
|
+
const connected = await maybeConnectRepository(root, globals);
|
|
1939
|
+
if (!connected) {
|
|
1940
|
+
process.stderr.write(`[prim] ${GITHUB_CONNECTION_REQUIRED}
|
|
1941
|
+
`);
|
|
1942
|
+
printJson({
|
|
1943
|
+
active: false,
|
|
1944
|
+
repo: root,
|
|
1945
|
+
bindingStatus: binding.status,
|
|
1946
|
+
repositoryFullName: binding.repositoryFullName,
|
|
1947
|
+
...postCommitHook ? { postCommitHook } : {},
|
|
1948
|
+
...postRewriteHook ? { postRewriteHook } : {}
|
|
1949
|
+
});
|
|
1950
|
+
process.exitCode = 1;
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
binding = connected;
|
|
2894
1954
|
}
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
"--scope <scope>",
|
|
2901
|
-
"user (default for preauth \u2014 covers every repo) or project (this repo's .claude/settings.json)"
|
|
2902
|
-
).action((opts) => {
|
|
2903
|
-
const scope = resolveScope(opts.scope ?? "user");
|
|
2904
|
-
const result = performPermissionInstall(scope);
|
|
2905
|
-
console.error(
|
|
2906
|
-
result.changed ? `[prim] prim pre-authorization written (${scope} scope) at ${result.path} (allow-rule + auto-mode trust)` : `[prim] prim pre-authorization already present at ${result.path} (no changes)`
|
|
2907
|
-
);
|
|
2908
|
-
console.log(JSON.stringify(result, null, JSON_INDENT));
|
|
2909
|
-
});
|
|
2910
|
-
claude.command("uninstall").description("Remove all prim hooks + the prim statusline from settings.json").option(
|
|
2911
|
-
"--scope <scope>",
|
|
2912
|
-
"project (default, the repo's .claude/settings.json) or user (~/.claude/settings.json)"
|
|
2913
|
-
).action((opts) => {
|
|
2914
|
-
const scope = resolveScope(opts.scope);
|
|
2915
|
-
const result = performUninstall(scope);
|
|
2916
|
-
if (result.changed) {
|
|
2917
|
-
console.error(`[prim] prim hooks removed from ${result.path}`);
|
|
2918
|
-
} else {
|
|
2919
|
-
console.error(`[prim] no prim hooks to remove at ${result.path} (nothing changed)`);
|
|
1955
|
+
if (active) {
|
|
1956
|
+
try {
|
|
1957
|
+
await fetchAndCacheCollectScope(root);
|
|
1958
|
+
} catch {
|
|
1959
|
+
}
|
|
2920
1960
|
}
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
1961
|
+
phase = "local activation";
|
|
1962
|
+
setRepoActive(root, active);
|
|
1963
|
+
await daemonRequest("statusline_invalidate", {}, { timeoutMs: 250 });
|
|
1964
|
+
process.stderr.write(`[prim] prim ${active ? "enabled" : "disabled"} in ${root}
|
|
1965
|
+
`);
|
|
1966
|
+
printJson({
|
|
1967
|
+
active,
|
|
1968
|
+
repo: root,
|
|
1969
|
+
...binding ? {
|
|
1970
|
+
bindingStatus: binding.status,
|
|
1971
|
+
repositoryFullName: binding.repositoryFullName,
|
|
1972
|
+
...binding.status === "connected" ? { repoSyncId: binding.repoSyncId } : {}
|
|
1973
|
+
} : {},
|
|
1974
|
+
...postCommitHook ? { postCommitHook } : {},
|
|
1975
|
+
...postRewriteHook ? { postRewriteHook } : {}
|
|
1976
|
+
});
|
|
1977
|
+
} catch (err) {
|
|
1978
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1979
|
+
process.stderr.write(
|
|
1980
|
+
active ? `[prim] failed to enable prim during ${phase}: ${detail}
|
|
1981
|
+
` : `[prim] failed to disable prim: ${detail}
|
|
1982
|
+
`
|
|
1983
|
+
);
|
|
1984
|
+
process.exit(1);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
function registerActivationCommands(program2) {
|
|
1988
|
+
program2.command("enable").description("Activate prim's hooks in this repo (git config prim.active=true)").action((_opts, command) => applyActivation(true, command.optsWithGlobals()));
|
|
1989
|
+
program2.command("disable").description("Mute prim's hooks in this repo (git config prim.active=false)").action(
|
|
1990
|
+
(_opts, command) => applyActivation(false, command.optsWithGlobals())
|
|
1991
|
+
);
|
|
2933
1992
|
}
|
|
2934
1993
|
|
|
2935
1994
|
// src/commands/codex-install.ts
|
|
2936
|
-
import { homedir as
|
|
2937
|
-
import { join as
|
|
2938
|
-
var
|
|
2939
|
-
var
|
|
2940
|
-
var
|
|
2941
|
-
var
|
|
1995
|
+
import { homedir as homedir2 } from "os";
|
|
1996
|
+
import { join as join2 } from "path";
|
|
1997
|
+
var CAPTURE_BIN = "prim-hook";
|
|
1998
|
+
var GATE_BIN = "prim-pre-tool-use";
|
|
1999
|
+
var POST_TOOL_USE_BIN = "prim-post-tool-use";
|
|
2000
|
+
var SESSION_START_BIN = "prim-session-start";
|
|
2942
2001
|
var CODEX_ARGS = "--agent codex";
|
|
2943
2002
|
var CODEX_EDIT_MATCHER = "apply_patch|Bash";
|
|
2944
2003
|
var CODEX_POST_TOOL_MATCHER = "apply_patch|Bash";
|
|
2945
|
-
var
|
|
2004
|
+
var JSON_INDENT = 2;
|
|
2946
2005
|
var CODEX_CAPTURE_EVENTS = [
|
|
2947
2006
|
"SessionStart",
|
|
2948
2007
|
"UserPromptSubmit",
|
|
@@ -2951,31 +2010,31 @@ var CODEX_CAPTURE_EVENTS = [
|
|
|
2951
2010
|
"Stop",
|
|
2952
2011
|
"SubagentStop"
|
|
2953
2012
|
];
|
|
2954
|
-
var
|
|
2013
|
+
var PRIM_BINS = [CAPTURE_BIN, GATE_BIN, POST_TOOL_USE_BIN, SESSION_START_BIN];
|
|
2955
2014
|
var CODEX_REGISTRATIONS = [
|
|
2956
|
-
...CODEX_CAPTURE_EVENTS.map((event) => makeRegistration(event, "*",
|
|
2957
|
-
makeRegistration("PreToolUse", CODEX_EDIT_MATCHER,
|
|
2958
|
-
makeRegistration("PostToolUse", CODEX_POST_TOOL_MATCHER,
|
|
2959
|
-
makeRegistration("SessionStart", "*",
|
|
2015
|
+
...CODEX_CAPTURE_EVENTS.map((event) => makeRegistration(event, "*", CAPTURE_BIN, CODEX_ARGS)),
|
|
2016
|
+
makeRegistration("PreToolUse", CODEX_EDIT_MATCHER, GATE_BIN, CODEX_ARGS),
|
|
2017
|
+
makeRegistration("PostToolUse", CODEX_POST_TOOL_MATCHER, POST_TOOL_USE_BIN, CODEX_ARGS),
|
|
2018
|
+
makeRegistration("SessionStart", "*", SESSION_START_BIN, CODEX_ARGS)
|
|
2960
2019
|
];
|
|
2961
|
-
var
|
|
2962
|
-
var
|
|
2963
|
-
function
|
|
2964
|
-
return scope === "user" ?
|
|
2020
|
+
var USER_SCOPE_PATH = join2(homedir2(), ".codex", "hooks.json");
|
|
2021
|
+
var projectScopePath = () => join2(projectRoot(), ".codex", "hooks.json");
|
|
2022
|
+
function settingsPathFor(scope) {
|
|
2023
|
+
return scope === "user" ? USER_SCOPE_PATH : projectScopePath();
|
|
2965
2024
|
}
|
|
2966
|
-
function
|
|
2025
|
+
function applyInstall(settings, options = {}) {
|
|
2967
2026
|
const hooks = { ...settings.hooks ?? {} };
|
|
2968
2027
|
for (const reg of CODEX_REGISTRATIONS) {
|
|
2969
2028
|
hooks[reg.event] = ensureRegistration(hooks[reg.event] ?? [], reg, options.force ?? false);
|
|
2970
2029
|
}
|
|
2971
2030
|
return { ...settings, hooks };
|
|
2972
2031
|
}
|
|
2973
|
-
function
|
|
2032
|
+
function applyUninstall(settings) {
|
|
2974
2033
|
const source = settings.hooks ?? {};
|
|
2975
2034
|
const hooks = {};
|
|
2976
2035
|
for (const event of Object.keys(source)) {
|
|
2977
2036
|
let list = source[event] ?? [];
|
|
2978
|
-
for (const bin of
|
|
2037
|
+
for (const bin of PRIM_BINS) {
|
|
2979
2038
|
list = stripCommand(list, bin);
|
|
2980
2039
|
}
|
|
2981
2040
|
if (list.length > 0) {
|
|
@@ -2984,89 +2043,89 @@ function applyUninstall2(settings) {
|
|
|
2984
2043
|
}
|
|
2985
2044
|
return { ...settings, hooks };
|
|
2986
2045
|
}
|
|
2987
|
-
function
|
|
2046
|
+
function captureInstalled(settings) {
|
|
2988
2047
|
return CODEX_CAPTURE_EVENTS.some(
|
|
2989
|
-
(event) => (settings.hooks?.[event] ?? []).some((e) => entryHasCommand(e,
|
|
2048
|
+
(event) => (settings.hooks?.[event] ?? []).some((e) => entryHasCommand(e, CAPTURE_BIN))
|
|
2990
2049
|
);
|
|
2991
2050
|
}
|
|
2992
|
-
function
|
|
2051
|
+
function ownedRegistrationEntries(settings) {
|
|
2993
2052
|
return Object.entries(settings.hooks ?? {}).flatMap(
|
|
2994
|
-
([event, entries]) => (entries ?? []).filter((entry) =>
|
|
2053
|
+
([event, entries]) => (entries ?? []).filter((entry) => PRIM_BINS.some((bin) => entryHasCommand(entry, bin))).map((entry) => ({ event, entry }))
|
|
2995
2054
|
);
|
|
2996
2055
|
}
|
|
2997
|
-
function
|
|
2998
|
-
return
|
|
2056
|
+
function hasAnyHookRegistration(settings) {
|
|
2057
|
+
return ownedRegistrationEntries(settings).length > 0;
|
|
2999
2058
|
}
|
|
3000
|
-
function
|
|
2059
|
+
function hookRuntimeResolutions(settings) {
|
|
3001
2060
|
return hookCommandResolutions(
|
|
3002
2061
|
Object.values(settings.hooks ?? {}).flatMap(
|
|
3003
2062
|
(entries) => (entries ?? []).flatMap((entry) => (entry.hooks ?? []).map((hook) => hook.command))
|
|
3004
2063
|
),
|
|
3005
|
-
|
|
2064
|
+
PRIM_BINS
|
|
3006
2065
|
);
|
|
3007
2066
|
}
|
|
3008
|
-
function
|
|
2067
|
+
function inspectHookRuntimeResolutions3() {
|
|
3009
2068
|
return [
|
|
3010
|
-
...
|
|
3011
|
-
...
|
|
2069
|
+
...hookRuntimeResolutions(readSettings(USER_SCOPE_PATH)),
|
|
2070
|
+
...hookRuntimeResolutions(readSettings(projectScopePath()))
|
|
3012
2071
|
];
|
|
3013
2072
|
}
|
|
3014
|
-
function
|
|
2073
|
+
function entryMatchesRegistration(owned, registration) {
|
|
3015
2074
|
return owned.event === registration.event && owned.entry.matcher === registration.matcher && owned.entry.hooks?.length === 1 && owned.entry.hooks[0].type === "command" && owned.entry.hooks[0].command === registration.command;
|
|
3016
2075
|
}
|
|
3017
|
-
function
|
|
3018
|
-
const owned =
|
|
2076
|
+
function hasCompleteHookRegistration(settings) {
|
|
2077
|
+
const owned = ownedRegistrationEntries(settings);
|
|
3019
2078
|
return owned.length === CODEX_REGISTRATIONS.length && CODEX_REGISTRATIONS.every(
|
|
3020
|
-
(registration) => owned.filter((entry) =>
|
|
2079
|
+
(registration) => owned.filter((entry) => entryMatchesRegistration(entry, registration)).length === 1
|
|
3021
2080
|
);
|
|
3022
2081
|
}
|
|
3023
|
-
function
|
|
3024
|
-
return (settings.hooks?.PreToolUse ?? []).some((e) => entryHasCommand(e,
|
|
2082
|
+
function isGateInstalled(settings) {
|
|
2083
|
+
return (settings.hooks?.PreToolUse ?? []).some((e) => entryHasCommand(e, GATE_BIN));
|
|
3025
2084
|
}
|
|
3026
2085
|
function resultFor(scope, path, after, changed) {
|
|
3027
2086
|
return {
|
|
3028
2087
|
scope,
|
|
3029
2088
|
path,
|
|
3030
|
-
gate:
|
|
3031
|
-
capture:
|
|
2089
|
+
gate: isGateInstalled(after),
|
|
2090
|
+
capture: captureInstalled(after),
|
|
3032
2091
|
changed
|
|
3033
2092
|
};
|
|
3034
2093
|
}
|
|
3035
|
-
function
|
|
2094
|
+
function performInstall(scope, force) {
|
|
3036
2095
|
stageHookRuntime();
|
|
3037
|
-
const path =
|
|
2096
|
+
const path = settingsPathFor(scope);
|
|
3038
2097
|
const before = readSettings(path);
|
|
3039
|
-
const after =
|
|
2098
|
+
const after = applyInstall(before, { force });
|
|
3040
2099
|
const changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
3041
2100
|
if (changed) {
|
|
3042
2101
|
atomicWrite(path, after);
|
|
3043
2102
|
}
|
|
3044
2103
|
return resultFor(scope, path, after, changed);
|
|
3045
2104
|
}
|
|
3046
|
-
function
|
|
3047
|
-
const path =
|
|
2105
|
+
function performUninstall(scope) {
|
|
2106
|
+
const path = settingsPathFor(scope);
|
|
3048
2107
|
const before = readSettings(path);
|
|
3049
|
-
const after =
|
|
2108
|
+
const after = applyUninstall(before);
|
|
3050
2109
|
const changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
3051
2110
|
if (changed) {
|
|
3052
2111
|
atomicWrite(path, after);
|
|
3053
2112
|
}
|
|
3054
2113
|
return resultFor(scope, path, after, changed);
|
|
3055
2114
|
}
|
|
3056
|
-
function
|
|
2115
|
+
function performStatus3() {
|
|
3057
2116
|
const statusFor = (path) => {
|
|
3058
2117
|
const settings = readSettings(path);
|
|
3059
2118
|
return {
|
|
3060
2119
|
path,
|
|
3061
|
-
present:
|
|
3062
|
-
gate:
|
|
3063
|
-
capture:
|
|
3064
|
-
complete:
|
|
2120
|
+
present: hasAnyHookRegistration(settings),
|
|
2121
|
+
gate: isGateInstalled(settings),
|
|
2122
|
+
capture: captureInstalled(settings),
|
|
2123
|
+
complete: hasCompleteHookRegistration(settings)
|
|
3065
2124
|
};
|
|
3066
2125
|
};
|
|
3067
|
-
return { user: statusFor(
|
|
2126
|
+
return { user: statusFor(USER_SCOPE_PATH), project: statusFor(projectScopePath()) };
|
|
3068
2127
|
}
|
|
3069
|
-
function
|
|
2128
|
+
function resolveScope(input) {
|
|
3070
2129
|
if (input === void 0 || input === "project") {
|
|
3071
2130
|
return "project";
|
|
3072
2131
|
}
|
|
@@ -3083,48 +2142,48 @@ function registerCodexCommands(program2) {
|
|
|
3083
2142
|
"--scope <scope>",
|
|
3084
2143
|
"project (default, the repo's .codex/hooks.json) or user (~/.codex/hooks.json)"
|
|
3085
2144
|
).option("--force", "Replace any drifted prim hook entries").action((opts) => {
|
|
3086
|
-
const scope =
|
|
3087
|
-
const result =
|
|
2145
|
+
const scope = resolveScope(opts.scope);
|
|
2146
|
+
const result = performInstall(scope, opts.force ?? false);
|
|
3088
2147
|
if (result.changed) {
|
|
3089
2148
|
console.error(`[prim] Codex integration installed (${scope} scope) at ${result.path}`);
|
|
3090
2149
|
} else {
|
|
3091
2150
|
console.error(`[prim] Codex integration already present at ${result.path} (no changes)`);
|
|
3092
2151
|
}
|
|
3093
2152
|
console.error(TRUST_NOTICE);
|
|
3094
|
-
console.log(JSON.stringify(result, null,
|
|
2153
|
+
console.log(JSON.stringify(result, null, JSON_INDENT));
|
|
3095
2154
|
});
|
|
3096
2155
|
codex.command("uninstall").description("Remove all prim hooks from Codex's hooks.json").option(
|
|
3097
2156
|
"--scope <scope>",
|
|
3098
2157
|
"project (default, the repo's .codex/hooks.json) or user (~/.codex/hooks.json)"
|
|
3099
2158
|
).action((opts) => {
|
|
3100
|
-
const scope =
|
|
3101
|
-
const result =
|
|
2159
|
+
const scope = resolveScope(opts.scope);
|
|
2160
|
+
const result = performUninstall(scope);
|
|
3102
2161
|
if (result.changed) {
|
|
3103
2162
|
console.error(`[prim] prim hooks removed from ${result.path}`);
|
|
3104
2163
|
} else {
|
|
3105
2164
|
console.error(`[prim] no prim hooks to remove at ${result.path} (nothing changed)`);
|
|
3106
2165
|
}
|
|
3107
|
-
console.log(JSON.stringify(result, null,
|
|
2166
|
+
console.log(JSON.stringify(result, null, JSON_INDENT));
|
|
3108
2167
|
});
|
|
3109
2168
|
codex.command("status").description("Report whether each prim surface (gate, capture) is installed per scope").action(() => {
|
|
3110
|
-
const result =
|
|
2169
|
+
const result = performStatus3();
|
|
3111
2170
|
const mark = (b) => b ? "\u2713" : "\u2717";
|
|
3112
2171
|
const line = (label, s) => `[prim] ${label}: gate ${mark(s.gate)} \xB7 capture ${mark(s.capture)} (${s.path})`;
|
|
3113
2172
|
console.error(`${line("user", result.user)}
|
|
3114
2173
|
${line("project", result.project)}`);
|
|
3115
|
-
console.log(JSON.stringify(result, null,
|
|
2174
|
+
console.log(JSON.stringify(result, null, JSON_INDENT));
|
|
3116
2175
|
});
|
|
3117
2176
|
}
|
|
3118
2177
|
|
|
3119
2178
|
// src/commands/daemon.ts
|
|
3120
2179
|
import { spawn as spawn2 } from "child_process";
|
|
3121
|
-
import { closeSync, existsSync as
|
|
3122
|
-
import { join as
|
|
2180
|
+
import { closeSync, existsSync as existsSync4, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync4, unlinkSync as unlinkSync2 } from "fs";
|
|
2181
|
+
import { join as join5 } from "path";
|
|
3123
2182
|
import { Option as Option2 } from "commander";
|
|
3124
2183
|
|
|
3125
2184
|
// src/daemon/latest-bootstrap.ts
|
|
3126
2185
|
import { spawn } from "child_process";
|
|
3127
|
-
import { join as
|
|
2186
|
+
import { join as join3 } from "path";
|
|
3128
2187
|
var LATEST_BOOTSTRAP_TIMEOUT_MS = 45e3;
|
|
3129
2188
|
var LATEST_BOOTSTRAP_LOCK_NAME = "daemon-latest.lock";
|
|
3130
2189
|
async function runPinnedEnsure(options) {
|
|
@@ -3136,7 +2195,7 @@ async function runPinnedEnsure(options) {
|
|
|
3136
2195
|
npm_config_fetch_retry_maxtimeout: "5000",
|
|
3137
2196
|
npm_config_fetch_timeout: "15000"
|
|
3138
2197
|
};
|
|
3139
|
-
return await new Promise((
|
|
2198
|
+
return await new Promise((resolve5) => {
|
|
3140
2199
|
let settled = false;
|
|
3141
2200
|
let child;
|
|
3142
2201
|
try {
|
|
@@ -3146,7 +2205,7 @@ async function runPinnedEnsure(options) {
|
|
|
3146
2205
|
{ env, stdio: "ignore" }
|
|
3147
2206
|
);
|
|
3148
2207
|
} catch {
|
|
3149
|
-
|
|
2208
|
+
resolve5(false);
|
|
3150
2209
|
return;
|
|
3151
2210
|
}
|
|
3152
2211
|
const timer = setTimeout(() => {
|
|
@@ -3157,7 +2216,7 @@ async function runPinnedEnsure(options) {
|
|
|
3157
2216
|
if (settled) return;
|
|
3158
2217
|
settled = true;
|
|
3159
2218
|
clearTimeout(timer);
|
|
3160
|
-
|
|
2219
|
+
resolve5(result);
|
|
3161
2220
|
};
|
|
3162
2221
|
timer.unref();
|
|
3163
2222
|
child.once("error", () => finish(false));
|
|
@@ -3165,7 +2224,7 @@ async function runPinnedEnsure(options) {
|
|
|
3165
2224
|
});
|
|
3166
2225
|
}
|
|
3167
2226
|
async function runLatestDaemonBootstrap(ensureCurrent, options = {}) {
|
|
3168
|
-
const lockPath = options.lockPath ??
|
|
2227
|
+
const lockPath = options.lockPath ?? join3(
|
|
3169
2228
|
primConfigDirectory({ env: options.env, homeDir: options.homeDir }),
|
|
3170
2229
|
LATEST_BOOTSTRAP_LOCK_NAME
|
|
3171
2230
|
);
|
|
@@ -3189,27 +2248,27 @@ async function runLatestDaemonBootstrap(ensureCurrent, options = {}) {
|
|
|
3189
2248
|
|
|
3190
2249
|
// src/daemon/launchd.ts
|
|
3191
2250
|
import { spawnSync } from "child_process";
|
|
3192
|
-
import { createHash as
|
|
2251
|
+
import { createHash as createHash2, randomBytes as randomBytes3 } from "crypto";
|
|
3193
2252
|
import {
|
|
3194
|
-
constants
|
|
3195
|
-
accessSync
|
|
3196
|
-
chmodSync
|
|
3197
|
-
copyFileSync
|
|
3198
|
-
existsSync as
|
|
3199
|
-
lstatSync as
|
|
3200
|
-
mkdirSync as
|
|
3201
|
-
mkdtempSync
|
|
3202
|
-
readFileSync as
|
|
3203
|
-
readdirSync as
|
|
2253
|
+
constants,
|
|
2254
|
+
accessSync,
|
|
2255
|
+
chmodSync,
|
|
2256
|
+
copyFileSync,
|
|
2257
|
+
existsSync as existsSync3,
|
|
2258
|
+
lstatSync as lstatSync2,
|
|
2259
|
+
mkdirSync as mkdirSync2,
|
|
2260
|
+
mkdtempSync,
|
|
2261
|
+
readFileSync as readFileSync3,
|
|
2262
|
+
readdirSync as readdirSync2,
|
|
3204
2263
|
readlinkSync,
|
|
3205
|
-
realpathSync
|
|
3206
|
-
renameSync
|
|
3207
|
-
rmSync
|
|
2264
|
+
realpathSync,
|
|
2265
|
+
renameSync,
|
|
2266
|
+
rmSync,
|
|
3208
2267
|
symlinkSync,
|
|
3209
|
-
writeFileSync as
|
|
2268
|
+
writeFileSync as writeFileSync2
|
|
3210
2269
|
} from "fs";
|
|
3211
|
-
import { homedir as
|
|
3212
|
-
import { basename, dirname as
|
|
2270
|
+
import { homedir as homedir3 } from "os";
|
|
2271
|
+
import { basename, dirname as dirname2, isAbsolute, join as join4, resolve as resolve2 } from "path";
|
|
3213
2272
|
|
|
3214
2273
|
// src/daemon/env-binding.ts
|
|
3215
2274
|
function normalizeApiUrl(url) {
|
|
@@ -3248,49 +2307,49 @@ var LaunchctlError = class extends Error {
|
|
|
3248
2307
|
}
|
|
3249
2308
|
};
|
|
3250
2309
|
function xdgDataHome(options) {
|
|
3251
|
-
const home = options.homeDir ??
|
|
2310
|
+
const home = options.homeDir ?? homedir3();
|
|
3252
2311
|
const configured = (options.env ?? process.env).XDG_DATA_HOME;
|
|
3253
|
-
return configured &&
|
|
2312
|
+
return configured && isAbsolute(configured) ? configured : join4(home, ".local", "share");
|
|
3254
2313
|
}
|
|
3255
2314
|
function runtimePaths(options = {}) {
|
|
3256
|
-
const dataDir =
|
|
3257
|
-
const runtimeDir =
|
|
3258
|
-
const currentLink =
|
|
2315
|
+
const dataDir = join4(xdgDataHome(options), "prim");
|
|
2316
|
+
const runtimeDir = join4(dataDir, "runtime");
|
|
2317
|
+
const currentLink = join4(runtimeDir, "current");
|
|
3259
2318
|
return {
|
|
3260
2319
|
dataDir,
|
|
3261
2320
|
runtimeDir,
|
|
3262
|
-
releasesDir:
|
|
2321
|
+
releasesDir: join4(runtimeDir, "releases"),
|
|
3263
2322
|
currentLink,
|
|
3264
|
-
statuslineLauncher:
|
|
3265
|
-
disabledMarker:
|
|
3266
|
-
lifecycleLockDir:
|
|
2323
|
+
statuslineLauncher: join4(runtimeDir, "prim-statusline"),
|
|
2324
|
+
disabledMarker: join4(dataDir, "daemon.disabled"),
|
|
2325
|
+
lifecycleLockDir: join4(dataDir, "daemon.lifecycle.lock")
|
|
3267
2326
|
};
|
|
3268
2327
|
}
|
|
3269
2328
|
function daemonControlPaths(options = {}) {
|
|
3270
2329
|
const configDir = primConfigDirectory(options);
|
|
3271
2330
|
const legacy = runtimePaths(options);
|
|
3272
2331
|
return {
|
|
3273
|
-
launcher:
|
|
3274
|
-
disabledMarker:
|
|
3275
|
-
lifecycleLockDir:
|
|
2332
|
+
launcher: join4(configDir, `prim-daemon-launcher-v${LAUNCHER_SCHEMA_VERSION}`),
|
|
2333
|
+
disabledMarker: join4(configDir, "daemon.disabled"),
|
|
2334
|
+
lifecycleLockDir: join4(configDir, "daemon.lifecycle.lock"),
|
|
3276
2335
|
legacyDisabledMarker: legacy.disabledMarker,
|
|
3277
2336
|
legacyLifecycleLockDir: legacy.lifecycleLockDir
|
|
3278
2337
|
};
|
|
3279
2338
|
}
|
|
3280
2339
|
function findPackageVersion(entryFile) {
|
|
3281
|
-
let dir =
|
|
2340
|
+
let dir = dirname2(resolve2(entryFile));
|
|
3282
2341
|
for (let depth = 0; depth < 6; depth++) {
|
|
3283
|
-
const packagePath =
|
|
3284
|
-
if (
|
|
2342
|
+
const packagePath = join4(dir, "package.json");
|
|
2343
|
+
if (existsSync3(packagePath)) {
|
|
3285
2344
|
try {
|
|
3286
|
-
const manifest = JSON.parse(
|
|
2345
|
+
const manifest = JSON.parse(readFileSync3(packagePath, "utf8"));
|
|
3287
2346
|
if (manifest.name === "@primitive.ai/prim" && manifest.version) {
|
|
3288
2347
|
return manifest.version;
|
|
3289
2348
|
}
|
|
3290
2349
|
} catch {
|
|
3291
2350
|
}
|
|
3292
2351
|
}
|
|
3293
|
-
const parent =
|
|
2352
|
+
const parent = dirname2(dir);
|
|
3294
2353
|
if (parent === dir) break;
|
|
3295
2354
|
dir = parent;
|
|
3296
2355
|
}
|
|
@@ -3298,7 +2357,7 @@ function findPackageVersion(entryFile) {
|
|
|
3298
2357
|
}
|
|
3299
2358
|
function readRuntimeManifest(path) {
|
|
3300
2359
|
try {
|
|
3301
|
-
const parsed = JSON.parse(
|
|
2360
|
+
const parsed = JSON.parse(readFileSync3(path, "utf8"));
|
|
3302
2361
|
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)) {
|
|
3303
2362
|
return null;
|
|
3304
2363
|
}
|
|
@@ -3308,19 +2367,19 @@ function readRuntimeManifest(path) {
|
|
|
3308
2367
|
}
|
|
3309
2368
|
}
|
|
3310
2369
|
var RUNTIME_RELEASE_RE = /^release-[0-9a-f]{16}$/u;
|
|
3311
|
-
function
|
|
2370
|
+
function exactKeys(value, expected) {
|
|
3312
2371
|
const actual = Object.keys(value).sort();
|
|
3313
2372
|
const wanted = [...expected].sort();
|
|
3314
2373
|
return actual.length === wanted.length && actual.every((key, index) => key === wanted[index]);
|
|
3315
2374
|
}
|
|
3316
2375
|
function readOwnedRuntimeManifest(releaseDir) {
|
|
3317
|
-
const manifestPath =
|
|
3318
|
-
const daemonPath =
|
|
2376
|
+
const manifestPath = join4(releaseDir, "manifest.json");
|
|
2377
|
+
const daemonPath = join4(releaseDir, "prim-daemon-server");
|
|
3319
2378
|
try {
|
|
3320
|
-
const raw =
|
|
2379
|
+
const raw = readFileSync3(manifestPath, "utf8");
|
|
3321
2380
|
const parsed = JSON.parse(raw);
|
|
3322
2381
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
3323
|
-
if (!
|
|
2382
|
+
if (!exactKeys(parsed, [
|
|
3324
2383
|
"schemaVersion",
|
|
3325
2384
|
"version",
|
|
3326
2385
|
"nodePath",
|
|
@@ -3331,11 +2390,11 @@ function readOwnedRuntimeManifest(releaseDir) {
|
|
|
3331
2390
|
return null;
|
|
3332
2391
|
}
|
|
3333
2392
|
const manifest = parsed;
|
|
3334
|
-
if (manifest.schemaVersion !== RUNTIME_SCHEMA_VERSION || compareSemver(manifest.version, manifest.version) !== 0 || !
|
|
3335
|
-
` || !
|
|
2393
|
+
if (manifest.schemaVersion !== RUNTIME_SCHEMA_VERSION || compareSemver(manifest.version, manifest.version) !== 0 || !isAbsolute(manifest.nodePath) || /[\r\n]/u.test(manifest.nodePath) || manifest.daemonFile !== "prim-daemon-server" || !/^[0-9a-f]{64}$/u.test(manifest.daemonSha256) || new Date(manifest.stagedAt).toISOString() !== manifest.stagedAt || raw !== `${JSON.stringify(manifest, null, 2)}
|
|
2394
|
+
` || !lstatSync2(daemonPath).isFile() || sha256File(daemonPath) !== manifest.daemonSha256) {
|
|
3336
2395
|
return null;
|
|
3337
2396
|
}
|
|
3338
|
-
const entries =
|
|
2397
|
+
const entries = readdirSync2(releaseDir).sort();
|
|
3339
2398
|
return entries.length === 2 && entries[0] === "manifest.json" && entries[1] === "prim-daemon-server" ? manifest : null;
|
|
3340
2399
|
} catch {
|
|
3341
2400
|
return null;
|
|
@@ -3352,25 +2411,25 @@ function sameRuntime(current, desired, daemonPath) {
|
|
|
3352
2411
|
}
|
|
3353
2412
|
}
|
|
3354
2413
|
function sha256File(path) {
|
|
3355
|
-
return
|
|
2414
|
+
return createHash2("sha256").update(readFileSync3(path)).digest("hex");
|
|
3356
2415
|
}
|
|
3357
2416
|
function atomicSymlink(target, linkPath) {
|
|
3358
|
-
const tempLink = `${linkPath}.tmp-${process.pid}-${
|
|
2417
|
+
const tempLink = `${linkPath}.tmp-${process.pid}-${createHash2("sha256").update(`${target}\0${Date.now()}\0${Math.random()}`).digest("hex").slice(0, 12)}`;
|
|
3359
2418
|
symlinkSync(target, tempLink);
|
|
3360
2419
|
try {
|
|
3361
|
-
|
|
2420
|
+
renameSync(tempLink, linkPath);
|
|
3362
2421
|
} catch (error) {
|
|
3363
|
-
|
|
2422
|
+
rmSync(tempLink, { force: true });
|
|
3364
2423
|
throw error;
|
|
3365
2424
|
}
|
|
3366
2425
|
}
|
|
3367
2426
|
function stageRuntime(options = {}) {
|
|
3368
2427
|
const paths = runtimePaths(options);
|
|
3369
2428
|
const daemonSource = options.daemonSource ?? binFile("prim-daemon-server");
|
|
3370
|
-
if (!daemonSource || !
|
|
2429
|
+
if (!daemonSource || !existsSync3(daemonSource)) {
|
|
3371
2430
|
throw new Error("cannot stage runtime: prim-daemon-server bundle is unavailable");
|
|
3372
2431
|
}
|
|
3373
|
-
const nodePath =
|
|
2432
|
+
const nodePath = resolve2(options.nodePath ?? process.execPath);
|
|
3374
2433
|
const version = options.version ?? findPackageVersion(daemonSource);
|
|
3375
2434
|
const desired = {
|
|
3376
2435
|
schemaVersion: RUNTIME_SCHEMA_VERSION,
|
|
@@ -3382,11 +2441,11 @@ function stageRuntime(options = {}) {
|
|
|
3382
2441
|
};
|
|
3383
2442
|
let currentRelease = null;
|
|
3384
2443
|
try {
|
|
3385
|
-
currentRelease =
|
|
2444
|
+
currentRelease = realpathSync(paths.currentLink);
|
|
3386
2445
|
} catch {
|
|
3387
2446
|
}
|
|
3388
|
-
const current = currentRelease ? readRuntimeManifest(
|
|
3389
|
-
const currentDaemon = currentRelease ?
|
|
2447
|
+
const current = currentRelease ? readRuntimeManifest(join4(currentRelease, "manifest.json")) : null;
|
|
2448
|
+
const currentDaemon = currentRelease ? join4(currentRelease, desired.daemonFile) : "";
|
|
3390
2449
|
if (currentRelease && sameRuntime(current, desired, currentDaemon)) {
|
|
3391
2450
|
const manifest = current;
|
|
3392
2451
|
atomicWrite2(
|
|
@@ -3401,52 +2460,52 @@ function stageRuntime(options = {}) {
|
|
|
3401
2460
|
daemonPath: currentDaemon
|
|
3402
2461
|
};
|
|
3403
2462
|
}
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
const stagingDir =
|
|
3408
|
-
|
|
2463
|
+
mkdirSync2(paths.releasesDir, { recursive: true, mode: RUNTIME_DIR_MODE });
|
|
2464
|
+
chmodSync(paths.runtimeDir, RUNTIME_DIR_MODE);
|
|
2465
|
+
chmodSync(paths.releasesDir, RUNTIME_DIR_MODE);
|
|
2466
|
+
const stagingDir = mkdtempSync(join4(paths.releasesDir, ".stage-"));
|
|
2467
|
+
chmodSync(stagingDir, RUNTIME_DIR_MODE);
|
|
3409
2468
|
let releaseDir;
|
|
3410
2469
|
try {
|
|
3411
|
-
const daemonTarget =
|
|
3412
|
-
|
|
3413
|
-
|
|
2470
|
+
const daemonTarget = join4(stagingDir, desired.daemonFile);
|
|
2471
|
+
copyFileSync(daemonSource, daemonTarget);
|
|
2472
|
+
chmodSync(daemonTarget, RUNTIME_FILE_MODE);
|
|
3414
2473
|
if (sha256File(daemonTarget) !== desired.daemonSha256) {
|
|
3415
2474
|
throw new Error("cannot stage runtime: daemon bundle changed while it was copied");
|
|
3416
2475
|
}
|
|
3417
|
-
const manifestTarget =
|
|
3418
|
-
|
|
2476
|
+
const manifestTarget = join4(stagingDir, "manifest.json");
|
|
2477
|
+
writeFileSync2(manifestTarget, `${JSON.stringify(desired, null, 2)}
|
|
3419
2478
|
`, {
|
|
3420
2479
|
encoding: "utf8",
|
|
3421
2480
|
mode: RUNTIME_FILE_MODE,
|
|
3422
2481
|
flag: "wx"
|
|
3423
2482
|
});
|
|
3424
|
-
const releaseName = `release-${
|
|
3425
|
-
releaseDir =
|
|
3426
|
-
|
|
3427
|
-
releaseDir =
|
|
3428
|
-
atomicSymlink(
|
|
2483
|
+
const releaseName = `release-${createHash2("sha256").update(`${version}\0${nodePath}\0${desired.stagedAt}\0${process.pid}\0${Math.random()}`).digest("hex").slice(0, 16)}`;
|
|
2484
|
+
releaseDir = join4(paths.releasesDir, releaseName);
|
|
2485
|
+
renameSync(stagingDir, releaseDir);
|
|
2486
|
+
releaseDir = realpathSync(releaseDir);
|
|
2487
|
+
atomicSymlink(join4("releases", releaseName), paths.currentLink);
|
|
3429
2488
|
atomicWrite2(
|
|
3430
2489
|
paths.statuslineLauncher,
|
|
3431
2490
|
statuslineLauncherContent(desired.version, options),
|
|
3432
2491
|
RUNTIME_LAUNCHER_MODE
|
|
3433
2492
|
);
|
|
3434
2493
|
} catch (error) {
|
|
3435
|
-
|
|
2494
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
3436
2495
|
throw error;
|
|
3437
2496
|
}
|
|
3438
2497
|
return {
|
|
3439
2498
|
changed: true,
|
|
3440
2499
|
manifest: desired,
|
|
3441
2500
|
paths,
|
|
3442
|
-
daemonPath:
|
|
2501
|
+
daemonPath: join4(releaseDir, desired.daemonFile)
|
|
3443
2502
|
};
|
|
3444
2503
|
}
|
|
3445
2504
|
function shellQuote(value) {
|
|
3446
2505
|
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
3447
2506
|
}
|
|
3448
2507
|
function statuslineLauncherContent(version, options = {}) {
|
|
3449
|
-
const socketPath =
|
|
2508
|
+
const socketPath = join4(primConfigDirectory(options), "sock");
|
|
3450
2509
|
const fallback = `primitive ${version} (daemon: down)`;
|
|
3451
2510
|
return `#!/bin/sh
|
|
3452
2511
|
response=$(
|
|
@@ -3462,14 +2521,14 @@ fi
|
|
|
3462
2521
|
`;
|
|
3463
2522
|
}
|
|
3464
2523
|
function assertRegularFile(path, label) {
|
|
3465
|
-
const stat =
|
|
2524
|
+
const stat = lstatIfPresent2(path);
|
|
3466
2525
|
if (stat && !stat.isFile()) {
|
|
3467
2526
|
throw new Error(`refusing to remove non-file ${label} at ${path}`);
|
|
3468
2527
|
}
|
|
3469
2528
|
}
|
|
3470
|
-
function
|
|
2529
|
+
function lstatIfPresent2(path) {
|
|
3471
2530
|
try {
|
|
3472
|
-
return
|
|
2531
|
+
return lstatSync2(path);
|
|
3473
2532
|
} catch (error) {
|
|
3474
2533
|
const code = error.code;
|
|
3475
2534
|
if (code === "ENOENT" || code === "ENOTDIR") return void 0;
|
|
@@ -3477,9 +2536,9 @@ function lstatIfPresent3(path) {
|
|
|
3477
2536
|
}
|
|
3478
2537
|
}
|
|
3479
2538
|
function assertExactFile(path, expected, label) {
|
|
3480
|
-
if (!
|
|
2539
|
+
if (!lstatIfPresent2(path)) return;
|
|
3481
2540
|
assertRegularFile(path, label);
|
|
3482
|
-
if (
|
|
2541
|
+
if (readFileSync3(path, "utf8") !== expected) {
|
|
3483
2542
|
throw new Error(`refusing to remove unrecognized ${label} at ${path}`);
|
|
3484
2543
|
}
|
|
3485
2544
|
}
|
|
@@ -3488,18 +2547,18 @@ function assertOwnedDaemonRuntime(options) {
|
|
|
3488
2547
|
const control = daemonControlPaths(options);
|
|
3489
2548
|
const configDir = primConfigDirectory(options);
|
|
3490
2549
|
const service = process.platform === "darwin" ? launchdPaths(options) : void 0;
|
|
3491
|
-
const logPath = service?.logPath ??
|
|
2550
|
+
const logPath = service?.logPath ?? join4(configDir, "daemon.log");
|
|
3492
2551
|
for (const activeArtifact of [
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
2552
|
+
join4(configDir, "daemon.pid"),
|
|
2553
|
+
join4(configDir, "sock"),
|
|
2554
|
+
join4(configDir, "daemon.lock"),
|
|
2555
|
+
join4(configDir, "client-instance.lock")
|
|
3497
2556
|
]) {
|
|
3498
|
-
if (
|
|
2557
|
+
if (lstatIfPresent2(activeArtifact)) {
|
|
3499
2558
|
throw new Error(`refusing runtime removal while daemon state remains at ${activeArtifact}`);
|
|
3500
2559
|
}
|
|
3501
2560
|
}
|
|
3502
|
-
if (
|
|
2561
|
+
if (lstatIfPresent2(control.launcher)) {
|
|
3503
2562
|
assertRegularFile(control.launcher, "daemon launcher");
|
|
3504
2563
|
if (!readDaemonLauncher(control.launcher)) {
|
|
3505
2564
|
throw new Error(`refusing to remove unrecognized daemon launcher at ${control.launcher}`);
|
|
@@ -3514,42 +2573,42 @@ function assertOwnedDaemonRuntime(options) {
|
|
|
3514
2573
|
);
|
|
3515
2574
|
}
|
|
3516
2575
|
assertRegularFile(logPath, "daemon log");
|
|
3517
|
-
assertRegularFile(
|
|
3518
|
-
assertRegularFile(
|
|
3519
|
-
if (service &&
|
|
2576
|
+
assertRegularFile(join4(configDir, "daemon-health.json"), "daemon health snapshot");
|
|
2577
|
+
assertRegularFile(join4(configDir, "client_instance_id"), "daemon client instance id");
|
|
2578
|
+
if (service && lstatIfPresent2(service.plistPath)) {
|
|
3520
2579
|
assertRegularFile(service.plistPath, "launchd property list");
|
|
3521
2580
|
const expected = generateLaunchAgentPlist({
|
|
3522
2581
|
launcherPath: control.launcher,
|
|
3523
2582
|
logPath,
|
|
3524
|
-
workingDirectory: options.homeDir ??
|
|
2583
|
+
workingDirectory: options.homeDir ?? homedir3(),
|
|
3525
2584
|
label: options.label
|
|
3526
2585
|
});
|
|
3527
|
-
if (
|
|
2586
|
+
if (readFileSync3(service.plistPath, "utf8") !== expected) {
|
|
3528
2587
|
throw new Error(
|
|
3529
2588
|
`refusing to remove unrecognized launchd property list at ${service.plistPath}`
|
|
3530
2589
|
);
|
|
3531
2590
|
}
|
|
3532
2591
|
}
|
|
3533
|
-
const runtimeDirectory =
|
|
2592
|
+
const runtimeDirectory = lstatIfPresent2(runtime.runtimeDir);
|
|
3534
2593
|
if (!runtimeDirectory) return;
|
|
3535
2594
|
if (!runtimeDirectory.isDirectory()) {
|
|
3536
2595
|
throw new Error(`refusing to remove non-directory daemon runtime at ${runtime.runtimeDir}`);
|
|
3537
2596
|
}
|
|
3538
2597
|
const allowed = /* @__PURE__ */ new Set(["current", "prim-statusline", "releases"]);
|
|
3539
|
-
const unexpected =
|
|
2598
|
+
const unexpected = readdirSync2(runtime.runtimeDir).filter((entry) => !allowed.has(entry));
|
|
3540
2599
|
if (unexpected.length > 0) {
|
|
3541
2600
|
throw new Error(
|
|
3542
2601
|
`refusing to remove daemon runtime with unrecognized entries: ${unexpected.sort().join(", ")}`
|
|
3543
2602
|
);
|
|
3544
2603
|
}
|
|
3545
2604
|
const manifests = /* @__PURE__ */ new Map();
|
|
3546
|
-
const releasesDirectory =
|
|
2605
|
+
const releasesDirectory = lstatIfPresent2(runtime.releasesDir);
|
|
3547
2606
|
if (releasesDirectory) {
|
|
3548
2607
|
if (!releasesDirectory.isDirectory()) {
|
|
3549
2608
|
throw new Error(`refusing to remove non-directory daemon releases at ${runtime.releasesDir}`);
|
|
3550
2609
|
}
|
|
3551
|
-
for (const entry of
|
|
3552
|
-
const releaseDir =
|
|
2610
|
+
for (const entry of readdirSync2(runtime.releasesDir, { withFileTypes: true })) {
|
|
2611
|
+
const releaseDir = join4(runtime.releasesDir, entry.name);
|
|
3553
2612
|
const manifest = entry.isDirectory() && RUNTIME_RELEASE_RE.test(entry.name) ? readOwnedRuntimeManifest(releaseDir) : null;
|
|
3554
2613
|
if (!manifest) {
|
|
3555
2614
|
throw new Error(`refusing to remove unrecognized daemon release ${releaseDir}`);
|
|
@@ -3558,7 +2617,7 @@ function assertOwnedDaemonRuntime(options) {
|
|
|
3558
2617
|
}
|
|
3559
2618
|
}
|
|
3560
2619
|
let selected;
|
|
3561
|
-
const currentLink =
|
|
2620
|
+
const currentLink = lstatIfPresent2(runtime.currentLink);
|
|
3562
2621
|
if (currentLink) {
|
|
3563
2622
|
if (!currentLink.isSymbolicLink()) {
|
|
3564
2623
|
throw new Error(
|
|
@@ -3567,7 +2626,7 @@ function assertOwnedDaemonRuntime(options) {
|
|
|
3567
2626
|
}
|
|
3568
2627
|
const target = readlinkSync(runtime.currentLink);
|
|
3569
2628
|
const releaseName = basename(target);
|
|
3570
|
-
if (target !==
|
|
2629
|
+
if (target !== join4("releases", releaseName) || !RUNTIME_RELEASE_RE.test(releaseName)) {
|
|
3571
2630
|
throw new Error(
|
|
3572
2631
|
`refusing to remove malformed daemon runtime selector ${runtime.currentLink}`
|
|
3573
2632
|
);
|
|
@@ -3577,7 +2636,7 @@ function assertOwnedDaemonRuntime(options) {
|
|
|
3577
2636
|
throw new Error(`refusing to remove missing selected daemon release ${releaseName}`);
|
|
3578
2637
|
}
|
|
3579
2638
|
}
|
|
3580
|
-
if (
|
|
2639
|
+
if (lstatIfPresent2(runtime.statuslineLauncher)) {
|
|
3581
2640
|
if (!selected) {
|
|
3582
2641
|
throw new Error(
|
|
3583
2642
|
`refusing to remove daemon statusline without a selected runtime at ${runtime.statuslineLauncher}`
|
|
@@ -3595,7 +2654,7 @@ async function removeDaemonRuntime(options = {}) {
|
|
|
3595
2654
|
const control = daemonControlPaths(options);
|
|
3596
2655
|
const configDir = primConfigDirectory(options);
|
|
3597
2656
|
const service = process.platform === "darwin" ? launchdPaths(options) : void 0;
|
|
3598
|
-
const logPath = service?.logPath ??
|
|
2657
|
+
const logPath = service?.logPath ?? join4(configDir, "daemon.log");
|
|
3599
2658
|
const ownedArtifacts = [
|
|
3600
2659
|
runtime.runtimeDir,
|
|
3601
2660
|
control.launcher,
|
|
@@ -3603,21 +2662,21 @@ async function removeDaemonRuntime(options = {}) {
|
|
|
3603
2662
|
control.legacyDisabledMarker,
|
|
3604
2663
|
...service ? [service.plistPath] : [],
|
|
3605
2664
|
logPath,
|
|
3606
|
-
|
|
3607
|
-
|
|
2665
|
+
join4(configDir, "daemon-health.json"),
|
|
2666
|
+
join4(configDir, "client_instance_id")
|
|
3608
2667
|
];
|
|
3609
|
-
if (!ownedArtifacts.some((path) =>
|
|
2668
|
+
if (!ownedArtifacts.some((path) => lstatIfPresent2(path))) {
|
|
3610
2669
|
return { changed: false, runtimePaths: runtime };
|
|
3611
2670
|
}
|
|
3612
2671
|
return withDaemonLifecycleLock(async () => {
|
|
3613
2672
|
assertOwnedDaemonRuntime(options);
|
|
3614
2673
|
for (const path of ownedArtifacts.filter((candidate) => candidate !== runtime.runtimeDir)) {
|
|
3615
|
-
|
|
2674
|
+
rmSync(path, { force: true });
|
|
3616
2675
|
}
|
|
3617
|
-
if (
|
|
3618
|
-
const quarantined = `${runtime.runtimeDir}.uninstall-${String(process.pid)}-${
|
|
3619
|
-
|
|
3620
|
-
|
|
2676
|
+
if (lstatIfPresent2(runtime.runtimeDir)) {
|
|
2677
|
+
const quarantined = `${runtime.runtimeDir}.uninstall-${String(process.pid)}-${randomBytes3(8).toString("hex")}`;
|
|
2678
|
+
renameSync(runtime.runtimeDir, quarantined);
|
|
2679
|
+
rmSync(quarantined, { recursive: true, force: true });
|
|
3621
2680
|
}
|
|
3622
2681
|
return { changed: true, runtimePaths: runtime };
|
|
3623
2682
|
}, options);
|
|
@@ -3632,7 +2691,7 @@ function generateDaemonLauncher(config) {
|
|
|
3632
2691
|
apiUrl: apiUrl ?? null,
|
|
3633
2692
|
...config.configDir ? { configDir: config.configDir } : {}
|
|
3634
2693
|
};
|
|
3635
|
-
const revision =
|
|
2694
|
+
const revision = createHash2("sha256").update(JSON.stringify(metadata)).digest("hex");
|
|
3636
2695
|
const encoded = Buffer.from(JSON.stringify({ ...metadata, revision })).toString("base64url");
|
|
3637
2696
|
return {
|
|
3638
2697
|
revision,
|
|
@@ -3648,11 +2707,11 @@ ${config.configDir ? `export PRIM_CONFIG_DIR=${shellQuote(config.configDir)}
|
|
|
3648
2707
|
}
|
|
3649
2708
|
function readDaemonLauncher(path) {
|
|
3650
2709
|
try {
|
|
3651
|
-
const content =
|
|
2710
|
+
const content = readFileSync3(path, "utf8");
|
|
3652
2711
|
const encoded = /^# prim-daemon-launcher: ([A-Za-z0-9_-]+)$/mu.exec(content)?.[1];
|
|
3653
2712
|
if (!encoded) return null;
|
|
3654
2713
|
const value = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
3655
|
-
if (value.schemaVersion !== LAUNCHER_SCHEMA_VERSION || typeof value.nodePath !== "string" || typeof value.daemonPath !== "string" || typeof value.runtimeVersion !== "string" || value.apiUrl !== null && typeof value.apiUrl !== "string" || value.configDir !== void 0 && (typeof value.configDir !== "string" || !
|
|
2714
|
+
if (value.schemaVersion !== LAUNCHER_SCHEMA_VERSION || typeof value.nodePath !== "string" || typeof value.daemonPath !== "string" || typeof value.runtimeVersion !== "string" || value.apiUrl !== null && typeof value.apiUrl !== "string" || value.configDir !== void 0 && (typeof value.configDir !== "string" || !isAbsolute(value.configDir)) || typeof value.revision !== "string") {
|
|
3656
2715
|
return null;
|
|
3657
2716
|
}
|
|
3658
2717
|
const config = {
|
|
@@ -3671,9 +2730,9 @@ function readDaemonLauncher(path) {
|
|
|
3671
2730
|
function readUsableDaemonConfig(path) {
|
|
3672
2731
|
const config = readDaemonLauncher(path);
|
|
3673
2732
|
if (!config) return null;
|
|
3674
|
-
const manifest = readRuntimeManifest(
|
|
2733
|
+
const manifest = readRuntimeManifest(join4(dirname2(config.daemonPath), "manifest.json"));
|
|
3675
2734
|
try {
|
|
3676
|
-
|
|
2735
|
+
accessSync(config.nodePath, constants.X_OK);
|
|
3677
2736
|
return manifest?.version === config.runtimeVersion && manifest.nodePath === config.nodePath && manifest.daemonSha256 === sha256File(config.daemonPath) ? config : null;
|
|
3678
2737
|
} catch {
|
|
3679
2738
|
return null;
|
|
@@ -3716,7 +2775,7 @@ function generateLaunchAgentPlist(config) {
|
|
|
3716
2775
|
`;
|
|
3717
2776
|
}
|
|
3718
2777
|
function launchdPaths(options = {}) {
|
|
3719
|
-
const home = options.homeDir ??
|
|
2778
|
+
const home = options.homeDir ?? homedir3();
|
|
3720
2779
|
const configDir = primConfigDirectory(options);
|
|
3721
2780
|
const uid = options.uid ?? process.getuid?.();
|
|
3722
2781
|
const label = options.label ?? LAUNCHD_LABEL;
|
|
@@ -3724,8 +2783,8 @@ function launchdPaths(options = {}) {
|
|
|
3724
2783
|
return {
|
|
3725
2784
|
domainTarget: `gui/${uid}`,
|
|
3726
2785
|
serviceTarget: `gui/${uid}/${label}`,
|
|
3727
|
-
plistPath:
|
|
3728
|
-
logPath:
|
|
2786
|
+
plistPath: join4(home, "Library", "LaunchAgents", `${label}.plist`),
|
|
2787
|
+
logPath: join4(configDir, "daemon.log")
|
|
3729
2788
|
};
|
|
3730
2789
|
}
|
|
3731
2790
|
var runLaunchctl = (args, timeoutMs2 = COMMAND_TIMEOUT_MS) => {
|
|
@@ -3758,21 +2817,21 @@ function getLaunchdService(options = {}) {
|
|
|
3758
2817
|
}
|
|
3759
2818
|
function atomicWrite2(path, content, mode, validate) {
|
|
3760
2819
|
try {
|
|
3761
|
-
if (
|
|
3762
|
-
|
|
2820
|
+
if (readFileSync3(path, "utf8") === content) {
|
|
2821
|
+
chmodSync(path, mode);
|
|
3763
2822
|
validate?.(path, COMMAND_TIMEOUT_MS);
|
|
3764
2823
|
return false;
|
|
3765
2824
|
}
|
|
3766
2825
|
} catch {
|
|
3767
2826
|
}
|
|
3768
|
-
|
|
2827
|
+
mkdirSync2(dirname2(path), { recursive: true, mode: RUNTIME_DIR_MODE });
|
|
3769
2828
|
const temp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
3770
|
-
|
|
2829
|
+
writeFileSync2(temp, content, { encoding: "utf8", mode, flag: "wx" });
|
|
3771
2830
|
try {
|
|
3772
2831
|
validate?.(temp, COMMAND_TIMEOUT_MS);
|
|
3773
|
-
|
|
2832
|
+
renameSync(temp, path);
|
|
3774
2833
|
} catch (error) {
|
|
3775
|
-
|
|
2834
|
+
rmSync(temp, { force: true });
|
|
3776
2835
|
throw error;
|
|
3777
2836
|
}
|
|
3778
2837
|
return true;
|
|
@@ -3799,17 +2858,17 @@ async function inspectDaemon(timeoutMs2) {
|
|
|
3799
2858
|
}
|
|
3800
2859
|
function canonicalProspectivePath(path) {
|
|
3801
2860
|
const suffix = [];
|
|
3802
|
-
let ancestor =
|
|
3803
|
-
while (!
|
|
3804
|
-
const parent =
|
|
3805
|
-
if (parent === ancestor) return
|
|
2861
|
+
let ancestor = resolve2(path);
|
|
2862
|
+
while (!existsSync3(ancestor)) {
|
|
2863
|
+
const parent = dirname2(ancestor);
|
|
2864
|
+
if (parent === ancestor) return resolve2(path);
|
|
3806
2865
|
suffix.unshift(basename(ancestor));
|
|
3807
2866
|
ancestor = parent;
|
|
3808
2867
|
}
|
|
3809
2868
|
try {
|
|
3810
|
-
return
|
|
2869
|
+
return join4(realpathSync(ancestor), ...suffix);
|
|
3811
2870
|
} catch {
|
|
3812
|
-
return
|
|
2871
|
+
return resolve2(path);
|
|
3813
2872
|
}
|
|
3814
2873
|
}
|
|
3815
2874
|
async function withDaemonLifecycleLock(operation, options = {}) {
|
|
@@ -3822,10 +2881,10 @@ async function withDaemonLifecycleLock(operation, options = {}) {
|
|
|
3822
2881
|
);
|
|
3823
2882
|
}
|
|
3824
2883
|
async function stopVerifiedLegacyDaemon(options, deadlineMs, nowMs, sleep2) {
|
|
3825
|
-
const pidPath =
|
|
2884
|
+
const pidPath = join4(primConfigDirectory(options), "daemon.pid");
|
|
3826
2885
|
let pid;
|
|
3827
2886
|
try {
|
|
3828
|
-
pid = Number(
|
|
2887
|
+
pid = Number(readFileSync3(pidPath, "utf8").trim());
|
|
3829
2888
|
} catch {
|
|
3830
2889
|
throw new Error("an unsupervised daemon answered, but it has no verifiable pidfile");
|
|
3831
2890
|
}
|
|
@@ -4002,13 +3061,13 @@ async function waitForAbsence(context) {
|
|
|
4002
3061
|
}
|
|
4003
3062
|
function daemonExplicitlyDisabled(options = {}) {
|
|
4004
3063
|
const paths = daemonControlPaths(options);
|
|
4005
|
-
return
|
|
3064
|
+
return existsSync3(paths.disabledMarker) || existsSync3(paths.legacyDisabledMarker);
|
|
4006
3065
|
}
|
|
4007
3066
|
function setDaemonExplicitlyDisabled(disabled, options = {}) {
|
|
4008
3067
|
const paths = daemonControlPaths(options);
|
|
4009
3068
|
if (!disabled) {
|
|
4010
|
-
|
|
4011
|
-
|
|
3069
|
+
rmSync(paths.disabledMarker, { force: true });
|
|
3070
|
+
rmSync(paths.legacyDisabledMarker, { force: true });
|
|
4012
3071
|
return;
|
|
4013
3072
|
}
|
|
4014
3073
|
const content = DAEMON_DISABLED_CONTENT;
|
|
@@ -4025,7 +3084,7 @@ async function ensureMacDaemonLocked(options) {
|
|
|
4025
3084
|
let service = getLaunchdService({ ...options, runner });
|
|
4026
3085
|
if (!options.explicitlyStarted && daemonExplicitlyDisabled(options)) {
|
|
4027
3086
|
const control2 = daemonControlPaths(options);
|
|
4028
|
-
if (!
|
|
3087
|
+
if (!existsSync3(control2.disabledMarker)) {
|
|
4029
3088
|
atomicWrite2(control2.disabledMarker, "disabled by `prim daemon stop`\n", RUNTIME_FILE_MODE);
|
|
4030
3089
|
}
|
|
4031
3090
|
return {
|
|
@@ -4052,7 +3111,7 @@ async function ensureMacDaemonLocked(options) {
|
|
|
4052
3111
|
const configuredApiUrl = (options.env ?? process.env).PRIM_API_URL;
|
|
4053
3112
|
const apiUrl = configuredApiUrl ? normalizeApiUrl(configuredApiUrl) || void 0 : void 0;
|
|
4054
3113
|
const configDir = primConfigDirectory(options);
|
|
4055
|
-
const legacyDefaultConfigDir =
|
|
3114
|
+
const legacyDefaultConfigDir = join4(options.homeDir ?? homedir3(), ".config", "prim");
|
|
4056
3115
|
const launcherConfigDir = configDir === legacyDefaultConfigDir ? void 0 : configDir;
|
|
4057
3116
|
if (retainSelected && selected) {
|
|
4058
3117
|
const usable = readUsableDaemonConfig(control.launcher);
|
|
@@ -4079,15 +3138,15 @@ async function ensureMacDaemonLocked(options) {
|
|
|
4079
3138
|
};
|
|
4080
3139
|
}
|
|
4081
3140
|
const servicePaths = launchdPaths(options);
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
3141
|
+
mkdirSync2(dirname2(servicePaths.logPath), { recursive: true, mode: RUNTIME_DIR_MODE });
|
|
3142
|
+
writeFileSync2(servicePaths.logPath, "", { mode: RUNTIME_FILE_MODE, flag: "a" });
|
|
3143
|
+
chmodSync(servicePaths.logPath, RUNTIME_FILE_MODE);
|
|
4085
3144
|
const launcher = generateDaemonLauncher(daemonConfig);
|
|
4086
3145
|
atomicWrite2(control.launcher, launcher.content, RUNTIME_LAUNCHER_MODE);
|
|
4087
3146
|
const plist = generateLaunchAgentPlist({
|
|
4088
3147
|
launcherPath: control.launcher,
|
|
4089
3148
|
logPath: servicePaths.logPath,
|
|
4090
|
-
workingDirectory: options.homeDir ??
|
|
3149
|
+
workingDirectory: options.homeDir ?? homedir3(),
|
|
4091
3150
|
label: options.label
|
|
4092
3151
|
});
|
|
4093
3152
|
const plistChanged = atomicWrite2(
|
|
@@ -4159,7 +3218,7 @@ async function bootoutMacDaemonLocked(options) {
|
|
|
4159
3218
|
const wasLoaded = service.loaded;
|
|
4160
3219
|
if (wasLoaded) {
|
|
4161
3220
|
const control = daemonControlPaths(options);
|
|
4162
|
-
const launcher =
|
|
3221
|
+
const launcher = lstatIfPresent2(control.launcher);
|
|
4163
3222
|
if (service.program !== control.launcher || !launcher?.isFile() || readDaemonLauncher(control.launcher) === null) {
|
|
4164
3223
|
throw new Error(
|
|
4165
3224
|
`refusing to stop unrecognized launchd program ${service.program ?? "(missing)"}`
|
|
@@ -4207,9 +3266,9 @@ async function bootoutMacDaemonLocked(options) {
|
|
|
4207
3266
|
// src/commands/daemon.ts
|
|
4208
3267
|
var DAEMON_BIN = "prim-daemon-server";
|
|
4209
3268
|
var CONFIG_DIR = primConfigDirectory();
|
|
4210
|
-
var PID_PATH =
|
|
4211
|
-
var SOCK_PATH =
|
|
4212
|
-
var LOG_PATH =
|
|
3269
|
+
var PID_PATH = join5(CONFIG_DIR, "daemon.pid");
|
|
3270
|
+
var SOCK_PATH = join5(CONFIG_DIR, "sock");
|
|
3271
|
+
var LOG_PATH = join5(CONFIG_DIR, "daemon.log");
|
|
4213
3272
|
var CONFIG_DIR_MODE = 448;
|
|
4214
3273
|
var LOG_FILE_MODE = 384;
|
|
4215
3274
|
var STOP_TIMEOUT_MS = 5e3;
|
|
@@ -4247,10 +3306,10 @@ function daemonDegradedReason(snapshot) {
|
|
|
4247
3306
|
return "health checks have not recovered";
|
|
4248
3307
|
}
|
|
4249
3308
|
function readPidfile() {
|
|
4250
|
-
if (!
|
|
3309
|
+
if (!existsSync4(PID_PATH)) {
|
|
4251
3310
|
return null;
|
|
4252
3311
|
}
|
|
4253
|
-
const raw =
|
|
3312
|
+
const raw = readFileSync4(PID_PATH, "utf-8").trim();
|
|
4254
3313
|
const pid = Number(raw);
|
|
4255
3314
|
if (!Number.isInteger(pid) || pid <= 0) {
|
|
4256
3315
|
return null;
|
|
@@ -4268,17 +3327,17 @@ function clearStaleArtifacts() {
|
|
|
4268
3327
|
}
|
|
4269
3328
|
}
|
|
4270
3329
|
function sleep(ms) {
|
|
4271
|
-
return new Promise((
|
|
3330
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
4272
3331
|
}
|
|
4273
3332
|
function spawnDaemon(options) {
|
|
4274
3333
|
const file = binFile(DAEMON_BIN);
|
|
4275
3334
|
return file ? spawn2(process.execPath, [file], options) : spawn2(DAEMON_BIN, [], options);
|
|
4276
3335
|
}
|
|
4277
3336
|
function openDaemonLog(configDir = CONFIG_DIR) {
|
|
4278
|
-
if (!
|
|
4279
|
-
|
|
3337
|
+
if (!existsSync4(configDir)) {
|
|
3338
|
+
mkdirSync3(configDir, { recursive: true, mode: CONFIG_DIR_MODE });
|
|
4280
3339
|
}
|
|
4281
|
-
return openSync(
|
|
3340
|
+
return openSync(join5(configDir, "daemon.log"), "a", LOG_FILE_MODE);
|
|
4282
3341
|
}
|
|
4283
3342
|
async function waitForReady() {
|
|
4284
3343
|
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
@@ -4412,7 +3471,7 @@ async function detachedDaemonStop() {
|
|
|
4412
3471
|
{},
|
|
4413
3472
|
{ timeoutMs: STATUS_PROBE_TIMEOUT_MS }
|
|
4414
3473
|
);
|
|
4415
|
-
if (
|
|
3474
|
+
if (existsSync4(PID_PATH) || existsSync4(SOCK_PATH) || socketOwner) {
|
|
4416
3475
|
process.stderr.write(
|
|
4417
3476
|
`[prim] refusing to verify daemon absence: ${socketOwner ? `socket is owned by pid=${socketOwner.pid}` : "daemon artifacts remain"}
|
|
4418
3477
|
`
|
|
@@ -4444,7 +3503,7 @@ async function detachedDaemonStop() {
|
|
|
4444
3503
|
{},
|
|
4445
3504
|
{ timeoutMs: STATUS_PROBE_TIMEOUT_MS }
|
|
4446
3505
|
);
|
|
4447
|
-
if (socketOwner ||
|
|
3506
|
+
if (socketOwner || existsSync4(SOCK_PATH)) {
|
|
4448
3507
|
process.stderr.write(
|
|
4449
3508
|
`[prim] refusing to clear stale pidfile: ${socketOwner ? `socket is owned by pid=${socketOwner.pid}` : "socket artifact remains"}
|
|
4450
3509
|
`
|
|
@@ -5212,6 +4271,8 @@ function authorLabel(row) {
|
|
|
5212
4271
|
return "Your Claude Code";
|
|
5213
4272
|
case "codex":
|
|
5214
4273
|
return "Your Codex";
|
|
4274
|
+
case "cursor":
|
|
4275
|
+
return "Your Cursor";
|
|
5215
4276
|
case "hermes":
|
|
5216
4277
|
return "Your Hermes";
|
|
5217
4278
|
case "chat":
|
|
@@ -6644,6 +5705,7 @@ function describeAudienceMember(member) {
|
|
|
6644
5705
|
return {
|
|
6645
5706
|
claude_code: "Claude Code agent",
|
|
6646
5707
|
codex: "Codex agent",
|
|
5708
|
+
cursor: "Cursor agent",
|
|
6647
5709
|
hermes: "Hermes agent"
|
|
6648
5710
|
}[member.agent] ?? terminalSafeLine(member.agent);
|
|
6649
5711
|
case "credential":
|
|
@@ -6767,7 +5829,7 @@ var UserScopeInputError = class extends Error {
|
|
|
6767
5829
|
}
|
|
6768
5830
|
};
|
|
6769
5831
|
var USER_SCOPE_ROLES = /* @__PURE__ */ new Set(["owner", "admin", "member"]);
|
|
6770
|
-
var USER_SCOPE_AGENTS = /* @__PURE__ */ new Set(["claude_code", "codex", "hermes"]);
|
|
5832
|
+
var USER_SCOPE_AGENTS = /* @__PURE__ */ new Set(["claude_code", "codex", "cursor", "hermes"]);
|
|
6771
5833
|
var USER_SCOPE_CREDENTIALS = /* @__PURE__ */ new Set(["workos_jwt", "workos_api_key", "service_token"]);
|
|
6772
5834
|
function requiredScopeValue(flag, value) {
|
|
6773
5835
|
const trimmed = value.trim();
|
|
@@ -6795,9 +5857,12 @@ function userScopeFromOptions(opts) {
|
|
|
6795
5857
|
for (const value of agents) {
|
|
6796
5858
|
const agent = requiredScopeValue("--for-agent", value);
|
|
6797
5859
|
if (!USER_SCOPE_AGENTS.has(agent)) {
|
|
6798
|
-
throw new UserScopeInputError("--for-agent must be claude_code, codex, or hermes");
|
|
5860
|
+
throw new UserScopeInputError("--for-agent must be claude_code, codex, cursor, or hermes");
|
|
6799
5861
|
}
|
|
6800
|
-
members.push({
|
|
5862
|
+
members.push({
|
|
5863
|
+
kind: "agent",
|
|
5864
|
+
agent
|
|
5865
|
+
});
|
|
6801
5866
|
}
|
|
6802
5867
|
for (const value of credentials) {
|
|
6803
5868
|
const credential = requiredScopeValue("--for-credential", value);
|
|
@@ -7004,7 +6069,7 @@ function registerDecisionsCommands(program2) {
|
|
|
7004
6069
|
[]
|
|
7005
6070
|
).option(
|
|
7006
6071
|
"--for-agent <agent>",
|
|
7007
|
-
"Include one agent: claude_code | codex | hermes (repeatable)",
|
|
6072
|
+
"Include one agent: claude_code | codex | cursor | hermes (repeatable)",
|
|
7008
6073
|
collectItem,
|
|
7009
6074
|
[]
|
|
7010
6075
|
).option(
|
|
@@ -7160,7 +6225,7 @@ function registerDecisionsCommands(program2) {
|
|
|
7160
6225
|
[]
|
|
7161
6226
|
).option(
|
|
7162
6227
|
"--for-agent <agent>",
|
|
7163
|
-
"Set one agent: claude_code | codex | hermes (repeatable)",
|
|
6228
|
+
"Set one agent: claude_code | codex | cursor | hermes (repeatable)",
|
|
7164
6229
|
collectItem,
|
|
7165
6230
|
[]
|
|
7166
6231
|
).option(
|
|
@@ -7240,7 +6305,7 @@ function registerDecisionsCommands(program2) {
|
|
|
7240
6305
|
}
|
|
7241
6306
|
|
|
7242
6307
|
// src/commands/doctor.ts
|
|
7243
|
-
import { existsSync as
|
|
6308
|
+
import { existsSync as existsSync6 } from "fs";
|
|
7244
6309
|
|
|
7245
6310
|
// src/protocol/decision-lifecycle.ts
|
|
7246
6311
|
var DECISION_LIFECYCLE_PROTOCOL_VERSION = 2;
|
|
@@ -7358,28 +6423,28 @@ async function inspectJournalDelivery(buckets, options = {}) {
|
|
|
7358
6423
|
}
|
|
7359
6424
|
|
|
7360
6425
|
// src/commands/hermes-install.ts
|
|
7361
|
-
import { existsSync as
|
|
7362
|
-
import { homedir as
|
|
7363
|
-
import { join as
|
|
6426
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, rmSync as rmSync2, statSync } from "fs";
|
|
6427
|
+
import { homedir as homedir4 } from "os";
|
|
6428
|
+
import { join as join6 } from "path";
|
|
7364
6429
|
import { Document, isMap, isSeq, parseAllDocuments, stringify, visit } from "yaml";
|
|
7365
|
-
var
|
|
7366
|
-
var
|
|
7367
|
-
var
|
|
7368
|
-
var
|
|
7369
|
-
var
|
|
6430
|
+
var CAPTURE_BIN2 = "prim-hook";
|
|
6431
|
+
var GATE_BIN2 = "prim-pre-tool-use";
|
|
6432
|
+
var POST_TOOL_USE_BIN2 = "prim-post-tool-use";
|
|
6433
|
+
var SESSION_START_BIN2 = "prim-session-start";
|
|
6434
|
+
var SESSION_END_BIN = "prim-session-end";
|
|
7370
6435
|
var HERMES_ARGS = "--agent hermes";
|
|
7371
6436
|
var EDIT_MATCHER = "write_file|patch";
|
|
7372
|
-
var
|
|
6437
|
+
var JSON_INDENT2 = 2;
|
|
7373
6438
|
var CONFIG_MODE = 384;
|
|
7374
|
-
var
|
|
6439
|
+
var PRIM_BINS2 = [CAPTURE_BIN2, GATE_BIN2, POST_TOOL_USE_BIN2, SESSION_START_BIN2, SESSION_END_BIN];
|
|
7375
6440
|
function hermesHome() {
|
|
7376
|
-
return process.env.HERMES_HOME ??
|
|
6441
|
+
return process.env.HERMES_HOME ?? join6(homedir4(), ".hermes");
|
|
7377
6442
|
}
|
|
7378
6443
|
function configPath() {
|
|
7379
|
-
return
|
|
6444
|
+
return join6(hermesHome(), "config.yaml");
|
|
7380
6445
|
}
|
|
7381
6446
|
function legacyShimPath() {
|
|
7382
|
-
return
|
|
6447
|
+
return join6(hermesHome(), "agent-hooks", "prim-shim.sh");
|
|
7383
6448
|
}
|
|
7384
6449
|
function commandFor(bin) {
|
|
7385
6450
|
return stableHookCommand(bin, HERMES_ARGS);
|
|
@@ -7390,7 +6455,7 @@ function legacyCommandFor(bin) {
|
|
|
7390
6455
|
function commandUsesBin(command, bin) {
|
|
7391
6456
|
return command === commandFor(bin) || command === legacyCommandFor(bin);
|
|
7392
6457
|
}
|
|
7393
|
-
var
|
|
6458
|
+
var CAPTURE_EVENTS = [
|
|
7394
6459
|
"on_session_start",
|
|
7395
6460
|
"pre_llm_call",
|
|
7396
6461
|
"pre_tool_call",
|
|
@@ -7400,13 +6465,13 @@ var CAPTURE_EVENTS2 = [
|
|
|
7400
6465
|
"subagent_stop"
|
|
7401
6466
|
];
|
|
7402
6467
|
var GATE_TIMEOUT_SECONDS = 10;
|
|
7403
|
-
var
|
|
7404
|
-
...
|
|
7405
|
-
{ event: "pre_tool_call", matcher: EDIT_MATCHER, bin:
|
|
7406
|
-
{ event: "post_tool_call", matcher: EDIT_MATCHER, bin:
|
|
7407
|
-
{ event: "post_approval_response", bin:
|
|
7408
|
-
{ event: "on_session_start", bin:
|
|
7409
|
-
{ event: "on_session_end", bin:
|
|
6468
|
+
var REGISTRATIONS = [
|
|
6469
|
+
...CAPTURE_EVENTS.map((event) => ({ event, bin: CAPTURE_BIN2 })),
|
|
6470
|
+
{ event: "pre_tool_call", matcher: EDIT_MATCHER, bin: GATE_BIN2, timeout: GATE_TIMEOUT_SECONDS },
|
|
6471
|
+
{ event: "post_tool_call", matcher: EDIT_MATCHER, bin: POST_TOOL_USE_BIN2 },
|
|
6472
|
+
{ event: "post_approval_response", bin: POST_TOOL_USE_BIN2 },
|
|
6473
|
+
{ event: "on_session_start", bin: SESSION_START_BIN2 },
|
|
6474
|
+
{ event: "on_session_end", bin: SESSION_END_BIN }
|
|
7410
6475
|
];
|
|
7411
6476
|
function yamlValue(node) {
|
|
7412
6477
|
return typeof node?.toJSON === "function" ? node.toJSON?.() : node;
|
|
@@ -7436,18 +6501,18 @@ function ensureReg(list, reg, force) {
|
|
|
7436
6501
|
}
|
|
7437
6502
|
return [...stripBin(list, reg.bin), entry];
|
|
7438
6503
|
}
|
|
7439
|
-
function
|
|
6504
|
+
function applyInstall2(hooks, force) {
|
|
7440
6505
|
const next = { ...hooks };
|
|
7441
|
-
for (const reg of
|
|
6506
|
+
for (const reg of REGISTRATIONS) {
|
|
7442
6507
|
next[reg.event] = ensureReg(next[reg.event] ?? [], reg, force);
|
|
7443
6508
|
}
|
|
7444
6509
|
return next;
|
|
7445
6510
|
}
|
|
7446
|
-
function
|
|
6511
|
+
function applyUninstall2(hooks) {
|
|
7447
6512
|
const next = {};
|
|
7448
6513
|
for (const [event, list] of Object.entries(hooks)) {
|
|
7449
6514
|
let kept = list;
|
|
7450
|
-
for (const bin of
|
|
6515
|
+
for (const bin of PRIM_BINS2) {
|
|
7451
6516
|
kept = stripBin(kept, bin);
|
|
7452
6517
|
}
|
|
7453
6518
|
if (kept.length > 0) {
|
|
@@ -7456,54 +6521,54 @@ function applyUninstall3(hooks) {
|
|
|
7456
6521
|
}
|
|
7457
6522
|
return next;
|
|
7458
6523
|
}
|
|
7459
|
-
function
|
|
7460
|
-
return (hooks.pre_tool_call ?? []).some((e) => commandUsesBin(e.command,
|
|
6524
|
+
function isGateInstalled2(hooks) {
|
|
6525
|
+
return (hooks.pre_tool_call ?? []).some((e) => commandUsesBin(e.command, GATE_BIN2));
|
|
7461
6526
|
}
|
|
7462
6527
|
function isCaptureInstalled(hooks) {
|
|
7463
|
-
return
|
|
7464
|
-
(event) => (hooks[event] ?? []).some((e) => commandUsesBin(e.command,
|
|
6528
|
+
return CAPTURE_EVENTS.some(
|
|
6529
|
+
(event) => (hooks[event] ?? []).some((e) => commandUsesBin(e.command, CAPTURE_BIN2))
|
|
7465
6530
|
);
|
|
7466
6531
|
}
|
|
7467
6532
|
function entryUsesOwnedBin(entry) {
|
|
7468
|
-
return
|
|
6533
|
+
return PRIM_BINS2.some(
|
|
7469
6534
|
(bin) => commandUsesBin(entry.command, bin) || commandMatchesBin(entry.command, bin)
|
|
7470
6535
|
);
|
|
7471
6536
|
}
|
|
7472
|
-
function
|
|
6537
|
+
function ownedRegistrationEntries2(hooks) {
|
|
7473
6538
|
return Object.entries(hooks).flatMap(
|
|
7474
6539
|
([event, entries]) => entries.filter(entryUsesOwnedBin).map((entry) => ({ event, entry }))
|
|
7475
6540
|
);
|
|
7476
6541
|
}
|
|
7477
|
-
function
|
|
7478
|
-
return
|
|
6542
|
+
function hasAnyHookRegistration2(hooks) {
|
|
6543
|
+
return ownedRegistrationEntries2(hooks).length > 0;
|
|
7479
6544
|
}
|
|
7480
|
-
function
|
|
6545
|
+
function hookRuntimeResolutions2(hooks) {
|
|
7481
6546
|
const resolutions = hookCommandResolutions(
|
|
7482
6547
|
Object.values(hooks).flatMap((entries) => entries.map((entry) => entry.command)),
|
|
7483
|
-
|
|
6548
|
+
PRIM_BINS2
|
|
7484
6549
|
);
|
|
7485
6550
|
for (const entries of Object.values(hooks)) {
|
|
7486
6551
|
for (const entry of entries) {
|
|
7487
|
-
for (const bin of
|
|
6552
|
+
for (const bin of PRIM_BINS2) {
|
|
7488
6553
|
if (entry.command === legacyCommandFor(bin)) resolutions.push({ kind: "legacy_path" });
|
|
7489
6554
|
}
|
|
7490
6555
|
}
|
|
7491
6556
|
}
|
|
7492
6557
|
return resolutions;
|
|
7493
6558
|
}
|
|
7494
|
-
function
|
|
6559
|
+
function inspectHookRuntimeResolutions4() {
|
|
7495
6560
|
const path = configPath();
|
|
7496
|
-
if (!
|
|
7497
|
-
return
|
|
6561
|
+
if (!existsSync5(path)) return [];
|
|
6562
|
+
return hookRuntimeResolutions2(readHooks(parseHermesDocument(readFileSync5(path, "utf8"))));
|
|
7498
6563
|
}
|
|
7499
|
-
function
|
|
6564
|
+
function entryMatchesRegistration2(owned, registration) {
|
|
7500
6565
|
const expected = entryFor(registration);
|
|
7501
6566
|
return owned.event === registration.event && owned.entry.command === expected.command && (owned.entry.matcher ?? "") === (expected.matcher ?? "") && (owned.entry.timeout ?? null) === (expected.timeout ?? null);
|
|
7502
6567
|
}
|
|
7503
|
-
function
|
|
7504
|
-
const owned =
|
|
7505
|
-
return owned.length ===
|
|
7506
|
-
(registration) => owned.filter((entry) =>
|
|
6568
|
+
function hasCompleteHookRegistration2(hooks) {
|
|
6569
|
+
const owned = ownedRegistrationEntries2(hooks);
|
|
6570
|
+
return owned.length === REGISTRATIONS.length && REGISTRATIONS.every(
|
|
6571
|
+
(registration) => owned.filter((entry) => entryMatchesRegistration2(entry, registration)).length === 1
|
|
7507
6572
|
);
|
|
7508
6573
|
}
|
|
7509
6574
|
var INVALID_CONFIG_MESSAGE = "Hermes config must be valid, single-document YAML with a top-level mapping";
|
|
@@ -7578,7 +6643,7 @@ function serializeHooks(hooks) {
|
|
|
7578
6643
|
return stringify({ hooks }, { lineWidth: 0 });
|
|
7579
6644
|
}
|
|
7580
6645
|
function isPrimEntry(entry) {
|
|
7581
|
-
return
|
|
6646
|
+
return PRIM_BINS2.some((bin) => commandUsesBin(entry.command, bin));
|
|
7582
6647
|
}
|
|
7583
6648
|
function entryKey(entry) {
|
|
7584
6649
|
return JSON.stringify(entry);
|
|
@@ -7763,11 +6828,11 @@ function assertMergeValid(before, after, expectedHooks) {
|
|
|
7763
6828
|
}
|
|
7764
6829
|
}
|
|
7765
6830
|
function readConfigSnapshot(path) {
|
|
7766
|
-
if (!
|
|
6831
|
+
if (!existsSync5(path)) return { exists: false, mode: CONFIG_MODE, raw: "" };
|
|
7767
6832
|
return {
|
|
7768
6833
|
exists: true,
|
|
7769
|
-
mode:
|
|
7770
|
-
raw:
|
|
6834
|
+
mode: statSync(path).mode & 511,
|
|
6835
|
+
raw: readFileSync5(path, "utf8")
|
|
7771
6836
|
};
|
|
7772
6837
|
}
|
|
7773
6838
|
var CONCURRENT_CONFIG_MESSAGE = "Hermes config changed during update; concurrent changes were preserved, retry the command";
|
|
@@ -7776,7 +6841,7 @@ function writeHermesConfigSnapshot(path, before, after, expectedHooks) {
|
|
|
7776
6841
|
ensureParent: true,
|
|
7777
6842
|
mode: before.mode,
|
|
7778
6843
|
validate(temporaryPath) {
|
|
7779
|
-
const flushed =
|
|
6844
|
+
const flushed = readFileSync5(temporaryPath, "utf8");
|
|
7780
6845
|
if (!mergePreservesHermesSemantics(before.raw, flushed, expectedHooks)) {
|
|
7781
6846
|
throw new Error(INVALID_MERGE_MESSAGE);
|
|
7782
6847
|
}
|
|
@@ -7788,7 +6853,7 @@ function writeHermesConfigSnapshot(path, before, after, expectedHooks) {
|
|
|
7788
6853
|
});
|
|
7789
6854
|
}
|
|
7790
6855
|
function removeLegacyShim() {
|
|
7791
|
-
|
|
6856
|
+
rmSync2(legacyShimPath(), { force: true });
|
|
7792
6857
|
}
|
|
7793
6858
|
function autoAcceptOf(raw) {
|
|
7794
6859
|
if (raw.trim().length === 0) {
|
|
@@ -7799,13 +6864,13 @@ function autoAcceptOf(raw) {
|
|
|
7799
6864
|
function readHooksFromRaw(raw) {
|
|
7800
6865
|
return readHooks(parseHermesDocument(raw));
|
|
7801
6866
|
}
|
|
7802
|
-
function
|
|
6867
|
+
function performInstall2(opts) {
|
|
7803
6868
|
(opts.stageRuntime ?? stageHookRuntime)();
|
|
7804
6869
|
const path = configPath();
|
|
7805
6870
|
const snapshot = readConfigSnapshot(path);
|
|
7806
6871
|
const raw = snapshot.raw;
|
|
7807
6872
|
const existing = readHooksFromRaw(raw);
|
|
7808
|
-
const desired =
|
|
6873
|
+
const desired = applyInstall2(existing, opts.force);
|
|
7809
6874
|
let next = raw;
|
|
7810
6875
|
if (JSON.stringify(existing) !== JSON.stringify(desired)) {
|
|
7811
6876
|
next = spliceHooks(next, desired);
|
|
@@ -7821,22 +6886,22 @@ function performInstall3(opts) {
|
|
|
7821
6886
|
removeLegacyShim();
|
|
7822
6887
|
return {
|
|
7823
6888
|
path,
|
|
7824
|
-
gate:
|
|
6889
|
+
gate: isGateInstalled2(desired),
|
|
7825
6890
|
capture: isCaptureInstalled(desired),
|
|
7826
6891
|
autoAccept: autoAcceptOf(next),
|
|
7827
6892
|
changed
|
|
7828
6893
|
};
|
|
7829
6894
|
}
|
|
7830
|
-
function
|
|
6895
|
+
function performUninstall2() {
|
|
7831
6896
|
const path = configPath();
|
|
7832
|
-
if (!
|
|
6897
|
+
if (!existsSync5(path)) {
|
|
7833
6898
|
removeLegacyShim();
|
|
7834
6899
|
return { path, gate: false, capture: false, autoAccept: false, changed: false };
|
|
7835
6900
|
}
|
|
7836
6901
|
const snapshot = readConfigSnapshot(path);
|
|
7837
6902
|
const raw = snapshot.raw;
|
|
7838
6903
|
const existing = readHooksFromRaw(raw);
|
|
7839
|
-
const remaining =
|
|
6904
|
+
const remaining = applyUninstall2(existing);
|
|
7840
6905
|
const next = JSON.stringify(existing) === JSON.stringify(remaining) ? raw : spliceHooks(raw, remaining);
|
|
7841
6906
|
const changed = next !== raw;
|
|
7842
6907
|
if (changed) {
|
|
@@ -7846,22 +6911,22 @@ function performUninstall3() {
|
|
|
7846
6911
|
removeLegacyShim();
|
|
7847
6912
|
return {
|
|
7848
6913
|
path,
|
|
7849
|
-
gate:
|
|
6914
|
+
gate: isGateInstalled2(remaining),
|
|
7850
6915
|
capture: isCaptureInstalled(remaining),
|
|
7851
6916
|
autoAccept: autoAcceptOf(next),
|
|
7852
6917
|
changed
|
|
7853
6918
|
};
|
|
7854
6919
|
}
|
|
7855
|
-
function
|
|
6920
|
+
function performStatus4() {
|
|
7856
6921
|
const path = configPath();
|
|
7857
|
-
const raw =
|
|
6922
|
+
const raw = existsSync5(path) ? readFileSync5(path, "utf8") : "";
|
|
7858
6923
|
const hooks = raw ? readHooks(parseHermesDocument(raw)) : {};
|
|
7859
6924
|
return {
|
|
7860
6925
|
path,
|
|
7861
|
-
present:
|
|
7862
|
-
gate:
|
|
6926
|
+
present: hasAnyHookRegistration2(hooks),
|
|
6927
|
+
gate: isGateInstalled2(hooks),
|
|
7863
6928
|
capture: isCaptureInstalled(hooks),
|
|
7864
|
-
complete:
|
|
6929
|
+
complete: hasCompleteHookRegistration2(hooks),
|
|
7865
6930
|
autoAccept: raw ? autoAcceptOf(raw) : false
|
|
7866
6931
|
};
|
|
7867
6932
|
}
|
|
@@ -7878,7 +6943,7 @@ function registerHermesCommands(program2) {
|
|
|
7878
6943
|
const hermes = program2.command("hermes").description("Manage the prim Hermes Agent integration (capture, gate, ingest, presence)");
|
|
7879
6944
|
hermes.command("install").description("Register the prim hooks in Hermes's ~/.hermes/config.yaml").option("--scope <scope>", "user (default; Hermes config is global-only)").option("--force", "Replace any drifted prim hook entries").option("--auto-accept", "Also set hooks_auto_accept: true so the hooks need no TTY consent").action((opts) => {
|
|
7880
6945
|
rejectProjectScope(opts.scope);
|
|
7881
|
-
const result =
|
|
6946
|
+
const result = performInstall2({
|
|
7882
6947
|
force: opts.force ?? false,
|
|
7883
6948
|
autoAccept: opts.autoAccept ?? false
|
|
7884
6949
|
});
|
|
@@ -7886,22 +6951,22 @@ function registerHermesCommands(program2) {
|
|
|
7886
6951
|
result.changed ? `[prim] Hermes integration installed at ${result.path}` : `[prim] Hermes integration already present at ${result.path} (no changes)`
|
|
7887
6952
|
);
|
|
7888
6953
|
console.error(TRUST_NOTICE2);
|
|
7889
|
-
console.log(JSON.stringify(result, null,
|
|
6954
|
+
console.log(JSON.stringify(result, null, JSON_INDENT2));
|
|
7890
6955
|
});
|
|
7891
6956
|
hermes.command("uninstall").description("Remove all prim hooks from Hermes's ~/.hermes/config.yaml").action(() => {
|
|
7892
|
-
const result =
|
|
6957
|
+
const result = performUninstall2();
|
|
7893
6958
|
console.error(
|
|
7894
6959
|
result.changed ? `[prim] prim hooks removed from ${result.path}` : `[prim] no prim hooks to remove at ${result.path} (nothing changed)`
|
|
7895
6960
|
);
|
|
7896
|
-
console.log(JSON.stringify(result, null,
|
|
6961
|
+
console.log(JSON.stringify(result, null, JSON_INDENT2));
|
|
7897
6962
|
});
|
|
7898
6963
|
hermes.command("status").description("Report whether each prim surface (gate, capture) is installed").action(() => {
|
|
7899
|
-
const result =
|
|
6964
|
+
const result = performStatus4();
|
|
7900
6965
|
const mark = (b) => b ? "\u2713" : "\u2717";
|
|
7901
6966
|
console.error(
|
|
7902
6967
|
`[prim] hermes: gate ${mark(result.gate)} \xB7 capture ${mark(result.capture)} (${result.path})`
|
|
7903
6968
|
);
|
|
7904
|
-
console.log(JSON.stringify(result, null,
|
|
6969
|
+
console.log(JSON.stringify(result, null, JSON_INDENT2));
|
|
7905
6970
|
});
|
|
7906
6971
|
}
|
|
7907
6972
|
|
|
@@ -7944,7 +7009,7 @@ function checkAuth() {
|
|
|
7944
7009
|
return classifyAuthCredential(
|
|
7945
7010
|
credential,
|
|
7946
7011
|
storedCredential ? getTokenExpiresAt() : void 0,
|
|
7947
|
-
storedCredential &&
|
|
7012
|
+
storedCredential && existsSync6(REFRESH_TOKEN_PATH)
|
|
7948
7013
|
);
|
|
7949
7014
|
}
|
|
7950
7015
|
function classifyDaemonHealth(snapshot, options = {}) {
|
|
@@ -8288,6 +7353,34 @@ function classifyCodexHooks(statuses) {
|
|
|
8288
7353
|
detail: "installed; Codex trust is not machine-readable \u2014 verify with `/hooks`"
|
|
8289
7354
|
};
|
|
8290
7355
|
}
|
|
7356
|
+
function classifyCursorHooks(statuses) {
|
|
7357
|
+
const installed = statuses.filter((status) => status.present);
|
|
7358
|
+
if (installed.length === 0)
|
|
7359
|
+
return { name: "cursor-hooks", status: "ok", detail: "not installed" };
|
|
7360
|
+
if (installed.some((status) => !status.complete)) {
|
|
7361
|
+
return {
|
|
7362
|
+
name: "cursor-hooks",
|
|
7363
|
+
status: "fail",
|
|
7364
|
+
detail: "incomplete or drifted native lifecycle \u2014 run `prim cursor install --force`"
|
|
7365
|
+
};
|
|
7366
|
+
}
|
|
7367
|
+
const user = statuses.find((status) => status.footer !== void 0);
|
|
7368
|
+
if (user?.present && user.footer === false && !user.footerPreservedCustom) {
|
|
7369
|
+
return {
|
|
7370
|
+
name: "cursor-hooks",
|
|
7371
|
+
status: "fail",
|
|
7372
|
+
detail: "native lifecycle ready but Cursor CLI footer is missing \u2014 run `prim cursor install --scope user --force`"
|
|
7373
|
+
};
|
|
7374
|
+
}
|
|
7375
|
+
if (statuses.some((status) => status.footerPreservedCustom)) {
|
|
7376
|
+
return {
|
|
7377
|
+
name: "cursor-hooks",
|
|
7378
|
+
status: "warn",
|
|
7379
|
+
detail: "native lifecycle ready; custom Cursor CLI footer preserved"
|
|
7380
|
+
};
|
|
7381
|
+
}
|
|
7382
|
+
return { name: "cursor-hooks", status: "ok", detail: "complete native lifecycle ready" };
|
|
7383
|
+
}
|
|
8291
7384
|
function classifyHermesHooks(status) {
|
|
8292
7385
|
if (!status.present) {
|
|
8293
7386
|
return { name: "hermes-hooks", status: "ok", detail: "not installed" };
|
|
@@ -8308,7 +7401,7 @@ function classifyHermesHooks(status) {
|
|
|
8308
7401
|
function checkAgentHooks() {
|
|
8309
7402
|
const checks = [];
|
|
8310
7403
|
try {
|
|
8311
|
-
const status =
|
|
7404
|
+
const status = performStatus3();
|
|
8312
7405
|
checks.push(classifyCodexHooks([status.project, status.user]));
|
|
8313
7406
|
} catch (error) {
|
|
8314
7407
|
const detail = boundedHealthError(error instanceof Error ? error.message : String(error));
|
|
@@ -8319,7 +7412,18 @@ function checkAgentHooks() {
|
|
|
8319
7412
|
});
|
|
8320
7413
|
}
|
|
8321
7414
|
try {
|
|
8322
|
-
|
|
7415
|
+
const status = performStatus2();
|
|
7416
|
+
checks.push(classifyCursorHooks([status.project, status.user]));
|
|
7417
|
+
} catch (error) {
|
|
7418
|
+
const detail = boundedHealthError(error instanceof Error ? error.message : String(error));
|
|
7419
|
+
checks.push({
|
|
7420
|
+
name: "cursor-hooks",
|
|
7421
|
+
status: "fail",
|
|
7422
|
+
detail: detail ?? "hook configuration is unreadable"
|
|
7423
|
+
});
|
|
7424
|
+
}
|
|
7425
|
+
try {
|
|
7426
|
+
checks.push(classifyHermesHooks(performStatus4()));
|
|
8323
7427
|
} catch (error) {
|
|
8324
7428
|
const detail = boundedHealthError(error instanceof Error ? error.message : String(error));
|
|
8325
7429
|
checks.push({
|
|
@@ -8428,8 +7532,9 @@ function checkHookRuntime() {
|
|
|
8428
7532
|
return diagnoseRegisteredHookRuntime(
|
|
8429
7533
|
[
|
|
8430
7534
|
...inspectHookRuntimeResolutions(),
|
|
7535
|
+
...inspectHookRuntimeResolutions3(),
|
|
8431
7536
|
...inspectHookRuntimeResolutions2(),
|
|
8432
|
-
...
|
|
7537
|
+
...inspectHookRuntimeResolutions4()
|
|
8433
7538
|
],
|
|
8434
7539
|
inspectHookRuntime,
|
|
8435
7540
|
packageVersion
|
|
@@ -8564,70 +7669,70 @@ function registerDoctorCommands(program2) {
|
|
|
8564
7669
|
}
|
|
8565
7670
|
|
|
8566
7671
|
// src/commands/moves.ts
|
|
8567
|
-
import { existsSync as
|
|
8568
|
-
import { join as
|
|
7672
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
7673
|
+
import { join as join9 } from "path";
|
|
8569
7674
|
|
|
8570
7675
|
// src/flusher.ts
|
|
8571
|
-
import { createReadStream, renameSync as
|
|
8572
|
-
import { dirname as
|
|
7676
|
+
import { createReadStream, renameSync as renameSync3, unlinkSync as unlinkSync3 } from "fs";
|
|
7677
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
8573
7678
|
|
|
8574
7679
|
// src/dead-letter.ts
|
|
8575
|
-
import { createHash as
|
|
7680
|
+
import { createHash as createHash3, randomBytes as randomBytes4 } from "crypto";
|
|
8576
7681
|
import {
|
|
8577
|
-
constants as
|
|
8578
|
-
chmodSync as
|
|
7682
|
+
constants as constants2,
|
|
7683
|
+
chmodSync as chmodSync2,
|
|
8579
7684
|
closeSync as closeSync2,
|
|
8580
|
-
existsSync as
|
|
7685
|
+
existsSync as existsSync7,
|
|
8581
7686
|
fchmodSync,
|
|
8582
7687
|
fsyncSync,
|
|
8583
|
-
mkdirSync as
|
|
7688
|
+
mkdirSync as mkdirSync4,
|
|
8584
7689
|
openSync as openSync2,
|
|
8585
|
-
renameSync as
|
|
8586
|
-
rmSync as
|
|
8587
|
-
writeFileSync as
|
|
7690
|
+
renameSync as renameSync2,
|
|
7691
|
+
rmSync as rmSync3,
|
|
7692
|
+
writeFileSync as writeFileSync3
|
|
8588
7693
|
} from "fs";
|
|
8589
|
-
import { dirname as
|
|
8590
|
-
var
|
|
7694
|
+
import { dirname as dirname3, join as join7 } from "path";
|
|
7695
|
+
var DIRECTORY_MODE = 448;
|
|
8591
7696
|
var FILE_MODE = 384;
|
|
8592
7697
|
var DEAD_LETTER_DIRNAME = "dead-letter";
|
|
8593
7698
|
function deadLetterDirectoryForRotation(flushingPath) {
|
|
8594
|
-
return
|
|
7699
|
+
return join7(dirname3(flushingPath), DEAD_LETTER_DIRNAME);
|
|
8595
7700
|
}
|
|
8596
7701
|
function deadLetterPathForRawLine(flushingPath, rawLine) {
|
|
8597
|
-
const quarantineId =
|
|
8598
|
-
return
|
|
7702
|
+
const quarantineId = createHash3("sha256").update(rawLine).digest("hex");
|
|
7703
|
+
return join7(deadLetterDirectoryForRotation(flushingPath), `raw-${quarantineId}.json`);
|
|
8599
7704
|
}
|
|
8600
7705
|
function ensureDeadLetterDirectory(flushingPath, persistDirectory) {
|
|
8601
7706
|
const directory = deadLetterDirectoryForRotation(flushingPath);
|
|
8602
|
-
|
|
8603
|
-
|
|
8604
|
-
persistDirectory(
|
|
7707
|
+
mkdirSync4(directory, { recursive: true, mode: DIRECTORY_MODE });
|
|
7708
|
+
chmodSync2(directory, DIRECTORY_MODE);
|
|
7709
|
+
persistDirectory(dirname3(directory));
|
|
8605
7710
|
return directory;
|
|
8606
7711
|
}
|
|
8607
7712
|
function persistDeadLetter(directory, path, quarantineId, record2, persistDirectory) {
|
|
8608
|
-
if (
|
|
7713
|
+
if (existsSync7(path)) {
|
|
8609
7714
|
persistDirectory(directory);
|
|
8610
7715
|
return;
|
|
8611
7716
|
}
|
|
8612
|
-
const temporaryPath =
|
|
7717
|
+
const temporaryPath = join7(
|
|
8613
7718
|
directory,
|
|
8614
|
-
`.${quarantineId}.${String(process.pid)}.${
|
|
7719
|
+
`.${quarantineId}.${String(process.pid)}.${randomBytes4(8).toString("hex")}.tmp`
|
|
8615
7720
|
);
|
|
8616
|
-
const flags =
|
|
7721
|
+
const flags = constants2.O_CREAT | constants2.O_EXCL | constants2.O_WRONLY | constants2.O_NOFOLLOW;
|
|
8617
7722
|
try {
|
|
8618
7723
|
const fd = openSync2(temporaryPath, flags, FILE_MODE);
|
|
8619
7724
|
try {
|
|
8620
7725
|
fchmodSync(fd, FILE_MODE);
|
|
8621
|
-
|
|
7726
|
+
writeFileSync3(fd, `${JSON.stringify(record2)}
|
|
8622
7727
|
`, "utf8");
|
|
8623
7728
|
fsyncSync(fd);
|
|
8624
7729
|
} finally {
|
|
8625
7730
|
closeSync2(fd);
|
|
8626
7731
|
}
|
|
8627
|
-
|
|
7732
|
+
renameSync2(temporaryPath, path);
|
|
8628
7733
|
persistDirectory(directory);
|
|
8629
7734
|
} finally {
|
|
8630
|
-
|
|
7735
|
+
rmSync3(temporaryPath, { force: true });
|
|
8631
7736
|
}
|
|
8632
7737
|
}
|
|
8633
7738
|
function quarantineMove(flushingPath, move, reason, now = Date.now, options = {}) {
|
|
@@ -8636,12 +7741,12 @@ function quarantineMove(flushingPath, move, reason, now = Date.now, options = {}
|
|
|
8636
7741
|
const serializedMove = JSON.stringify(move);
|
|
8637
7742
|
const record2 = {
|
|
8638
7743
|
version: 1,
|
|
8639
|
-
quarantineId:
|
|
7744
|
+
quarantineId: createHash3("sha256").update(serializedMove).digest("hex"),
|
|
8640
7745
|
quarantinedAt: now(),
|
|
8641
7746
|
reason,
|
|
8642
7747
|
move
|
|
8643
7748
|
};
|
|
8644
|
-
const path =
|
|
7749
|
+
const path = join7(directory, `${record2.quarantineId}.json`);
|
|
8645
7750
|
persistDeadLetter(directory, path, record2.quarantineId, record2, persistDirectory);
|
|
8646
7751
|
return record2;
|
|
8647
7752
|
}
|
|
@@ -8652,7 +7757,7 @@ function quarantineRawLine(flushingPath, rawLine, now = Date.now, options = {})
|
|
|
8652
7757
|
const record2 = {
|
|
8653
7758
|
version: 1,
|
|
8654
7759
|
recordKind: "raw_line_v1",
|
|
8655
|
-
quarantineId:
|
|
7760
|
+
quarantineId: createHash3("sha256").update(bytes).digest("hex"),
|
|
8656
7761
|
quarantinedAt: now(),
|
|
8657
7762
|
reason: "invalid_move",
|
|
8658
7763
|
rawLineEncoding: "base64",
|
|
@@ -8717,6 +7822,12 @@ function deadLetterReason(error) {
|
|
|
8717
7822
|
}
|
|
8718
7823
|
return void 0;
|
|
8719
7824
|
}
|
|
7825
|
+
function mayBeBatchSizeFailure(error) {
|
|
7826
|
+
if (error instanceof HttpError) {
|
|
7827
|
+
return error.status >= 500 && error.status !== 503;
|
|
7828
|
+
}
|
|
7829
|
+
return error instanceof Error && error.name === "TimeoutError";
|
|
7830
|
+
}
|
|
8720
7831
|
async function* rawJournalLines(input) {
|
|
8721
7832
|
let pending = [];
|
|
8722
7833
|
let pendingBytes = 0;
|
|
@@ -8758,7 +7869,7 @@ async function drainFlushingPath(flushingPath, client, binding, options = {}) {
|
|
|
8758
7869
|
return { flushed: batch2.length, quarantined: 0 };
|
|
8759
7870
|
} catch (error) {
|
|
8760
7871
|
const reason = deadLetterReason(error);
|
|
8761
|
-
if (!reason) {
|
|
7872
|
+
if (!(reason || mayBeBatchSizeFailure(error))) {
|
|
8762
7873
|
throw error;
|
|
8763
7874
|
}
|
|
8764
7875
|
if (batch2.length > 1) {
|
|
@@ -8767,6 +7878,9 @@ async function drainFlushingPath(flushingPath, client, binding, options = {}) {
|
|
|
8767
7878
|
const right = await postBatch(batch2.slice(midpoint));
|
|
8768
7879
|
return addDrainCounts(left, right);
|
|
8769
7880
|
}
|
|
7881
|
+
if (!reason) {
|
|
7882
|
+
throw error;
|
|
7883
|
+
}
|
|
8770
7884
|
const [move] = batch2;
|
|
8771
7885
|
const quarantined = quarantineMove(
|
|
8772
7886
|
flushingPath,
|
|
@@ -8825,7 +7939,7 @@ async function drainFlushingPath(flushingPath, client, binding, options = {}) {
|
|
|
8825
7939
|
async function drainPath(path, client, binding) {
|
|
8826
7940
|
const tmpPath = `${path}.flushing.${String(Date.now())}.${String(process.pid)}`;
|
|
8827
7941
|
try {
|
|
8828
|
-
|
|
7942
|
+
renameSync3(path, tmpPath);
|
|
8829
7943
|
} catch (err) {
|
|
8830
7944
|
if (err.code === "ENOENT") {
|
|
8831
7945
|
return { flushed: 0, quarantined: 0 };
|
|
@@ -8924,7 +8038,7 @@ async function flushOnce() {
|
|
|
8924
8038
|
return inspection.retainedBuckets.length > 0 ? { flushed: total, quarantined, retained: inspection.retainedBuckets } : { flushed: total, quarantined };
|
|
8925
8039
|
}
|
|
8926
8040
|
var flushInFlight;
|
|
8927
|
-
var FLUSH_LOCK_PATH =
|
|
8041
|
+
var FLUSH_LOCK_PATH = join8(dirname4(JOURNAL_DIR), ".flush.lock");
|
|
8928
8042
|
var FLUSH_LOCK_TIMEOUT_MS = 250;
|
|
8929
8043
|
function isFlushLockContended(error) {
|
|
8930
8044
|
return error instanceof Error && error.message.startsWith("timed out waiting for file lock");
|
|
@@ -9058,19 +8172,19 @@ function registerMovesCommands(program2) {
|
|
|
9058
8172
|
}
|
|
9059
8173
|
});
|
|
9060
8174
|
moves.command("bind").description("Pin the current directory to an org via .prim/workspace.json").requiredOption("--orgId <orgId>", "Convex organization id").action((opts) => {
|
|
9061
|
-
const dir =
|
|
9062
|
-
if (!
|
|
9063
|
-
|
|
8175
|
+
const dir = join9(process.cwd(), ".prim");
|
|
8176
|
+
if (!existsSync8(dir)) {
|
|
8177
|
+
mkdirSync5(dir, { recursive: true, mode: DIR_MODE });
|
|
9064
8178
|
}
|
|
9065
|
-
const file =
|
|
9066
|
-
|
|
8179
|
+
const file = join9(process.cwd(), WORKSPACE_FILE);
|
|
8180
|
+
writeFileSync4(file, JSON.stringify({ orgId: opts.orgId, boundAt: Date.now() }, null, 2), {
|
|
9067
8181
|
mode: FILE_MODE2
|
|
9068
8182
|
});
|
|
9069
8183
|
console.log(`[prim] bound ${process.cwd()} to org ${opts.orgId}`);
|
|
9070
8184
|
});
|
|
9071
8185
|
moves.command("drop").description("Remove the .prim/workspace.json binding from the cwd").action(() => {
|
|
9072
|
-
const file =
|
|
9073
|
-
if (!
|
|
8186
|
+
const file = join9(process.cwd(), WORKSPACE_FILE);
|
|
8187
|
+
if (!existsSync8(file)) {
|
|
9074
8188
|
console.log("[prim] no workspace binding in cwd");
|
|
9075
8189
|
return;
|
|
9076
8190
|
}
|
|
@@ -9161,23 +8275,23 @@ function registerReconcileCommands(program2) {
|
|
|
9161
8275
|
|
|
9162
8276
|
// src/commands/session.ts
|
|
9163
8277
|
import {
|
|
9164
|
-
existsSync as
|
|
9165
|
-
mkdirSync as
|
|
9166
|
-
readFileSync as
|
|
9167
|
-
readdirSync as
|
|
8278
|
+
existsSync as existsSync9,
|
|
8279
|
+
mkdirSync as mkdirSync6,
|
|
8280
|
+
readFileSync as readFileSync6,
|
|
8281
|
+
readdirSync as readdirSync3,
|
|
9168
8282
|
unlinkSync as unlinkSync5,
|
|
9169
|
-
writeFileSync as
|
|
8283
|
+
writeFileSync as writeFileSync5
|
|
9170
8284
|
} from "fs";
|
|
9171
|
-
import { join as
|
|
8285
|
+
import { join as join10 } from "path";
|
|
9172
8286
|
var DIR_MODE2 = 448;
|
|
9173
8287
|
var FILE_MODE3 = 384;
|
|
9174
8288
|
function ensureDir() {
|
|
9175
|
-
if (!
|
|
9176
|
-
|
|
8289
|
+
if (!existsSync9(SESSIONS_DIR)) {
|
|
8290
|
+
mkdirSync6(SESSIONS_DIR, { recursive: true, mode: DIR_MODE2 });
|
|
9177
8291
|
}
|
|
9178
8292
|
}
|
|
9179
8293
|
function markerPath(sessionId) {
|
|
9180
|
-
return
|
|
8294
|
+
return join10(SESSIONS_DIR, `${sessionId}.json`);
|
|
9181
8295
|
}
|
|
9182
8296
|
function registerSessionCommands(program2) {
|
|
9183
8297
|
const session = program2.command("session").description("Decision Event Pipeline \u2014 session binding markers");
|
|
@@ -9187,17 +8301,17 @@ function registerSessionCommands(program2) {
|
|
|
9187
8301
|
orgId: opts.orgId,
|
|
9188
8302
|
startedAt: Date.now()
|
|
9189
8303
|
};
|
|
9190
|
-
|
|
8304
|
+
writeFileSync5(markerPath(sessionId), JSON.stringify(marker, null, 2), {
|
|
9191
8305
|
mode: FILE_MODE3
|
|
9192
8306
|
});
|
|
9193
8307
|
console.log(`[prim] session ${sessionId} bound to org ${opts.orgId}`);
|
|
9194
8308
|
});
|
|
9195
8309
|
session.command("list").description("List active session markers").action(() => {
|
|
9196
|
-
if (!
|
|
8310
|
+
if (!existsSync9(SESSIONS_DIR)) {
|
|
9197
8311
|
console.log("[prim] no session markers");
|
|
9198
8312
|
return;
|
|
9199
8313
|
}
|
|
9200
|
-
const files =
|
|
8314
|
+
const files = readdirSync3(SESSIONS_DIR).filter((f) => f.endsWith(".json"));
|
|
9201
8315
|
if (files.length === 0) {
|
|
9202
8316
|
console.log("[prim] no session markers");
|
|
9203
8317
|
return;
|
|
@@ -9205,7 +8319,7 @@ function registerSessionCommands(program2) {
|
|
|
9205
8319
|
for (const f of files) {
|
|
9206
8320
|
const sessionId = f.replace(/\.json$/, "");
|
|
9207
8321
|
try {
|
|
9208
|
-
const m = JSON.parse(
|
|
8322
|
+
const m = JSON.parse(readFileSync6(join10(SESSIONS_DIR, f), "utf-8"));
|
|
9209
8323
|
console.log(`${sessionId} org=${m.orgId}`);
|
|
9210
8324
|
} catch {
|
|
9211
8325
|
}
|
|
@@ -9213,7 +8327,7 @@ function registerSessionCommands(program2) {
|
|
|
9213
8327
|
});
|
|
9214
8328
|
session.command("drop <sessionId>").description("Remove a session marker").action((sessionId) => {
|
|
9215
8329
|
const p = markerPath(sessionId);
|
|
9216
|
-
if (!
|
|
8330
|
+
if (!existsSync9(p)) {
|
|
9217
8331
|
console.log(`[prim] no marker for session ${sessionId}`);
|
|
9218
8332
|
return;
|
|
9219
8333
|
}
|
|
@@ -9224,13 +8338,14 @@ function registerSessionCommands(program2) {
|
|
|
9224
8338
|
|
|
9225
8339
|
// src/commands/setup.ts
|
|
9226
8340
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
9227
|
-
import { existsSync as
|
|
9228
|
-
import { join as
|
|
8341
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
|
|
8342
|
+
import { join as join11 } from "path";
|
|
9229
8343
|
var EXIT_INCOMPLETE = 1;
|
|
9230
8344
|
var EXIT_USAGE3 = 2;
|
|
9231
8345
|
var SESSION_LABELS = {
|
|
9232
8346
|
claude: "Claude Code integration",
|
|
9233
8347
|
codex: "Codex integration",
|
|
8348
|
+
cursor: "Cursor integration",
|
|
9234
8349
|
hermes: "Hermes integration"
|
|
9235
8350
|
};
|
|
9236
8351
|
function planSetupSteps(opts) {
|
|
@@ -9306,8 +8421,8 @@ function detectProjectConflicts(agent, run) {
|
|
|
9306
8421
|
}
|
|
9307
8422
|
try {
|
|
9308
8423
|
const root = gitToplevel();
|
|
9309
|
-
const preCommit = root &&
|
|
9310
|
-
if (preCommit &&
|
|
8424
|
+
const preCommit = root && join11(root, ".git", "hooks", "pre-commit");
|
|
8425
|
+
if (preCommit && existsSync10(preCommit) && readFileSync7(preCommit, "utf-8").includes("prim-pre-commit")) {
|
|
9311
8426
|
conflicts.push(CONFLICT_HOOKS);
|
|
9312
8427
|
}
|
|
9313
8428
|
} catch {
|
|
@@ -9325,6 +8440,9 @@ function detectAgent(env) {
|
|
|
9325
8440
|
if (env.HERMES_INTERACTIVE) {
|
|
9326
8441
|
return "hermes";
|
|
9327
8442
|
}
|
|
8443
|
+
if (env.CURSOR_AGENT) {
|
|
8444
|
+
return "cursor";
|
|
8445
|
+
}
|
|
9328
8446
|
return "claude";
|
|
9329
8447
|
}
|
|
9330
8448
|
function resolveAgent(agentFlag, env) {
|
|
@@ -9346,7 +8464,7 @@ function parseSetupAuthStatus(result) {
|
|
|
9346
8464
|
function registerSetupCommand(program2, dependencies = {}) {
|
|
9347
8465
|
program2.command("setup").description(
|
|
9348
8466
|
"Install everything in one shot (auth, session + git hooks, daemon, skill, welcome)"
|
|
9349
|
-
).option("--agent <agent>", "claude, codex, or hermes (auto-detected when omitted)").option(
|
|
8467
|
+
).option("--agent <agent>", "claude, codex, cursor, or hermes (auto-detected when omitted)").option(
|
|
9350
8468
|
"--scope <scope>",
|
|
9351
8469
|
"user (default \u2014 install once for every repo) or project (this repo only)",
|
|
9352
8470
|
"user"
|
|
@@ -9355,9 +8473,9 @@ function registerSetupCommand(program2, dependencies = {}) {
|
|
|
9355
8473
|
"with the default user scope, remove any project-scoped prim config in this repo (else just warn)"
|
|
9356
8474
|
).option("--no-daemon", "stop and disable the companion daemon").action((opts) => {
|
|
9357
8475
|
const { agent: agentInput, detected } = resolveAgent(opts.agent, process.env);
|
|
9358
|
-
if (agentInput !== "claude" && agentInput !== "codex" && agentInput !== "hermes") {
|
|
8476
|
+
if (agentInput !== "claude" && agentInput !== "codex" && agentInput !== "cursor" && agentInput !== "hermes") {
|
|
9359
8477
|
process.stderr.write(
|
|
9360
|
-
`[prim] unknown --agent "${agentInput}" (expected claude, codex, or hermes)
|
|
8478
|
+
`[prim] unknown --agent "${agentInput}" (expected claude, codex, cursor, or hermes)
|
|
9361
8479
|
`
|
|
9362
8480
|
);
|
|
9363
8481
|
(dependencies.exit ?? process.exit)(EXIT_USAGE3);
|
|
@@ -9468,23 +8586,23 @@ function registerSetupCommand(program2, dependencies = {}) {
|
|
|
9468
8586
|
}
|
|
9469
8587
|
|
|
9470
8588
|
// src/commands/statusline.ts
|
|
9471
|
-
import { readFileSync as
|
|
9472
|
-
import { dirname as
|
|
8589
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
8590
|
+
import { dirname as dirname5, resolve as resolve3 } from "path";
|
|
9473
8591
|
import { fileURLToPath } from "url";
|
|
9474
8592
|
var STATUSLINE_TIMEOUT_MS = 200;
|
|
9475
8593
|
function readPackageVersion() {
|
|
9476
8594
|
try {
|
|
9477
|
-
const here =
|
|
8595
|
+
const here = dirname5(fileURLToPath(import.meta.url));
|
|
9478
8596
|
const candidates = [
|
|
9479
8597
|
// Schema-v2 runtimes staged a manifest beside this standalone bundle.
|
|
9480
8598
|
// Keep resolving it while those legacy launchers transition to schema v3.
|
|
9481
|
-
|
|
9482
|
-
|
|
9483
|
-
|
|
8599
|
+
resolve3(here, "manifest.json"),
|
|
8600
|
+
resolve3(here, "../../package.json"),
|
|
8601
|
+
resolve3(here, "../package.json")
|
|
9484
8602
|
];
|
|
9485
8603
|
for (const path of candidates) {
|
|
9486
8604
|
try {
|
|
9487
|
-
const pkg2 = JSON.parse(
|
|
8605
|
+
const pkg2 = JSON.parse(readFileSync8(path, "utf-8"));
|
|
9488
8606
|
if (pkg2.version) {
|
|
9489
8607
|
return pkg2.version;
|
|
9490
8608
|
}
|
|
@@ -9501,9 +8619,8 @@ function debug(msg) {
|
|
|
9501
8619
|
`);
|
|
9502
8620
|
}
|
|
9503
8621
|
}
|
|
9504
|
-
async function renderStatusline() {
|
|
8622
|
+
async function renderStatusline(cwd = process.cwd()) {
|
|
9505
8623
|
const version = readPackageVersion();
|
|
9506
|
-
const cwd = process.cwd();
|
|
9507
8624
|
let ingestion;
|
|
9508
8625
|
const resolveIngestion = () => {
|
|
9509
8626
|
ingestion ??= decisionIngestionStatus(cwd);
|
|
@@ -9551,6 +8668,11 @@ function planUninstallSteps(inRepository) {
|
|
|
9551
8668
|
label: "Codex project integration",
|
|
9552
8669
|
args: ["codex", "uninstall", "--scope", "project"]
|
|
9553
8670
|
},
|
|
8671
|
+
{
|
|
8672
|
+
key: "cursor-project",
|
|
8673
|
+
label: "Cursor project integration",
|
|
8674
|
+
args: ["cursor", "uninstall", "--scope", "project"]
|
|
8675
|
+
},
|
|
9554
8676
|
{
|
|
9555
8677
|
key: "hooks-project",
|
|
9556
8678
|
label: "project Git hooks",
|
|
@@ -9570,6 +8692,11 @@ function planUninstallSteps(inRepository) {
|
|
|
9570
8692
|
label: "Codex user integration",
|
|
9571
8693
|
args: ["codex", "uninstall", "--scope", "user"]
|
|
9572
8694
|
},
|
|
8695
|
+
{
|
|
8696
|
+
key: "cursor-user",
|
|
8697
|
+
label: "Cursor user integration",
|
|
8698
|
+
args: ["cursor", "uninstall", "--scope", "user"]
|
|
8699
|
+
},
|
|
9573
8700
|
{ key: "hermes-user", label: "Hermes integration", args: ["hermes", "uninstall"] },
|
|
9574
8701
|
{
|
|
9575
8702
|
key: "hooks-user",
|
|
@@ -9588,7 +8715,7 @@ function uninstallStepSucceeded(step, result) {
|
|
|
9588
8715
|
return false;
|
|
9589
8716
|
}
|
|
9590
8717
|
}
|
|
9591
|
-
if (!step.key.startsWith("claude-") && !step.key.startsWith("codex-") && step.key !== "hermes-user") {
|
|
8718
|
+
if (!step.key.startsWith("claude-") && !step.key.startsWith("codex-") && !step.key.startsWith("cursor-") && step.key !== "hermes-user") {
|
|
9592
8719
|
return true;
|
|
9593
8720
|
}
|
|
9594
8721
|
try {
|
|
@@ -9596,7 +8723,7 @@ function uninstallStepSucceeded(step, result) {
|
|
|
9596
8723
|
if (step.key.startsWith("claude-")) {
|
|
9597
8724
|
return parsed.gate === false && parsed.capture === false && parsed.feedback === false && parsed.statusline === false;
|
|
9598
8725
|
}
|
|
9599
|
-
return parsed.gate === false && parsed.capture === false;
|
|
8726
|
+
return parsed.gate === false && parsed.capture === false && (!step.key.startsWith("cursor-user") || parsed.footer === false);
|
|
9600
8727
|
} catch {
|
|
9601
8728
|
return false;
|
|
9602
8729
|
}
|
|
@@ -9736,7 +8863,7 @@ function registerUninstallCommand(program2, dependencies = {}) {
|
|
|
9736
8863
|
// src/commands/welcome.ts
|
|
9737
8864
|
var CMD_GUTTER = 38;
|
|
9738
8865
|
var RECENT_LIMIT = 5;
|
|
9739
|
-
var WELCOME_AGENTS = ["claude", "codex", "hermes"];
|
|
8866
|
+
var WELCOME_AGENTS = ["claude", "codex", "cursor", "hermes"];
|
|
9740
8867
|
function resolveWelcomeAgent(agentFlag, env) {
|
|
9741
8868
|
if (agentFlag !== void 0 && WELCOME_AGENTS.includes(agentFlag)) {
|
|
9742
8869
|
return agentFlag;
|
|
@@ -9744,6 +8871,9 @@ function resolveWelcomeAgent(agentFlag, env) {
|
|
|
9744
8871
|
if (agentFlag === void 0 && env.HERMES_INTERACTIVE) {
|
|
9745
8872
|
return "hermes";
|
|
9746
8873
|
}
|
|
8874
|
+
if (agentFlag === void 0 && env.CURSOR_AGENT) {
|
|
8875
|
+
return "cursor";
|
|
8876
|
+
}
|
|
9747
8877
|
return void 0;
|
|
9748
8878
|
}
|
|
9749
8879
|
var CAPTURE_LINES = {
|
|
@@ -9752,6 +8882,7 @@ var CAPTURE_LINES = {
|
|
|
9752
8882
|
"Capture starts once you trust the prim hooks \u2014 run /hooks in Codex;",
|
|
9753
8883
|
"until then nothing is recorded."
|
|
9754
8884
|
],
|
|
8885
|
+
cursor: ["Capture is automatic in trusted Cursor workspaces; restart Cursor CLI for the footer."],
|
|
9755
8886
|
hermes: [
|
|
9756
8887
|
"Capture starts once you approve the prim hooks when Hermes prompts;",
|
|
9757
8888
|
"until then nothing is recorded."
|
|
@@ -9767,6 +8898,7 @@ var PASSIVE_LINES = {
|
|
|
9767
8898
|
"Once you trust the prim hooks (/hooks in Codex), Primitive captures",
|
|
9768
8899
|
"decisions passively while you work."
|
|
9769
8900
|
],
|
|
8901
|
+
cursor: ["In trusted Cursor workspaces, Primitive captures decisions passively while you work."],
|
|
9770
8902
|
hermes: [
|
|
9771
8903
|
"Once you approve the prim hooks, Primitive captures decisions",
|
|
9772
8904
|
"passively while you work."
|
|
@@ -9779,6 +8911,7 @@ var PASSIVE_LINES = {
|
|
|
9779
8911
|
var GUIDANCE_PASSIVE = {
|
|
9780
8912
|
claude: "Otherwise, Primitive passively captures decisions in the background while you work.",
|
|
9781
8913
|
codex: "Otherwise, once you trust the prim hooks (run /hooks in Codex), Primitive passively captures decisions in the background while you work.",
|
|
8914
|
+
cursor: "Otherwise, Primitive passively captures decisions in trusted Cursor workspaces while you work.",
|
|
9782
8915
|
hermes: "Otherwise, once you approve the prim hooks when Hermes prompts, Primitive passively captures decisions in the background while you work.",
|
|
9783
8916
|
generic: "Otherwise, once your agent's prim hooks are active, Primitive passively captures decisions in the background while you work."
|
|
9784
8917
|
};
|
|
@@ -9873,10 +9006,7 @@ function welcomeJson(state, agent) {
|
|
|
9873
9006
|
return { welcomed: true, org: "unknown" };
|
|
9874
9007
|
}
|
|
9875
9008
|
function registerWelcomeCommand(program2, deps = { getClient }) {
|
|
9876
|
-
program2.command("welcome").description("Print a brief orientation to Primitive's decision graph").option(
|
|
9877
|
-
"--agent <agent>",
|
|
9878
|
-
"claude, codex, or hermes \u2014 tailors the capture guidance (Hermes auto-detected)"
|
|
9879
|
-
).action(async (options) => {
|
|
9009
|
+
program2.command("welcome").description("Print a brief orientation to Primitive's decision graph").option("--agent <agent>", "claude, codex, cursor, or hermes \u2014 tailors the capture guidance").action(async (options) => {
|
|
9880
9010
|
const agent = resolveWelcomeAgent(options.agent, process.env);
|
|
9881
9011
|
const result = await fetchRecent({ limit: RECENT_LIMIT }, deps);
|
|
9882
9012
|
const state = welcomeStateFromRecent(result);
|
|
@@ -9903,8 +9033,8 @@ function runStartupBackgroundWork(argv, env, work) {
|
|
|
9903
9033
|
}
|
|
9904
9034
|
|
|
9905
9035
|
// src/index.ts
|
|
9906
|
-
var __dirname =
|
|
9907
|
-
var pkg = JSON.parse(
|
|
9036
|
+
var __dirname = dirname6(fileURLToPath2(import.meta.url));
|
|
9037
|
+
var pkg = JSON.parse(readFileSync9(resolve4(__dirname, "../package.json"), "utf-8"));
|
|
9908
9038
|
runStartupBackgroundWork(process.argv.slice(2), process.env, {
|
|
9909
9039
|
notify: () => updateNotifier({ pkg }).notify(),
|
|
9910
9040
|
flush: () => flushIfNeeded()
|
|
@@ -9923,6 +9053,7 @@ registerSessionCommands(program);
|
|
|
9923
9053
|
registerDecisionsCommands(program);
|
|
9924
9054
|
registerClaudeCommands(program);
|
|
9925
9055
|
registerCodexCommands(program);
|
|
9056
|
+
registerCursorCommands(program);
|
|
9926
9057
|
registerHermesCommands(program);
|
|
9927
9058
|
registerDaemonCommands(program);
|
|
9928
9059
|
registerGithubCommands(program);
|