@primitive.ai/prim 0.1.0-alpha.28 → 0.1.0-alpha.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-JZGWQDM5.js → chunk-C4UYMR7X.js} +59 -8
- package/dist/{chunk-E5UZXMZL.js → chunk-UWAJCF7P.js} +7 -4
- package/dist/daemon/server.js +39 -6
- package/dist/hooks/post-commit.js +2 -1
- package/dist/hooks/post-tool-use.js +3 -3
- package/dist/hooks/pre-commit.js +1 -1
- package/dist/hooks/pre-tool-use.js +7 -4
- package/dist/hooks/prim-hook.js +2 -1
- package/dist/hooks/session-start.js +6 -1
- package/dist/index.js +256 -62
- package/package.json +1 -1
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getSiteUrl
|
|
3
|
+
} from "./chunk-26VA3ADF.js";
|
|
4
|
+
|
|
1
5
|
// src/journal.ts
|
|
2
6
|
import {
|
|
3
7
|
appendFileSync,
|
|
@@ -10,9 +14,15 @@ import {
|
|
|
10
14
|
import { homedir } from "os";
|
|
11
15
|
import { dirname, join } from "path";
|
|
12
16
|
var JOURNAL_DIR = join(homedir(), ".config", "prim", "moves");
|
|
13
|
-
var LEGACY_JOURNAL_PATH = join(JOURNAL_DIR, "journal.ndjson");
|
|
14
17
|
var UNBOUND_BUCKET = "_unbound";
|
|
15
18
|
var JOURNAL_BASENAME = "journal.ndjson";
|
|
19
|
+
function envSlug(apiUrl) {
|
|
20
|
+
const slug = apiUrl.replace(/^https?:\/\//, "").replace(/[^a-zA-Z0-9._-]/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
|
|
21
|
+
return slug.length > 0 ? slug : "default";
|
|
22
|
+
}
|
|
23
|
+
function currentEnvDir() {
|
|
24
|
+
return join(JOURNAL_DIR, envSlug(getSiteUrl()));
|
|
25
|
+
}
|
|
16
26
|
var DIR_MODE = 448;
|
|
17
27
|
var FILE_MODE = 384;
|
|
18
28
|
var SAFE_BUCKET = /^[A-Za-z0-9_-]+$/;
|
|
@@ -27,7 +37,7 @@ function appendMoveToPath(path, move) {
|
|
|
27
37
|
}
|
|
28
38
|
function bucketDir(orgId) {
|
|
29
39
|
const safe = orgId !== void 0 && SAFE_BUCKET.test(orgId) && !RESERVED_BUCKETS.has(orgId);
|
|
30
|
-
return join(
|
|
40
|
+
return join(currentEnvDir(), safe ? orgId : UNBOUND_BUCKET);
|
|
31
41
|
}
|
|
32
42
|
function journalPath(orgId) {
|
|
33
43
|
return join(bucketDir(orgId), JOURNAL_BASENAME);
|
|
@@ -54,23 +64,63 @@ function readMovesFromPath(path) {
|
|
|
54
64
|
}
|
|
55
65
|
function listBuckets() {
|
|
56
66
|
const out = [];
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
if (!existsSync(JOURNAL_DIR)) {
|
|
67
|
+
const envDir = currentEnvDir();
|
|
68
|
+
if (!existsSync(envDir)) {
|
|
61
69
|
return out;
|
|
62
70
|
}
|
|
63
|
-
for (const entry of readdirSync(
|
|
71
|
+
for (const entry of readdirSync(envDir, { withFileTypes: true })) {
|
|
64
72
|
if (!entry.isDirectory()) {
|
|
65
73
|
continue;
|
|
66
74
|
}
|
|
67
|
-
const path = join(
|
|
75
|
+
const path = join(envDir, entry.name, JOURNAL_BASENAME);
|
|
68
76
|
if (existsSync(path)) {
|
|
69
77
|
out.push({ bucket: entry.name, path });
|
|
70
78
|
}
|
|
71
79
|
}
|
|
72
80
|
return out;
|
|
73
81
|
}
|
|
82
|
+
var FLUSHING_PREFIX = `${JOURNAL_BASENAME}.flushing.`;
|
|
83
|
+
function parseFlushingPid(name) {
|
|
84
|
+
const segments = name.slice(FLUSHING_PREFIX.length).split(".");
|
|
85
|
+
const last = segments.length >= 2 ? segments[segments.length - 1] : void 0;
|
|
86
|
+
return last !== void 0 && /^[0-9]+$/.test(last) ? Number(last) : void 0;
|
|
87
|
+
}
|
|
88
|
+
function listFlushingInDir(dir, bucket) {
|
|
89
|
+
if (!existsSync(dir)) {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
const out = [];
|
|
93
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
94
|
+
if (!entry.isFile() || !entry.name.startsWith(FLUSHING_PREFIX)) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const path = join(dir, entry.name);
|
|
98
|
+
const stat = statSync(path);
|
|
99
|
+
const lineCount = readFileSync(path, "utf-8").split("\n").filter((l) => l.length > 0).length;
|
|
100
|
+
out.push({
|
|
101
|
+
bucket,
|
|
102
|
+
path,
|
|
103
|
+
pid: parseFlushingPid(entry.name),
|
|
104
|
+
sizeBytes: stat.size,
|
|
105
|
+
mtimeMs: stat.mtimeMs,
|
|
106
|
+
lineCount
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
function listFlushing() {
|
|
112
|
+
const envDir = currentEnvDir();
|
|
113
|
+
if (!existsSync(envDir)) {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
const out = [];
|
|
117
|
+
for (const entry of readdirSync(envDir, { withFileTypes: true })) {
|
|
118
|
+
if (entry.isDirectory()) {
|
|
119
|
+
out.push(...listFlushingInDir(join(envDir, entry.name), entry.name));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
74
124
|
function bucketStats() {
|
|
75
125
|
return listBuckets().map(({ bucket, path }) => {
|
|
76
126
|
const stat = statSync(path);
|
|
@@ -193,6 +243,7 @@ export {
|
|
|
193
243
|
appendMove,
|
|
194
244
|
readMovesFromPath,
|
|
195
245
|
listBuckets,
|
|
246
|
+
listFlushing,
|
|
196
247
|
bucketStats,
|
|
197
248
|
SESSIONS_DIR,
|
|
198
249
|
resolveOrg
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
getClient
|
|
2
|
+
getClient,
|
|
3
|
+
getSiteUrl
|
|
3
4
|
} from "./chunk-26VA3ADF.js";
|
|
4
5
|
import {
|
|
5
6
|
daemonRequest
|
|
@@ -9,9 +10,11 @@ import {
|
|
|
9
10
|
var DAEMON_HTTP_TIMEOUT_MS = 1e4;
|
|
10
11
|
var DAEMON_PROBE_TIMEOUT_MS = 250;
|
|
11
12
|
async function daemonOrDirect(method, params, direct) {
|
|
12
|
-
const fromDaemon = await daemonRequest(
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const fromDaemon = await daemonRequest(
|
|
14
|
+
method,
|
|
15
|
+
{ ...params, callerEnv: getSiteUrl() },
|
|
16
|
+
{ timeoutMs: DAEMON_PROBE_TIMEOUT_MS }
|
|
17
|
+
);
|
|
15
18
|
if (fromDaemon !== null) {
|
|
16
19
|
return fromDaemon;
|
|
17
20
|
}
|
package/dist/daemon/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
getClient,
|
|
4
|
+
getSiteUrl,
|
|
4
5
|
getTokenExpiresAt,
|
|
5
6
|
refreshToken
|
|
6
7
|
} from "../chunk-26VA3ADF.js";
|
|
@@ -10,6 +11,24 @@ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "
|
|
|
10
11
|
import { createServer } from "net";
|
|
11
12
|
import { homedir } from "os";
|
|
12
13
|
import { join } from "path";
|
|
14
|
+
|
|
15
|
+
// src/daemon/env-binding.ts
|
|
16
|
+
function normalizeApiUrl(url) {
|
|
17
|
+
return url.trim().replace(/\/+$/, "");
|
|
18
|
+
}
|
|
19
|
+
function apiUrlsMatch(a, b) {
|
|
20
|
+
return normalizeApiUrl(a) === normalizeApiUrl(b);
|
|
21
|
+
}
|
|
22
|
+
function isCrossEnv(callerEnv, boundEnv) {
|
|
23
|
+
return typeof callerEnv === "string" && !apiUrlsMatch(callerEnv, boundEnv);
|
|
24
|
+
}
|
|
25
|
+
function assertCallerEnvMatches(callerEnv, boundEnv) {
|
|
26
|
+
if (isCrossEnv(callerEnv, boundEnv)) {
|
|
27
|
+
throw new Error(`env mismatch: daemon is bound to ${boundEnv}, caller targets ${callerEnv}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/daemon/server.ts
|
|
13
32
|
var CONFIG_DIR = join(homedir(), ".config", "prim");
|
|
14
33
|
var SOCK_PATH = join(CONFIG_DIR, "sock");
|
|
15
34
|
var PID_PATH = join(CONFIG_DIR, "daemon.pid");
|
|
@@ -94,6 +113,7 @@ async function ensureTokenFresh() {
|
|
|
94
113
|
}
|
|
95
114
|
}
|
|
96
115
|
async function handleConflictCheck(params) {
|
|
116
|
+
assertCallerEnvMatches(params.callerEnv, getSiteUrl());
|
|
97
117
|
if (typeof params.file !== "string") {
|
|
98
118
|
throw new Error("conflict_check requires `file: string`");
|
|
99
119
|
}
|
|
@@ -111,18 +131,31 @@ function assertEndpointPath(path, endpoint) {
|
|
|
111
131
|
}
|
|
112
132
|
}
|
|
113
133
|
async function proxyGet(params, allowedPrefix) {
|
|
134
|
+
assertCallerEnvMatches(params.callerEnv, getSiteUrl());
|
|
114
135
|
const path = pathParam(params);
|
|
115
136
|
assertEndpointPath(path, allowedPrefix);
|
|
116
137
|
return await client.get(path, { signal: AbortSignal.timeout(HTTP_PROXY_TIMEOUT_MS) });
|
|
117
138
|
}
|
|
118
|
-
function handleStatusSnapshot() {
|
|
119
|
-
const
|
|
120
|
-
const presenceStale = lastOkAtLocal !== void 0 && !presenceFresh;
|
|
121
|
-
return {
|
|
139
|
+
function handleStatusSnapshot(params) {
|
|
140
|
+
const base = {
|
|
122
141
|
pid: process.pid,
|
|
123
142
|
uptimeMs: Date.now() - startedAt,
|
|
124
143
|
sessionId: activeSessionId,
|
|
125
|
-
lastHeartbeatAt
|
|
144
|
+
lastHeartbeatAt
|
|
145
|
+
};
|
|
146
|
+
if (isCrossEnv(params.callerEnv, getSiteUrl())) {
|
|
147
|
+
return {
|
|
148
|
+
...base,
|
|
149
|
+
onlineCount: void 0,
|
|
150
|
+
onlineNames: void 0,
|
|
151
|
+
presenceStale: false,
|
|
152
|
+
envMismatch: true
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const presenceFresh = lastOkAtLocal !== void 0 && Date.now() - lastOkAtLocal < PRESENCE_FRESH_WINDOW_MS;
|
|
156
|
+
const presenceStale = lastOkAtLocal !== void 0 && !presenceFresh;
|
|
157
|
+
return {
|
|
158
|
+
...base,
|
|
126
159
|
// Withhold a frozen count/names once they're no longer fresh; the
|
|
127
160
|
// statusline shows "presence: stale" rather than a confident, wrong list.
|
|
128
161
|
onlineCount: presenceFresh ? lastOnlineCount : void 0,
|
|
@@ -166,7 +199,7 @@ async function dispatchRequest(req) {
|
|
|
166
199
|
return { id, ok: true, result: { ack: true } };
|
|
167
200
|
}
|
|
168
201
|
case "status_snapshot":
|
|
169
|
-
return { id, ok: true, result: handleStatusSnapshot() };
|
|
202
|
+
return { id, ok: true, result: handleStatusSnapshot(req.params ?? {}) };
|
|
170
203
|
case "ping":
|
|
171
204
|
return { id, ok: true, result: { pong: true } };
|
|
172
205
|
default:
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
import {
|
|
3
3
|
appendMove,
|
|
4
4
|
resolveOrg
|
|
5
|
-
} from "../chunk-
|
|
5
|
+
} from "../chunk-C4UYMR7X.js";
|
|
6
6
|
import {
|
|
7
7
|
toCommitMove
|
|
8
8
|
} from "../chunk-7GHOFNJ2.js";
|
|
9
|
+
import "../chunk-26VA3ADF.js";
|
|
9
10
|
|
|
10
11
|
// src/hooks/post-commit.ts
|
|
11
12
|
import { execSync, spawn } from "child_process";
|
|
@@ -3,9 +3,6 @@ import {
|
|
|
3
3
|
bold,
|
|
4
4
|
color
|
|
5
5
|
} from "../chunk-4QJOQIY6.js";
|
|
6
|
-
import {
|
|
7
|
-
getClient
|
|
8
|
-
} from "../chunk-26VA3ADF.js";
|
|
9
6
|
import {
|
|
10
7
|
scrubFromCwd
|
|
11
8
|
} from "../chunk-6LAQVM26.js";
|
|
@@ -15,6 +12,9 @@ import {
|
|
|
15
12
|
import {
|
|
16
13
|
parseAgent
|
|
17
14
|
} from "../chunk-7YRBACIE.js";
|
|
15
|
+
import {
|
|
16
|
+
getClient
|
|
17
|
+
} from "../chunk-26VA3ADF.js";
|
|
18
18
|
|
|
19
19
|
// src/hooks/post-tool-use.ts
|
|
20
20
|
import { readFileSync } from "fs";
|
package/dist/hooks/pre-commit.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
getClient
|
|
4
|
-
} from "../chunk-26VA3ADF.js";
|
|
5
2
|
import {
|
|
6
3
|
parseAgent
|
|
7
4
|
} from "../chunk-7YRBACIE.js";
|
|
5
|
+
import {
|
|
6
|
+
getClient,
|
|
7
|
+
getSiteUrl
|
|
8
|
+
} from "../chunk-26VA3ADF.js";
|
|
8
9
|
import {
|
|
9
10
|
daemonRequest
|
|
10
11
|
} from "../chunk-UTKQTZHL.js";
|
|
@@ -199,7 +200,9 @@ function emit(output) {
|
|
|
199
200
|
async function checkOneFile(file) {
|
|
200
201
|
const fromDaemon = await daemonRequest(
|
|
201
202
|
"conflict_check",
|
|
202
|
-
|
|
203
|
+
// callerEnv lets a staging-bound daemon refuse a prod-context gate check
|
|
204
|
+
// (and vice versa) so we fall through to a direct call against our own env.
|
|
205
|
+
{ file, callerEnv: getSiteUrl() },
|
|
203
206
|
{ timeoutMs: DAEMON_TIMEOUT_MS }
|
|
204
207
|
);
|
|
205
208
|
if (fromDaemon) {
|
package/dist/hooks/prim-hook.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
appendMove,
|
|
4
4
|
resolveOrg
|
|
5
|
-
} from "../chunk-
|
|
5
|
+
} from "../chunk-C4UYMR7X.js";
|
|
6
6
|
import {
|
|
7
7
|
scrubFromCwd
|
|
8
8
|
} from "../chunk-6LAQVM26.js";
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import {
|
|
14
14
|
parseAgent
|
|
15
15
|
} from "../chunk-7YRBACIE.js";
|
|
16
|
+
import "../chunk-26VA3ADF.js";
|
|
16
17
|
|
|
17
18
|
// src/hooks/prim-hook.ts
|
|
18
19
|
import { spawn } from "child_process";
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
import {
|
|
3
3
|
parseAgent
|
|
4
4
|
} from "../chunk-7YRBACIE.js";
|
|
5
|
+
import {
|
|
6
|
+
getSiteUrl
|
|
7
|
+
} from "../chunk-26VA3ADF.js";
|
|
5
8
|
import {
|
|
6
9
|
daemonRequest
|
|
7
10
|
} from "../chunk-UTKQTZHL.js";
|
|
@@ -71,7 +74,9 @@ async function main() {
|
|
|
71
74
|
if (parseAgent(process.argv) === "codex") {
|
|
72
75
|
const snapshot = await daemonRequest(
|
|
73
76
|
"status_snapshot",
|
|
74
|
-
|
|
77
|
+
// callerEnv: a cross-env daemon withholds onlineCount, so a prod Codex
|
|
78
|
+
// session never gets a staging daemon's team count injected.
|
|
79
|
+
{ callerEnv: getSiteUrl() },
|
|
75
80
|
{ timeoutMs: DAEMON_TIMEOUT_MS }
|
|
76
81
|
);
|
|
77
82
|
if (snapshot && !snapshot.presenceStale && typeof snapshot.onlineCount === "number") {
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,15 @@ import {
|
|
|
10
10
|
checkAffectedDecisions,
|
|
11
11
|
daemonOrDirectGet,
|
|
12
12
|
formatDecisionsWarning
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-UWAJCF7P.js";
|
|
14
|
+
import {
|
|
15
|
+
JOURNAL_DIR,
|
|
16
|
+
SESSIONS_DIR,
|
|
17
|
+
bucketStats,
|
|
18
|
+
listBuckets,
|
|
19
|
+
listFlushing,
|
|
20
|
+
readMovesFromPath
|
|
21
|
+
} from "./chunk-C4UYMR7X.js";
|
|
14
22
|
import {
|
|
15
23
|
HttpError,
|
|
16
24
|
REFRESH_TOKEN_PATH,
|
|
@@ -22,13 +30,6 @@ import {
|
|
|
22
30
|
getTokenExpiresAt,
|
|
23
31
|
saveTokenExpiry
|
|
24
32
|
} from "./chunk-26VA3ADF.js";
|
|
25
|
-
import {
|
|
26
|
-
JOURNAL_DIR,
|
|
27
|
-
SESSIONS_DIR,
|
|
28
|
-
bucketStats,
|
|
29
|
-
listBuckets,
|
|
30
|
-
readMovesFromPath
|
|
31
|
-
} from "./chunk-JZGWQDM5.js";
|
|
32
33
|
import {
|
|
33
34
|
daemonIsLive,
|
|
34
35
|
daemonRequest
|
|
@@ -851,7 +852,7 @@ ${line("project", result.project)}`);
|
|
|
851
852
|
|
|
852
853
|
// src/commands/daemon.ts
|
|
853
854
|
import { spawn } from "child_process";
|
|
854
|
-
import { existsSync as existsSync4, readFileSync as readFileSync4, unlinkSync } from "fs";
|
|
855
|
+
import { closeSync as closeSync2, existsSync as existsSync4, mkdirSync as mkdirSync3, openSync as openSync2, readFileSync as readFileSync4, unlinkSync } from "fs";
|
|
855
856
|
import { homedir as homedir3 } from "os";
|
|
856
857
|
import { join as join4 } from "path";
|
|
857
858
|
|
|
@@ -871,8 +872,12 @@ function formatTeammates(names, cap) {
|
|
|
871
872
|
|
|
872
873
|
// src/commands/daemon.ts
|
|
873
874
|
var DAEMON_BIN = "prim-daemon-server";
|
|
874
|
-
var
|
|
875
|
-
var
|
|
875
|
+
var CONFIG_DIR = join4(homedir3(), ".config", "prim");
|
|
876
|
+
var PID_PATH = join4(CONFIG_DIR, "daemon.pid");
|
|
877
|
+
var SOCK_PATH = join4(CONFIG_DIR, "sock");
|
|
878
|
+
var LOG_PATH = join4(CONFIG_DIR, "daemon.log");
|
|
879
|
+
var CONFIG_DIR_MODE = 448;
|
|
880
|
+
var LOG_FILE_MODE = 384;
|
|
876
881
|
var STOP_TIMEOUT_MS = 5e3;
|
|
877
882
|
var STOP_POLL_MS = 100;
|
|
878
883
|
var STATUS_PROBE_TIMEOUT_MS = 500;
|
|
@@ -921,6 +926,12 @@ function spawnDaemon(options) {
|
|
|
921
926
|
const file = binFile(DAEMON_BIN);
|
|
922
927
|
return file ? spawn(process.execPath, [file], options) : spawn(DAEMON_BIN, [], options);
|
|
923
928
|
}
|
|
929
|
+
function openDaemonLog(configDir = CONFIG_DIR) {
|
|
930
|
+
if (!existsSync4(configDir)) {
|
|
931
|
+
mkdirSync3(configDir, { recursive: true, mode: CONFIG_DIR_MODE });
|
|
932
|
+
}
|
|
933
|
+
return openSync2(join4(configDir, "daemon.log"), "a", LOG_FILE_MODE);
|
|
934
|
+
}
|
|
924
935
|
async function waitForReady() {
|
|
925
936
|
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
926
937
|
while (Date.now() < deadline) {
|
|
@@ -949,8 +960,20 @@ async function daemonStart(opts) {
|
|
|
949
960
|
});
|
|
950
961
|
return;
|
|
951
962
|
}
|
|
952
|
-
|
|
963
|
+
let logFd;
|
|
964
|
+
try {
|
|
965
|
+
logFd = openDaemonLog();
|
|
966
|
+
} catch {
|
|
967
|
+
logFd = void 0;
|
|
968
|
+
}
|
|
969
|
+
const child = spawnDaemon({
|
|
970
|
+
detached: true,
|
|
971
|
+
stdio: logFd === void 0 ? ["ignore", "ignore", "ignore"] : ["ignore", logFd, logFd]
|
|
972
|
+
});
|
|
953
973
|
child.unref();
|
|
974
|
+
if (logFd !== void 0) {
|
|
975
|
+
closeSync2(logFd);
|
|
976
|
+
}
|
|
954
977
|
const live = await waitForReady();
|
|
955
978
|
if (live) {
|
|
956
979
|
const after = readPidfile();
|
|
@@ -962,7 +985,7 @@ async function daemonStart(opts) {
|
|
|
962
985
|
return;
|
|
963
986
|
}
|
|
964
987
|
process.stderr.write(
|
|
965
|
-
`[prim] \u2717 daemon start: spawned but the socket did not respond within ${READY_TIMEOUT_MS}ms (check that \`${DAEMON_BIN}\` resolves, and see
|
|
988
|
+
`[prim] \u2717 daemon start: spawned but the socket did not respond within ${READY_TIMEOUT_MS}ms (check that \`${DAEMON_BIN}\` resolves, and see ${LOG_PATH})
|
|
966
989
|
`
|
|
967
990
|
);
|
|
968
991
|
console.log(JSON.stringify({ started: false }, null, 2));
|
|
@@ -1860,9 +1883,124 @@ function registerDecisionsCommands(program2) {
|
|
|
1860
1883
|
});
|
|
1861
1884
|
}
|
|
1862
1885
|
|
|
1886
|
+
// src/commands/doctor.ts
|
|
1887
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1888
|
+
var DAEMON_PROBE_TIMEOUT_MS = 500;
|
|
1889
|
+
var CONNECTIVITY_TIMEOUT_MS = 3e3;
|
|
1890
|
+
var MS_PER_SECOND = 1e3;
|
|
1891
|
+
var STALE_PENDING_MS = 6e4;
|
|
1892
|
+
var EXIT_UNHEALTHY = 1;
|
|
1893
|
+
function classifyDoctor(checks) {
|
|
1894
|
+
const status = checks.some((c) => c.status === "fail") ? "fail" : checks.some((c) => c.status === "warn") ? "warn" : "ok";
|
|
1895
|
+
return {
|
|
1896
|
+
json: { ok: status !== "fail", status, checks },
|
|
1897
|
+
exitCode: status === "fail" ? EXIT_UNHEALTHY : 0
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
function checkAuth() {
|
|
1901
|
+
if (!getAuthToken()) {
|
|
1902
|
+
return { name: "auth", status: "fail", detail: "no token \u2014 run `prim auth login`" };
|
|
1903
|
+
}
|
|
1904
|
+
const expiresAt = getTokenExpiresAt();
|
|
1905
|
+
const hasRefresh = existsSync5(REFRESH_TOKEN_PATH);
|
|
1906
|
+
if (expiresAt !== void 0 && Date.now() >= expiresAt) {
|
|
1907
|
+
return hasRefresh ? { name: "auth", status: "warn", detail: "access token expired (refresh available)" } : {
|
|
1908
|
+
name: "auth",
|
|
1909
|
+
status: "fail",
|
|
1910
|
+
detail: "token expired, no refresh \u2014 run `prim auth login`"
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
if (!hasRefresh) {
|
|
1914
|
+
return { name: "auth", status: "warn", detail: "no refresh token \u2014 capture stops at expiry" };
|
|
1915
|
+
}
|
|
1916
|
+
const detail = expiresAt !== void 0 ? `valid (${String(Math.round((expiresAt - Date.now()) / MS_PER_SECOND))}s left)` : "valid";
|
|
1917
|
+
return { name: "auth", status: "ok", detail };
|
|
1918
|
+
}
|
|
1919
|
+
async function checkDaemon() {
|
|
1920
|
+
const live = await daemonIsLive(DAEMON_PROBE_TIMEOUT_MS);
|
|
1921
|
+
return live ? { name: "daemon", status: "ok", detail: "live" } : {
|
|
1922
|
+
name: "daemon",
|
|
1923
|
+
status: "warn",
|
|
1924
|
+
detail: "down \u2014 capture still journals; drains on next `prim` invocation"
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
function checkJournal() {
|
|
1928
|
+
const stats = bucketStats();
|
|
1929
|
+
const pending = stats.reduce((n, s) => n + s.lineCount, 0);
|
|
1930
|
+
if (pending === 0) {
|
|
1931
|
+
return { name: "journal", status: "ok", detail: "no pending moves" };
|
|
1932
|
+
}
|
|
1933
|
+
const oldestMs = Math.max(...stats.map((s) => Date.now() - s.mtimeMs));
|
|
1934
|
+
const oldestS = Math.round(oldestMs / MS_PER_SECOND);
|
|
1935
|
+
if (oldestMs > STALE_PENDING_MS) {
|
|
1936
|
+
return {
|
|
1937
|
+
name: "journal",
|
|
1938
|
+
status: "warn",
|
|
1939
|
+
detail: `${String(pending)} pending, oldest ${String(oldestS)}s \u2014 drain may be stalled`
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
return { name: "journal", status: "ok", detail: `${String(pending)} pending, draining` };
|
|
1943
|
+
}
|
|
1944
|
+
function checkStranded() {
|
|
1945
|
+
const stranded = listFlushing();
|
|
1946
|
+
if (stranded.length === 0) {
|
|
1947
|
+
return { name: "stranded", status: "ok", detail: "none" };
|
|
1948
|
+
}
|
|
1949
|
+
const moves = stranded.reduce((n, f) => n + f.lineCount, 0);
|
|
1950
|
+
return {
|
|
1951
|
+
name: "stranded",
|
|
1952
|
+
status: "warn",
|
|
1953
|
+
detail: `${String(moves)} move(s) in ${String(stranded.length)} file(s) \u2014 run \`prim moves flush\``
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
async function checkConnectivity() {
|
|
1957
|
+
try {
|
|
1958
|
+
await getClient().get("/api/cli/decisions/recent?limit=1", {
|
|
1959
|
+
signal: AbortSignal.timeout(CONNECTIVITY_TIMEOUT_MS)
|
|
1960
|
+
});
|
|
1961
|
+
return { name: "connectivity", status: "ok", detail: "server reachable" };
|
|
1962
|
+
} catch (err) {
|
|
1963
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1964
|
+
const status = message.includes("Authentication") ? "fail" : "warn";
|
|
1965
|
+
return { name: "connectivity", status, detail: message.slice(0, 80) };
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
async function collectChecks() {
|
|
1969
|
+
return [
|
|
1970
|
+
checkAuth(),
|
|
1971
|
+
await checkDaemon(),
|
|
1972
|
+
checkJournal(),
|
|
1973
|
+
checkStranded(),
|
|
1974
|
+
await checkConnectivity()
|
|
1975
|
+
];
|
|
1976
|
+
}
|
|
1977
|
+
function icon(status) {
|
|
1978
|
+
return status === "ok" ? "\u2713" : status === "warn" ? "\u26A0" : "\u2717";
|
|
1979
|
+
}
|
|
1980
|
+
async function runDoctor() {
|
|
1981
|
+
const checks = await collectChecks();
|
|
1982
|
+
const { json, exitCode } = classifyDoctor(checks);
|
|
1983
|
+
const headline = json.status === "ok" ? "\u2713 healthy" : json.status === "warn" ? "\u26A0 degraded" : "\u2717 unhealthy";
|
|
1984
|
+
process.stderr.write(`[prim] doctor: ${headline}
|
|
1985
|
+
`);
|
|
1986
|
+
for (const c of checks) {
|
|
1987
|
+
process.stderr.write(` ${icon(c.status)} ${c.name.padEnd(13)} ${c.detail}
|
|
1988
|
+
`);
|
|
1989
|
+
}
|
|
1990
|
+
console.log(JSON.stringify(json, null, 2));
|
|
1991
|
+
if (exitCode !== 0 && !process.exitCode) {
|
|
1992
|
+
process.exitCode = exitCode;
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
function registerDoctorCommands(program2) {
|
|
1996
|
+
program2.command("doctor").description("Check capture-pipeline health end to end (auth, daemon, journal, server)").action(async () => {
|
|
1997
|
+
await runDoctor();
|
|
1998
|
+
});
|
|
1999
|
+
}
|
|
2000
|
+
|
|
1863
2001
|
// src/commands/hooks.ts
|
|
1864
2002
|
import { execSync as execSync2 } from "child_process";
|
|
1865
|
-
import { existsSync as
|
|
2003
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
1866
2004
|
import { resolve } from "path";
|
|
1867
2005
|
import { Option } from "commander";
|
|
1868
2006
|
var PRE_COMMIT = { hookName: "pre-commit", binName: "prim-pre-commit" };
|
|
@@ -1905,11 +2043,11 @@ function getGitRoot() {
|
|
|
1905
2043
|
}
|
|
1906
2044
|
function detectHusky(gitRoot) {
|
|
1907
2045
|
const huskyDir = resolve(gitRoot, ".husky");
|
|
1908
|
-
if (!
|
|
1909
|
-
if (
|
|
1910
|
-
if (
|
|
2046
|
+
if (!existsSync6(huskyDir)) return false;
|
|
2047
|
+
if (existsSync6(resolve(huskyDir, "_"))) return true;
|
|
2048
|
+
if (existsSync6(resolve(huskyDir, "pre-commit"))) return true;
|
|
1911
2049
|
const pkgPath = resolve(gitRoot, "package.json");
|
|
1912
|
-
if (
|
|
2050
|
+
if (existsSync6(pkgPath)) {
|
|
1913
2051
|
try {
|
|
1914
2052
|
const pkg2 = JSON.parse(readFileSync5(pkgPath, "utf-8"));
|
|
1915
2053
|
const scripts = pkg2.scripts ?? {};
|
|
@@ -1938,7 +2076,7 @@ async function askConfirmation(question) {
|
|
|
1938
2076
|
}
|
|
1939
2077
|
function installToHusky(gitRoot, spec = PRE_COMMIT) {
|
|
1940
2078
|
const hookPath = resolve(gitRoot, ".husky", spec.hookName);
|
|
1941
|
-
if (
|
|
2079
|
+
if (existsSync6(hookPath)) {
|
|
1942
2080
|
const existing = readFileSync5(hookPath, "utf-8");
|
|
1943
2081
|
if (containsPrimHook(existing, spec.binName)) {
|
|
1944
2082
|
console.log(`Prim ${spec.hookName} hook is already installed in .husky/${spec.hookName}.`);
|
|
@@ -1963,10 +2101,10 @@ ${huskyBlock(spec)}
|
|
|
1963
2101
|
function installToDotGit(gitRoot, spec = PRE_COMMIT) {
|
|
1964
2102
|
const hooksDir = resolve(gitRoot, ".git", "hooks");
|
|
1965
2103
|
const hookPath = resolve(hooksDir, spec.hookName);
|
|
1966
|
-
if (!
|
|
1967
|
-
|
|
2104
|
+
if (!existsSync6(hooksDir)) {
|
|
2105
|
+
mkdirSync4(hooksDir, { recursive: true });
|
|
1968
2106
|
}
|
|
1969
|
-
if (
|
|
2107
|
+
if (existsSync6(hookPath)) {
|
|
1970
2108
|
const existing = readFileSync5(hookPath, "utf-8");
|
|
1971
2109
|
if (containsPrimHook(existing, spec.binName)) {
|
|
1972
2110
|
console.log(`Prim ${spec.hookName} hook is already installed at ${hookPath}.`);
|
|
@@ -2030,7 +2168,7 @@ function registerHooksCommands(program2) {
|
|
|
2030
2168
|
const gitRoot = getGitRoot();
|
|
2031
2169
|
for (const spec of HOOKS) {
|
|
2032
2170
|
const hookPath = resolve(gitRoot, ".git", "hooks", spec.hookName);
|
|
2033
|
-
if (!
|
|
2171
|
+
if (!existsSync6(hookPath)) {
|
|
2034
2172
|
console.log(`No ${spec.hookName} hook found.`);
|
|
2035
2173
|
continue;
|
|
2036
2174
|
}
|
|
@@ -2045,7 +2183,7 @@ function registerHooksCommands(program2) {
|
|
|
2045
2183
|
}
|
|
2046
2184
|
|
|
2047
2185
|
// src/commands/moves.ts
|
|
2048
|
-
import { existsSync as
|
|
2186
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
2049
2187
|
import { join as join5 } from "path";
|
|
2050
2188
|
|
|
2051
2189
|
// src/flusher.ts
|
|
@@ -2053,35 +2191,70 @@ import { renameSync as renameSync2, unlinkSync as unlinkSync3 } from "fs";
|
|
|
2053
2191
|
var BATCH_SIZE = 500;
|
|
2054
2192
|
var HTTP_TIMEOUT_MS = 1e4;
|
|
2055
2193
|
var OPPORTUNISTIC_FLUSH_AFTER_MS = 6e4;
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
if (err.code === "ENOENT") {
|
|
2062
|
-
return 0;
|
|
2063
|
-
}
|
|
2064
|
-
throw err;
|
|
2194
|
+
var ORPHAN_QUARANTINE_MS = 6e4;
|
|
2195
|
+
function batchMoves(moves, size = BATCH_SIZE) {
|
|
2196
|
+
const batches = [];
|
|
2197
|
+
for (let i = 0; i < moves.length; i += size) {
|
|
2198
|
+
batches.push(moves.slice(i, i + size));
|
|
2065
2199
|
}
|
|
2066
|
-
|
|
2200
|
+
return batches;
|
|
2201
|
+
}
|
|
2202
|
+
async function drainFlushingPath(flushingPath) {
|
|
2203
|
+
const moves = readMovesFromPath(flushingPath);
|
|
2067
2204
|
if (moves.length === 0) {
|
|
2068
|
-
unlinkSync3(
|
|
2205
|
+
unlinkSync3(flushingPath);
|
|
2069
2206
|
return 0;
|
|
2070
2207
|
}
|
|
2071
2208
|
const client = getClient();
|
|
2072
|
-
for (
|
|
2073
|
-
const batch = moves.slice(i, i + BATCH_SIZE);
|
|
2209
|
+
for (const batch of batchMoves(moves)) {
|
|
2074
2210
|
await client.post(
|
|
2075
2211
|
"/api/cli/moves/ingest",
|
|
2076
2212
|
{ batch },
|
|
2077
2213
|
{ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS) }
|
|
2078
2214
|
);
|
|
2079
2215
|
}
|
|
2080
|
-
unlinkSync3(
|
|
2216
|
+
unlinkSync3(flushingPath);
|
|
2081
2217
|
return moves.length;
|
|
2082
2218
|
}
|
|
2083
|
-
async function
|
|
2219
|
+
async function drainPath(path) {
|
|
2220
|
+
const tmpPath = `${path}.flushing.${String(Date.now())}.${String(process.pid)}`;
|
|
2221
|
+
try {
|
|
2222
|
+
renameSync2(path, tmpPath);
|
|
2223
|
+
} catch (err) {
|
|
2224
|
+
if (err.code === "ENOENT") {
|
|
2225
|
+
return 0;
|
|
2226
|
+
}
|
|
2227
|
+
throw err;
|
|
2228
|
+
}
|
|
2229
|
+
return drainFlushingPath(tmpPath);
|
|
2230
|
+
}
|
|
2231
|
+
function processIsAlive2(pid) {
|
|
2232
|
+
try {
|
|
2233
|
+
process.kill(pid, 0);
|
|
2234
|
+
return true;
|
|
2235
|
+
} catch {
|
|
2236
|
+
return false;
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
function selectRecoverable(files, now, opts = {}) {
|
|
2240
|
+
const quarantineMs = opts.quarantineMs ?? ORPHAN_QUARANTINE_MS;
|
|
2241
|
+
const isAlive = opts.isAlive ?? processIsAlive2;
|
|
2242
|
+
return files.filter(
|
|
2243
|
+
(f) => f.pid === void 0 ? now - f.mtimeMs > quarantineMs : !isAlive(f.pid)
|
|
2244
|
+
);
|
|
2245
|
+
}
|
|
2246
|
+
async function recoverOrphans() {
|
|
2084
2247
|
let total = 0;
|
|
2248
|
+
for (const file of selectRecoverable(listFlushing(), Date.now())) {
|
|
2249
|
+
try {
|
|
2250
|
+
total += await drainFlushingPath(file.path);
|
|
2251
|
+
} catch {
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
return total;
|
|
2255
|
+
}
|
|
2256
|
+
async function flush() {
|
|
2257
|
+
let total = await recoverOrphans();
|
|
2085
2258
|
for (const { path } of listBuckets()) {
|
|
2086
2259
|
total += await drainPath(path);
|
|
2087
2260
|
}
|
|
@@ -2102,7 +2275,7 @@ async function flushIfNeeded() {
|
|
|
2102
2275
|
}
|
|
2103
2276
|
|
|
2104
2277
|
// src/commands/moves.ts
|
|
2105
|
-
var
|
|
2278
|
+
var MS_PER_SECOND2 = 1e3;
|
|
2106
2279
|
var DEFAULT_TAIL_LINES = "20";
|
|
2107
2280
|
var RADIX_DECIMAL = 10;
|
|
2108
2281
|
var ID_PREFIX_LEN = 8;
|
|
@@ -2120,17 +2293,32 @@ function registerMovesCommands(program2) {
|
|
|
2120
2293
|
});
|
|
2121
2294
|
moves.command("status").description("Show per-bucket pending stats").action(() => {
|
|
2122
2295
|
const stats = bucketStats();
|
|
2123
|
-
|
|
2296
|
+
const stranded = listFlushing();
|
|
2297
|
+
if (stats.length === 0 && stranded.length === 0) {
|
|
2124
2298
|
console.log("[prim] journal: empty");
|
|
2125
2299
|
return;
|
|
2126
2300
|
}
|
|
2127
2301
|
console.log(`[prim] root: ${JOURNAL_DIR}`);
|
|
2128
2302
|
for (const s of stats) {
|
|
2129
|
-
const ageS = Math.round((Date.now() - s.mtimeMs) /
|
|
2303
|
+
const ageS = Math.round((Date.now() - s.mtimeMs) / MS_PER_SECOND2);
|
|
2130
2304
|
console.log(
|
|
2131
2305
|
` ${s.bucket.padEnd(BUCKET_COL_WIDTH)} ${String(s.lineCount).padStart(5)} pending, ${String(s.sizeBytes).padStart(8)} bytes, last write ${String(ageS)}s ago`
|
|
2132
2306
|
);
|
|
2133
2307
|
}
|
|
2308
|
+
if (stranded.length > 0) {
|
|
2309
|
+
const moveCount = stranded.reduce((n, f) => n + f.lineCount, 0);
|
|
2310
|
+
const byteCount = stranded.reduce((n, f) => n + f.sizeBytes, 0);
|
|
2311
|
+
console.log(
|
|
2312
|
+
`[prim] \u26A0 ${String(stranded.length)} stranded flush file(s): ${String(moveCount)} move(s), ${String(byteCount)} bytes \u2014 recover with \`prim moves flush\``
|
|
2313
|
+
);
|
|
2314
|
+
for (const f of stranded) {
|
|
2315
|
+
const ageS = Math.round((Date.now() - f.mtimeMs) / MS_PER_SECOND2);
|
|
2316
|
+
const owner = f.pid === void 0 ? "no pid" : `pid ${String(f.pid)}`;
|
|
2317
|
+
console.log(
|
|
2318
|
+
` ${f.bucket.padEnd(BUCKET_COL_WIDTH)} ${String(f.lineCount).padStart(5)} stranded, ${String(f.sizeBytes).padStart(8)} bytes, ${String(ageS)}s ago (${owner})`
|
|
2319
|
+
);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2134
2322
|
});
|
|
2135
2323
|
moves.command("tail").description("Pretty-print recent journal entries across all buckets").option("-n, --lines <n>", "number of lines to tail", DEFAULT_TAIL_LINES).action((opts) => {
|
|
2136
2324
|
const lines = Number.parseInt(opts.lines, RADIX_DECIMAL);
|
|
@@ -2156,8 +2344,8 @@ function registerMovesCommands(program2) {
|
|
|
2156
2344
|
});
|
|
2157
2345
|
moves.command("bind").description("Pin the current directory to an org via .prim/workspace.json").requiredOption("--orgId <orgId>", "Convex organization id").action((opts) => {
|
|
2158
2346
|
const dir = join5(process.cwd(), ".prim");
|
|
2159
|
-
if (!
|
|
2160
|
-
|
|
2347
|
+
if (!existsSync7(dir)) {
|
|
2348
|
+
mkdirSync5(dir, { recursive: true, mode: DIR_MODE });
|
|
2161
2349
|
}
|
|
2162
2350
|
const file = join5(process.cwd(), WORKSPACE_FILE);
|
|
2163
2351
|
writeFileSync4(file, JSON.stringify({ orgId: opts.orgId, boundAt: Date.now() }, null, 2), {
|
|
@@ -2167,7 +2355,7 @@ function registerMovesCommands(program2) {
|
|
|
2167
2355
|
});
|
|
2168
2356
|
moves.command("drop").description("Remove the .prim/workspace.json binding from the cwd").action(() => {
|
|
2169
2357
|
const file = join5(process.cwd(), WORKSPACE_FILE);
|
|
2170
|
-
if (!
|
|
2358
|
+
if (!existsSync7(file)) {
|
|
2171
2359
|
console.log("[prim] no workspace binding in cwd");
|
|
2172
2360
|
return;
|
|
2173
2361
|
}
|
|
@@ -2260,8 +2448,8 @@ function registerReconcileCommands(program2) {
|
|
|
2260
2448
|
|
|
2261
2449
|
// src/commands/session.ts
|
|
2262
2450
|
import {
|
|
2263
|
-
existsSync as
|
|
2264
|
-
mkdirSync as
|
|
2451
|
+
existsSync as existsSync8,
|
|
2452
|
+
mkdirSync as mkdirSync6,
|
|
2265
2453
|
readFileSync as readFileSync6,
|
|
2266
2454
|
readdirSync,
|
|
2267
2455
|
unlinkSync as unlinkSync5,
|
|
@@ -2271,8 +2459,8 @@ import { join as join6 } from "path";
|
|
|
2271
2459
|
var DIR_MODE2 = 448;
|
|
2272
2460
|
var FILE_MODE3 = 384;
|
|
2273
2461
|
function ensureDir() {
|
|
2274
|
-
if (!
|
|
2275
|
-
|
|
2462
|
+
if (!existsSync8(SESSIONS_DIR)) {
|
|
2463
|
+
mkdirSync6(SESSIONS_DIR, { recursive: true, mode: DIR_MODE2 });
|
|
2276
2464
|
}
|
|
2277
2465
|
}
|
|
2278
2466
|
function markerPath(sessionId) {
|
|
@@ -2292,7 +2480,7 @@ function registerSessionCommands(program2) {
|
|
|
2292
2480
|
console.log(`[prim] session ${sessionId} bound to org ${opts.orgId}`);
|
|
2293
2481
|
});
|
|
2294
2482
|
session.command("list").description("List active session markers").action(() => {
|
|
2295
|
-
if (!
|
|
2483
|
+
if (!existsSync8(SESSIONS_DIR)) {
|
|
2296
2484
|
console.log("[prim] no session markers");
|
|
2297
2485
|
return;
|
|
2298
2486
|
}
|
|
@@ -2312,7 +2500,7 @@ function registerSessionCommands(program2) {
|
|
|
2312
2500
|
});
|
|
2313
2501
|
session.command("drop <sessionId>").description("Remove a session marker").action((sessionId) => {
|
|
2314
2502
|
const p = markerPath(sessionId);
|
|
2315
|
-
if (!
|
|
2503
|
+
if (!existsSync8(p)) {
|
|
2316
2504
|
console.log(`[prim] no marker for session ${sessionId}`);
|
|
2317
2505
|
return;
|
|
2318
2506
|
}
|
|
@@ -2413,10 +2601,10 @@ function registerSetupCommand(program2) {
|
|
|
2413
2601
|
|
|
2414
2602
|
// src/commands/skill.ts
|
|
2415
2603
|
import {
|
|
2416
|
-
closeSync as
|
|
2417
|
-
existsSync as
|
|
2604
|
+
closeSync as closeSync3,
|
|
2605
|
+
existsSync as existsSync9,
|
|
2418
2606
|
fsyncSync as fsyncSync2,
|
|
2419
|
-
openSync as
|
|
2607
|
+
openSync as openSync3,
|
|
2420
2608
|
readFileSync as readFileSync7,
|
|
2421
2609
|
renameSync as renameSync3,
|
|
2422
2610
|
writeFileSync as writeFileSync6
|
|
@@ -2439,13 +2627,13 @@ function loadSkill() {
|
|
|
2439
2627
|
let dir = __dirname;
|
|
2440
2628
|
while (dir !== dirname4(dir)) {
|
|
2441
2629
|
const p = resolve2(dir, "SKILL.md");
|
|
2442
|
-
if (
|
|
2630
|
+
if (existsSync9(p)) return readFileSync7(p, "utf-8");
|
|
2443
2631
|
dir = dirname4(dir);
|
|
2444
2632
|
}
|
|
2445
2633
|
throw new Error("SKILL.md not found in package");
|
|
2446
2634
|
}
|
|
2447
2635
|
function detectTargets(cwd) {
|
|
2448
|
-
return TARGET_CANDIDATES.filter((p) =>
|
|
2636
|
+
return TARGET_CANDIDATES.filter((p) => existsSync9(resolve2(cwd, p)));
|
|
2449
2637
|
}
|
|
2450
2638
|
function detectNewline(content) {
|
|
2451
2639
|
return content.includes("\r\n") ? "\r\n" : "\n";
|
|
@@ -2474,11 +2662,11 @@ function removeBlock(existing) {
|
|
|
2474
2662
|
function atomicWrite2(target, content) {
|
|
2475
2663
|
const tmp = `${target}.tmp`;
|
|
2476
2664
|
writeFileSync6(tmp, content);
|
|
2477
|
-
const fd =
|
|
2665
|
+
const fd = openSync3(tmp, "r+");
|
|
2478
2666
|
try {
|
|
2479
2667
|
fsyncSync2(fd);
|
|
2480
2668
|
} finally {
|
|
2481
|
-
|
|
2669
|
+
closeSync3(fd);
|
|
2482
2670
|
}
|
|
2483
2671
|
renameSync3(tmp, target);
|
|
2484
2672
|
}
|
|
@@ -2494,7 +2682,7 @@ function resolveTarget(cwd, override) {
|
|
|
2494
2682
|
function runInstall(cwd, opts) {
|
|
2495
2683
|
const target = resolveTarget(cwd, opts.target);
|
|
2496
2684
|
if (target === null) return 1;
|
|
2497
|
-
const existing =
|
|
2685
|
+
const existing = existsSync9(target) ? readFileSync7(target, "utf-8") : "";
|
|
2498
2686
|
const eol = existing ? detectNewline(existing) : "\n";
|
|
2499
2687
|
const block = composeBlock(loadSkill(), eol);
|
|
2500
2688
|
const next = applyBlock(existing, block, eol);
|
|
@@ -2513,7 +2701,7 @@ function runInstall(cwd, opts) {
|
|
|
2513
2701
|
function runUninstall(cwd, opts) {
|
|
2514
2702
|
const target = resolveTarget(cwd, opts.target);
|
|
2515
2703
|
if (target === null) return 1;
|
|
2516
|
-
if (!
|
|
2704
|
+
if (!existsSync9(target)) {
|
|
2517
2705
|
console.log(`Skill block not present at ${target}`);
|
|
2518
2706
|
return 0;
|
|
2519
2707
|
}
|
|
@@ -2530,7 +2718,7 @@ function runUninstall(cwd, opts) {
|
|
|
2530
2718
|
function runStatus(cwd, opts) {
|
|
2531
2719
|
const target = resolveTarget(cwd, opts.target);
|
|
2532
2720
|
if (target === null) return 1;
|
|
2533
|
-
const fileExists =
|
|
2721
|
+
const fileExists = existsSync9(target);
|
|
2534
2722
|
let installed = false;
|
|
2535
2723
|
if (fileExists) {
|
|
2536
2724
|
const content = readFileSync7(target, "utf-8");
|
|
@@ -2602,13 +2790,18 @@ async function renderStatusline() {
|
|
|
2602
2790
|
const version = readPackageVersion();
|
|
2603
2791
|
const snapshot = await daemonRequest(
|
|
2604
2792
|
"status_snapshot",
|
|
2605
|
-
|
|
2793
|
+
// callerEnv lets the daemon withhold presence when it is bound to a different
|
|
2794
|
+
// deployment than this statusline targets.
|
|
2795
|
+
{ callerEnv: getSiteUrl() },
|
|
2606
2796
|
{ timeoutMs: STATUSLINE_TIMEOUT_MS }
|
|
2607
2797
|
);
|
|
2608
2798
|
if (!snapshot) {
|
|
2609
2799
|
debug("daemon snapshot missing");
|
|
2610
2800
|
return `primitive ${version} (daemon: down)`;
|
|
2611
2801
|
}
|
|
2802
|
+
if (snapshot.envMismatch) {
|
|
2803
|
+
return `primitive ${version} (daemon: live \xB7 presence: other env)`;
|
|
2804
|
+
}
|
|
2612
2805
|
if (snapshot.presenceStale) {
|
|
2613
2806
|
return `primitive ${version} (daemon: live \xB7 presence: stale)`;
|
|
2614
2807
|
}
|
|
@@ -2760,6 +2953,7 @@ registerDecisionsCommands(program);
|
|
|
2760
2953
|
registerClaudeCommands(program);
|
|
2761
2954
|
registerCodexCommands(program);
|
|
2762
2955
|
registerDaemonCommands(program);
|
|
2956
|
+
registerDoctorCommands(program);
|
|
2763
2957
|
registerReconcileCommands(program);
|
|
2764
2958
|
registerStatuslineCommands(program);
|
|
2765
2959
|
registerWelcomeCommand(program);
|
package/package.json
CHANGED