@primitive.ai/prim 0.1.0-alpha.44 → 0.1.0-alpha.46
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/SKILL.md +91 -6
- package/dist/{chunk-OKUXEBPT.js → chunk-ESLX7ISX.js} +1 -1
- package/dist/{chunk-UISU3A7W.js → chunk-JT4Q333I.js} +1 -1
- package/dist/chunk-VQ2ZV2D5.js +502 -0
- package/dist/{chunk-BSZGI5RL.js → chunk-Z4VI6TNX.js} +1 -1
- package/dist/daemon/server.js +520 -207
- package/dist/hooks/post-commit.js +2 -2
- package/dist/hooks/post-tool-use.js +2 -2
- package/dist/hooks/pre-commit.js +2 -2
- package/dist/hooks/pre-tool-use.js +1 -1
- package/dist/hooks/prim-hook.js +3 -3
- package/dist/hooks/session-start.js +2 -2
- package/dist/index.js +339 -286
- package/dist/statusline-main.js +45 -37
- package/package.json +1 -1
- package/dist/chunk-W6PAKEDO.js +0 -211
package/dist/daemon/server.js
CHANGED
|
@@ -1,44 +1,211 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/daemon/server.ts
|
|
4
|
-
import { existsSync as
|
|
4
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, unlinkSync as unlinkSync3 } from "fs";
|
|
5
5
|
import { createServer } from "net";
|
|
6
6
|
import { homedir as homedir5 } from "os";
|
|
7
|
-
import { join as
|
|
7
|
+
import { join as join7 } from "path";
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
9
9
|
|
|
10
10
|
// src/client.ts
|
|
11
|
-
import {
|
|
11
|
+
import { createHash, randomBytes as randomBytes2 } from "crypto";
|
|
12
|
+
import {
|
|
13
|
+
chmodSync,
|
|
14
|
+
existsSync as existsSync2,
|
|
15
|
+
mkdirSync as mkdirSync2,
|
|
16
|
+
readFileSync as readFileSync2,
|
|
17
|
+
renameSync,
|
|
18
|
+
rmSync as rmSync2,
|
|
19
|
+
writeFileSync as writeFileSync2
|
|
20
|
+
} from "fs";
|
|
12
21
|
import { homedir } from "os";
|
|
13
|
-
import { join, resolve } from "path";
|
|
22
|
+
import { dirname as dirname2, join as join2, resolve } from "path";
|
|
23
|
+
|
|
24
|
+
// src/lib/file-lock.ts
|
|
25
|
+
import { randomBytes } from "crypto";
|
|
26
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
|
27
|
+
import { dirname, join } from "path";
|
|
28
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
29
|
+
var DEFAULT_POLL_MS = 50;
|
|
30
|
+
var DEFAULT_INIT_GRACE_MS = 2e3;
|
|
31
|
+
var DIRECTORY_MODE = 448;
|
|
32
|
+
var FILE_MODE = 384;
|
|
33
|
+
function ownerPath(lockDir) {
|
|
34
|
+
return join(lockDir, "owner.json");
|
|
35
|
+
}
|
|
36
|
+
function readOwner(lockDir) {
|
|
37
|
+
try {
|
|
38
|
+
const owner = JSON.parse(readFileSync(ownerPath(lockDir), "utf8"));
|
|
39
|
+
if (!Number.isInteger(owner.pid) || owner.pid <= 0 || typeof owner.nonce !== "string" || typeof owner.createdAt !== "number") {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return owner;
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function sameOwner(a, b) {
|
|
48
|
+
return a?.pid === b.pid && a.nonce === b.nonce && a.createdAt === b.createdAt;
|
|
49
|
+
}
|
|
50
|
+
function tryTakeLock(lockDir, now) {
|
|
51
|
+
try {
|
|
52
|
+
mkdirSync(lockDir, { mode: DIRECTORY_MODE });
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error.code === "EEXIST") return null;
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
const owner = {
|
|
58
|
+
pid: process.pid,
|
|
59
|
+
nonce: randomBytes(16).toString("hex"),
|
|
60
|
+
createdAt: now
|
|
61
|
+
};
|
|
62
|
+
try {
|
|
63
|
+
writeFileSync(ownerPath(lockDir), `${JSON.stringify(owner)}
|
|
64
|
+
`, {
|
|
65
|
+
encoding: "utf8",
|
|
66
|
+
mode: FILE_MODE,
|
|
67
|
+
flag: "wx"
|
|
68
|
+
});
|
|
69
|
+
return owner;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
rmSync(lockDir, { recursive: true, force: true });
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function processIsAlive(pid) {
|
|
76
|
+
try {
|
|
77
|
+
process.kill(pid, 0);
|
|
78
|
+
return true;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return error.code === "EPERM";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function recoverStaleLock(lockDir, now, alive, initGraceMs) {
|
|
84
|
+
const owner = readOwner(lockDir);
|
|
85
|
+
if (owner) {
|
|
86
|
+
if (alive(owner.pid)) return false;
|
|
87
|
+
if (!sameOwner(readOwner(lockDir), owner)) return false;
|
|
88
|
+
rmSync(lockDir, { recursive: true, force: true });
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const before = statSync(lockDir).mtimeMs;
|
|
93
|
+
if (now - before < initGraceMs) return false;
|
|
94
|
+
if (readOwner(lockDir)) return false;
|
|
95
|
+
if (statSync(lockDir).mtimeMs !== before) return false;
|
|
96
|
+
rmSync(lockDir, { recursive: true, force: true });
|
|
97
|
+
return true;
|
|
98
|
+
} catch {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function abortError(signal) {
|
|
103
|
+
if (signal.reason instanceof Error) return signal.reason;
|
|
104
|
+
const error = new Error("The operation was aborted");
|
|
105
|
+
error.name = "AbortError";
|
|
106
|
+
return error;
|
|
107
|
+
}
|
|
108
|
+
function throwIfAborted(signal) {
|
|
109
|
+
if (signal?.aborted) throw abortError(signal);
|
|
110
|
+
}
|
|
111
|
+
async function defaultSleep(ms, signal) {
|
|
112
|
+
if (!signal) {
|
|
113
|
+
await new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
throwIfAborted(signal);
|
|
117
|
+
await new Promise((resolve2, reject) => {
|
|
118
|
+
const timer = setTimeout(() => {
|
|
119
|
+
signal.removeEventListener("abort", onAbort);
|
|
120
|
+
resolve2();
|
|
121
|
+
}, ms);
|
|
122
|
+
const onAbort = () => {
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
reject(abortError(signal));
|
|
125
|
+
};
|
|
126
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
async function withFileLock(lockDir, operation, options = {}) {
|
|
130
|
+
mkdirSync(dirname(lockDir), { recursive: true, mode: DIRECTORY_MODE });
|
|
131
|
+
const nowMs = options.nowMs ?? Date.now;
|
|
132
|
+
const alive = options.processAlive ?? processIsAlive;
|
|
133
|
+
const deadline = nowMs() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
134
|
+
let owner = null;
|
|
135
|
+
while (!owner) {
|
|
136
|
+
throwIfAborted(options.signal);
|
|
137
|
+
const now = nowMs();
|
|
138
|
+
owner = tryTakeLock(lockDir, now);
|
|
139
|
+
if (owner) break;
|
|
140
|
+
recoverStaleLock(lockDir, now, alive, options.initGraceMs ?? DEFAULT_INIT_GRACE_MS);
|
|
141
|
+
if (now >= deadline) {
|
|
142
|
+
throw new Error(`timed out waiting for file lock ${lockDir}`);
|
|
143
|
+
}
|
|
144
|
+
if (options.sleep) {
|
|
145
|
+
await options.sleep(options.pollMs ?? DEFAULT_POLL_MS);
|
|
146
|
+
throwIfAborted(options.signal);
|
|
147
|
+
} else {
|
|
148
|
+
await defaultSleep(options.pollMs ?? DEFAULT_POLL_MS, options.signal);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
throwIfAborted(options.signal);
|
|
153
|
+
return await operation();
|
|
154
|
+
} finally {
|
|
155
|
+
if (sameOwner(readOwner(lockDir), owner)) {
|
|
156
|
+
rmSync(lockDir, { recursive: true, force: true });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/client.ts
|
|
162
|
+
var CONFIG_DIR_MODE = 448;
|
|
163
|
+
var CREDENTIAL_FILE_MODE = 384;
|
|
164
|
+
var REFRESH_THRESHOLD_MS = 6e4;
|
|
165
|
+
var DEFAULT_API_URL = "https://api.getprimitive.ai";
|
|
14
166
|
function loadEnvFile() {
|
|
15
167
|
const envVars = {};
|
|
16
|
-
const
|
|
17
|
-
for (const file of candidates) {
|
|
168
|
+
for (const file of [".env.local", ".env"]) {
|
|
18
169
|
const filePath = resolve(process.cwd(), file);
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const value = trimmed.slice(eqIdx + 1).trim();
|
|
28
|
-
envVars[key] = value;
|
|
29
|
-
}
|
|
170
|
+
if (!existsSync2(filePath)) continue;
|
|
171
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
172
|
+
for (const line of content.split("\n")) {
|
|
173
|
+
const trimmed = line.trim();
|
|
174
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
175
|
+
const eqIdx = trimmed.indexOf("=");
|
|
176
|
+
if (eqIdx === -1) continue;
|
|
177
|
+
envVars[trimmed.slice(0, eqIdx).trim()] = trimmed.slice(eqIdx + 1).trim();
|
|
30
178
|
}
|
|
31
179
|
}
|
|
32
180
|
return envVars;
|
|
33
181
|
}
|
|
34
|
-
var TOKEN_FILE_PATH =
|
|
35
|
-
var REFRESH_TOKEN_PATH = TOKEN_FILE_PATH
|
|
36
|
-
var TOKEN_EXPIRES_PATH =
|
|
37
|
-
var
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
182
|
+
var TOKEN_FILE_PATH = join2(homedir(), ".config", "prim", "token");
|
|
183
|
+
var REFRESH_TOKEN_PATH = join2(dirname2(TOKEN_FILE_PATH), "refresh_token");
|
|
184
|
+
var TOKEN_EXPIRES_PATH = join2(dirname2(TOKEN_FILE_PATH), "token_expires_at");
|
|
185
|
+
var TERMINAL_REFRESH_PATH = join2(dirname2(TOKEN_FILE_PATH), "refresh_terminal");
|
|
186
|
+
var CREDENTIAL_LOCK_PATH = join2(dirname2(TOKEN_FILE_PATH), "credentials.lock");
|
|
187
|
+
function readTrimmed(path) {
|
|
188
|
+
try {
|
|
189
|
+
const value = readFileSync2(path, "utf8").trim();
|
|
190
|
+
return value || void 0;
|
|
191
|
+
} catch {
|
|
192
|
+
return void 0;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function resolveAuthCredential() {
|
|
196
|
+
if (process.env.PRIM_TOKEN) {
|
|
197
|
+
return { token: process.env.PRIM_TOKEN, source: "environment" };
|
|
198
|
+
}
|
|
199
|
+
const stored = readTrimmed(TOKEN_FILE_PATH);
|
|
200
|
+
if (stored) {
|
|
201
|
+
return { token: stored, source: "token_file" };
|
|
202
|
+
}
|
|
203
|
+
const envToken = loadEnvFile().PRIM_TOKEN;
|
|
204
|
+
return envToken ? { token: envToken, source: "env_file" } : void 0;
|
|
205
|
+
}
|
|
206
|
+
function getSiteUrl() {
|
|
207
|
+
if (process.env.PRIM_API_URL) return process.env.PRIM_API_URL;
|
|
208
|
+
return loadEnvFile().PRIM_API_URL ?? DEFAULT_API_URL;
|
|
42
209
|
}
|
|
43
210
|
function getJwtExpiry(token) {
|
|
44
211
|
const parts = token.split(".");
|
|
@@ -50,93 +217,169 @@ function getJwtExpiry(token) {
|
|
|
50
217
|
return void 0;
|
|
51
218
|
}
|
|
52
219
|
}
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
220
|
+
function expiresAtFor(token, expiresIn) {
|
|
221
|
+
return expiresIn === void 0 ? getJwtExpiry(token) : Date.now() + expiresIn * 1e3;
|
|
222
|
+
}
|
|
223
|
+
function ensureConfigDirectory() {
|
|
224
|
+
const directory = dirname2(TOKEN_FILE_PATH);
|
|
225
|
+
mkdirSync2(directory, { recursive: true, mode: CONFIG_DIR_MODE });
|
|
226
|
+
chmodSync(directory, CONFIG_DIR_MODE);
|
|
227
|
+
}
|
|
228
|
+
function atomicWrite(path, content) {
|
|
229
|
+
ensureConfigDirectory();
|
|
230
|
+
const temp = join2(
|
|
231
|
+
dirname2(path),
|
|
232
|
+
`.${path.slice(dirname2(path).length + 1)}.tmp-${process.pid}-${randomBytes2(8).toString("hex")}`
|
|
233
|
+
);
|
|
234
|
+
try {
|
|
235
|
+
writeFileSync2(temp, content, {
|
|
236
|
+
encoding: "utf8",
|
|
237
|
+
mode: CREDENTIAL_FILE_MODE,
|
|
238
|
+
flag: "wx"
|
|
239
|
+
});
|
|
240
|
+
renameSync(temp, path);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
rmSync2(temp, { force: true });
|
|
243
|
+
throw error;
|
|
57
244
|
}
|
|
58
245
|
}
|
|
246
|
+
function removeCredentialFile(path) {
|
|
247
|
+
if (!existsSync2(path)) return false;
|
|
248
|
+
rmSync2(path, { force: true });
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
59
251
|
function getTokenExpiresAt() {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
252
|
+
const stored = readTrimmed(TOKEN_EXPIRES_PATH);
|
|
253
|
+
if (stored === void 0) return void 0;
|
|
254
|
+
const value = Number(stored);
|
|
255
|
+
return Number.isNaN(value) ? void 0 : value;
|
|
63
256
|
}
|
|
64
|
-
function
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (existsSync(TOKEN_FILE_PATH)) {
|
|
69
|
-
const token = readFileSync(TOKEN_FILE_PATH, "utf-8").trim();
|
|
70
|
-
if (token) {
|
|
71
|
-
return token;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
const envVars = loadEnvFile();
|
|
75
|
-
if (envVars.PRIM_TOKEN) {
|
|
76
|
-
return envVars.PRIM_TOKEN;
|
|
77
|
-
}
|
|
78
|
-
return void 0;
|
|
257
|
+
function isTokenExpiringSoon(credential) {
|
|
258
|
+
if (credential.source !== "token_file") return false;
|
|
259
|
+
const expiresAt = getTokenExpiresAt();
|
|
260
|
+
return expiresAt !== void 0 && Date.now() >= expiresAt - REFRESH_THRESHOLD_MS;
|
|
79
261
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (process.env.PRIM_API_URL) {
|
|
83
|
-
return process.env.PRIM_API_URL;
|
|
84
|
-
}
|
|
85
|
-
const envVars = loadEnvFile();
|
|
86
|
-
if (envVars.PRIM_API_URL) {
|
|
87
|
-
return envVars.PRIM_API_URL;
|
|
88
|
-
}
|
|
89
|
-
return DEFAULT_API_URL;
|
|
262
|
+
function refreshFingerprint(refreshToken2) {
|
|
263
|
+
return createHash("sha256").update(refreshToken2).digest("hex");
|
|
90
264
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
265
|
+
function terminalFingerprint() {
|
|
266
|
+
return readTrimmed(TERMINAL_REFRESH_PATH);
|
|
267
|
+
}
|
|
268
|
+
function writeTerminalFingerprint(refreshToken2) {
|
|
269
|
+
atomicWrite(TERMINAL_REFRESH_PATH, `${refreshFingerprint(refreshToken2)}
|
|
270
|
+
`);
|
|
271
|
+
}
|
|
272
|
+
function clearTerminalFingerprint() {
|
|
273
|
+
removeCredentialFile(TERMINAL_REFRESH_PATH);
|
|
274
|
+
}
|
|
275
|
+
function isSessionEnded() {
|
|
276
|
+
if (resolveAuthCredential()?.source !== "token_file") return false;
|
|
277
|
+
const refreshToken2 = readTrimmed(REFRESH_TOKEN_PATH);
|
|
278
|
+
const ended = terminalFingerprint();
|
|
279
|
+
return Boolean(refreshToken2 && ended && refreshFingerprint(refreshToken2) === ended);
|
|
280
|
+
}
|
|
281
|
+
function withCredentialLock(operation, options = {}) {
|
|
282
|
+
return withFileLock(CREDENTIAL_LOCK_PATH, operation, options);
|
|
283
|
+
}
|
|
284
|
+
function commitCredentialsUnlocked(credentials) {
|
|
285
|
+
const accessToken = credentials.accessToken.trim();
|
|
286
|
+
const refreshToken2 = credentials.refreshToken.trim();
|
|
287
|
+
if (!accessToken || !refreshToken2) {
|
|
288
|
+
throw new Error("OAuth credentials require both access and refresh tokens");
|
|
94
289
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
290
|
+
atomicWrite(REFRESH_TOKEN_PATH, `${refreshToken2}
|
|
291
|
+
`);
|
|
292
|
+
const expiresAt = expiresAtFor(accessToken, credentials.expiresIn);
|
|
293
|
+
if (expiresAt === void 0) removeCredentialFile(TOKEN_EXPIRES_PATH);
|
|
294
|
+
else atomicWrite(TOKEN_EXPIRES_PATH, `${expiresAt}
|
|
295
|
+
`);
|
|
296
|
+
atomicWrite(TOKEN_FILE_PATH, `${accessToken}
|
|
297
|
+
`);
|
|
298
|
+
clearTerminalFingerprint();
|
|
299
|
+
_cachedCredential = { token: accessToken, source: "token_file" };
|
|
300
|
+
}
|
|
301
|
+
function isTerminalRefreshResponse(response, detail) {
|
|
302
|
+
if (detail.includes("invalid_grant") || detail.includes("Session has already ended")) return true;
|
|
303
|
+
try {
|
|
304
|
+
const error = JSON.parse(detail).error;
|
|
305
|
+
return error === "invalid_grant" || response.status === 401 && error === "Invalid or expired refresh token";
|
|
306
|
+
} catch {
|
|
307
|
+
return response.status === 401 && detail.includes("Invalid or expired refresh token");
|
|
98
308
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
signal: options.signal
|
|
105
|
-
});
|
|
106
|
-
if (!response.ok) {
|
|
107
|
-
if (options.quiet) return void 0;
|
|
108
|
-
const detail = (await response.text().catch(() => "")).slice(0, 200);
|
|
109
|
-
process.stderr.write(
|
|
110
|
-
`[prim] token refresh rejected by broker: ${response.status} ${response.statusText}${detail ? ` \u2014 ${detail}` : ""}
|
|
309
|
+
}
|
|
310
|
+
function refreshDiagnostic(response, detail, quiet) {
|
|
311
|
+
if (quiet) return;
|
|
312
|
+
process.stderr.write(
|
|
313
|
+
`[prim] token refresh rejected by broker: ${response.status} ${response.statusText}${detail ? ` \u2014 ${detail}` : ""}
|
|
111
314
|
`
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
async function performTokenRefresh(options = {}) {
|
|
318
|
+
const selected = resolveAuthCredential();
|
|
319
|
+
if (selected?.source !== "token_file") return void 0;
|
|
320
|
+
const startingGeneration = readTrimmed(REFRESH_TOKEN_PATH);
|
|
321
|
+
if (!startingGeneration || isSessionEnded()) return void 0;
|
|
322
|
+
return withCredentialLock(
|
|
323
|
+
async () => {
|
|
324
|
+
const currentCredential = resolveAuthCredential();
|
|
325
|
+
if (currentCredential?.source !== "token_file") return void 0;
|
|
326
|
+
const currentGeneration = readTrimmed(REFRESH_TOKEN_PATH);
|
|
327
|
+
if (!currentGeneration) return void 0;
|
|
328
|
+
if (currentGeneration !== startingGeneration) {
|
|
329
|
+
return currentCredential.token;
|
|
330
|
+
}
|
|
331
|
+
if (isSessionEnded()) return void 0;
|
|
332
|
+
const response = await fetch(`${getSiteUrl()}/mcp/broker/refresh`, {
|
|
333
|
+
method: "POST",
|
|
334
|
+
headers: { "Content-Type": "application/json" },
|
|
335
|
+
body: JSON.stringify({ refresh_token: currentGeneration }),
|
|
336
|
+
signal: options.signal
|
|
337
|
+
});
|
|
338
|
+
if (!response.ok) {
|
|
339
|
+
const detail = (await response.text().catch(() => "")).slice(0, 200);
|
|
340
|
+
if (isTerminalRefreshResponse(response, detail) && readTrimmed(REFRESH_TOKEN_PATH) === currentGeneration) {
|
|
341
|
+
writeTerminalFingerprint(currentGeneration);
|
|
342
|
+
}
|
|
343
|
+
refreshDiagnostic(response, detail, options.quiet);
|
|
344
|
+
const winner = resolveAuthCredential();
|
|
345
|
+
return readTrimmed(REFRESH_TOKEN_PATH) !== currentGeneration && winner?.source === "token_file" ? winner.token : void 0;
|
|
346
|
+
}
|
|
347
|
+
let data;
|
|
348
|
+
try {
|
|
349
|
+
data = await response.json();
|
|
350
|
+
} catch {
|
|
351
|
+
if (readTrimmed(REFRESH_TOKEN_PATH) === currentGeneration) {
|
|
352
|
+
writeTerminalFingerprint(currentGeneration);
|
|
353
|
+
}
|
|
354
|
+
return void 0;
|
|
355
|
+
}
|
|
356
|
+
const record = typeof data === "object" && data !== null && !Array.isArray(data) ? data : void 0;
|
|
357
|
+
const accessToken = typeof record?.access_token === "string" ? record.access_token.trim() : "";
|
|
358
|
+
const replacementRefreshToken = typeof record?.refresh_token === "string" ? record.refresh_token.trim() : "";
|
|
359
|
+
if (!(accessToken && replacementRefreshToken) || replacementRefreshToken === currentGeneration) {
|
|
360
|
+
if (readTrimmed(REFRESH_TOKEN_PATH) === currentGeneration) {
|
|
361
|
+
writeTerminalFingerprint(currentGeneration);
|
|
362
|
+
}
|
|
363
|
+
return void 0;
|
|
364
|
+
}
|
|
365
|
+
commitCredentialsUnlocked({
|
|
366
|
+
accessToken,
|
|
367
|
+
refreshToken: replacementRefreshToken,
|
|
368
|
+
expiresIn: typeof record?.expires_in === "number" && Number.isFinite(record.expires_in) ? record.expires_in : void 0
|
|
369
|
+
});
|
|
370
|
+
return accessToken;
|
|
371
|
+
},
|
|
372
|
+
{ signal: options.signal }
|
|
373
|
+
);
|
|
125
374
|
}
|
|
126
375
|
var _refreshInFlight;
|
|
127
376
|
function refreshToken(options = {}) {
|
|
128
|
-
if (_refreshInFlight)
|
|
129
|
-
return _refreshInFlight;
|
|
130
|
-
}
|
|
377
|
+
if (_refreshInFlight) return _refreshInFlight;
|
|
131
378
|
const attempt = performTokenRefresh(options).then((token) => {
|
|
132
|
-
if (token) {
|
|
133
|
-
_cachedToken = token;
|
|
134
|
-
}
|
|
379
|
+
if (token) _cachedCredential = { token, source: "token_file" };
|
|
135
380
|
return token;
|
|
136
381
|
}).finally(() => {
|
|
137
|
-
if (_refreshInFlight === attempt)
|
|
138
|
-
_refreshInFlight = void 0;
|
|
139
|
-
}
|
|
382
|
+
if (_refreshInFlight === attempt) _refreshInFlight = void 0;
|
|
140
383
|
});
|
|
141
384
|
_refreshInFlight = attempt;
|
|
142
385
|
return attempt;
|
|
@@ -149,29 +392,29 @@ var HttpError = class extends Error {
|
|
|
149
392
|
this.status = status;
|
|
150
393
|
}
|
|
151
394
|
};
|
|
152
|
-
var
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
_cachedToken = getAuthToken();
|
|
395
|
+
var _cachedCredential;
|
|
396
|
+
function selectedCredential() {
|
|
397
|
+
const resolved = resolveAuthCredential();
|
|
398
|
+
if (!_cachedCredential || resolved?.token !== _cachedCredential.token || resolved?.source !== _cachedCredential.source) {
|
|
399
|
+
_cachedCredential = resolved;
|
|
158
400
|
}
|
|
159
|
-
|
|
160
|
-
|
|
401
|
+
return _cachedCredential;
|
|
402
|
+
}
|
|
403
|
+
async function request(method, path, body, options) {
|
|
404
|
+
const url = `${getSiteUrl()}${path}`;
|
|
405
|
+
let credential = selectedCredential();
|
|
406
|
+
let refreshAttempted = false;
|
|
407
|
+
if (credential && isTokenExpiringSoon(credential)) {
|
|
408
|
+
refreshAttempted = true;
|
|
409
|
+
const token = await refreshToken({
|
|
161
410
|
signal: options?.signal,
|
|
162
411
|
quiet: options?.quietRefresh
|
|
163
412
|
});
|
|
164
|
-
if (
|
|
165
|
-
_cachedToken = newToken;
|
|
166
|
-
}
|
|
413
|
+
if (token) credential = { token, source: "token_file" };
|
|
167
414
|
}
|
|
168
|
-
const doFetch =
|
|
169
|
-
const headers = {
|
|
170
|
-
|
|
171
|
-
};
|
|
172
|
-
if (token) {
|
|
173
|
-
headers.Authorization = `Bearer ${token}`;
|
|
174
|
-
}
|
|
415
|
+
const doFetch = (token) => {
|
|
416
|
+
const headers = { "Content-Type": "application/json" };
|
|
417
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
175
418
|
return fetch(url, {
|
|
176
419
|
method,
|
|
177
420
|
headers,
|
|
@@ -179,26 +422,31 @@ async function request(method, path, body, options) {
|
|
|
179
422
|
signal: options?.signal
|
|
180
423
|
});
|
|
181
424
|
};
|
|
182
|
-
const tokenUsed =
|
|
183
|
-
let
|
|
184
|
-
if (
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
425
|
+
const tokenUsed = credential?.token;
|
|
426
|
+
let response = await doFetch(tokenUsed);
|
|
427
|
+
if (response.status === 401) {
|
|
428
|
+
const latest = resolveAuthCredential();
|
|
429
|
+
let retryToken = latest?.token !== tokenUsed ? latest?.token : void 0;
|
|
430
|
+
if (!retryToken && latest?.source === "token_file" && !refreshAttempted) {
|
|
431
|
+
refreshAttempted = true;
|
|
432
|
+
retryToken = await refreshToken({ signal: options?.signal, quiet: options?.quietRefresh });
|
|
433
|
+
}
|
|
434
|
+
if (retryToken) {
|
|
435
|
+
_cachedCredential = {
|
|
436
|
+
token: retryToken,
|
|
437
|
+
source: latest?.source === "token_file" ? "token_file" : latest?.source ?? "token_file"
|
|
438
|
+
};
|
|
439
|
+
response = await doFetch(retryToken);
|
|
192
440
|
}
|
|
193
441
|
}
|
|
194
|
-
if (!
|
|
195
|
-
if (
|
|
442
|
+
if (!response.ok) {
|
|
443
|
+
if (response.status === 401) {
|
|
196
444
|
throw new HttpError(401, "Authentication expired. Run `prim auth login` to re-authenticate.");
|
|
197
445
|
}
|
|
198
|
-
const errorBody = await
|
|
199
|
-
throw new HttpError(
|
|
446
|
+
const errorBody = await response.json().catch(() => null);
|
|
447
|
+
throw new HttpError(response.status, errorBody?.error ?? `HTTP ${response.status}`);
|
|
200
448
|
}
|
|
201
|
-
return
|
|
449
|
+
return response.json();
|
|
202
450
|
}
|
|
203
451
|
function getClient() {
|
|
204
452
|
return {
|
|
@@ -208,7 +456,7 @@ function getClient() {
|
|
|
208
456
|
}
|
|
209
457
|
|
|
210
458
|
// src/flusher.ts
|
|
211
|
-
import { createReadStream, renameSync, unlinkSync } from "fs";
|
|
459
|
+
import { createReadStream, renameSync as renameSync2, unlinkSync } from "fs";
|
|
212
460
|
import { createInterface } from "readline";
|
|
213
461
|
|
|
214
462
|
// src/ingest-response.ts
|
|
@@ -238,18 +486,18 @@ function requireDurableIngestAcknowledgement(value, expected) {
|
|
|
238
486
|
import {
|
|
239
487
|
appendFileSync,
|
|
240
488
|
closeSync,
|
|
241
|
-
existsSync as
|
|
489
|
+
existsSync as existsSync3,
|
|
242
490
|
fstatSync,
|
|
243
|
-
mkdirSync,
|
|
491
|
+
mkdirSync as mkdirSync3,
|
|
244
492
|
openSync,
|
|
245
493
|
opendirSync,
|
|
246
|
-
readFileSync as
|
|
494
|
+
readFileSync as readFileSync3,
|
|
247
495
|
readSync,
|
|
248
496
|
readdirSync
|
|
249
497
|
} from "fs";
|
|
250
498
|
import { homedir as homedir2 } from "os";
|
|
251
|
-
import { dirname, join as
|
|
252
|
-
var JOURNAL_DIR =
|
|
499
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
500
|
+
var JOURNAL_DIR = join3(homedir2(), ".config", "prim", "moves");
|
|
253
501
|
var JOURNAL_BASENAME = "journal.ndjson";
|
|
254
502
|
var JOURNAL_STATS_SAMPLE_BYTES = 64 * 1024;
|
|
255
503
|
var PENDING_STATS_MAX_FILES = 128;
|
|
@@ -261,7 +509,7 @@ function envSlug(apiUrl) {
|
|
|
261
509
|
return slug;
|
|
262
510
|
}
|
|
263
511
|
function currentEnvDir() {
|
|
264
|
-
return
|
|
512
|
+
return join3(JOURNAL_DIR, envSlug(getSiteUrl()));
|
|
265
513
|
}
|
|
266
514
|
function parseMoves(content) {
|
|
267
515
|
const moves = [];
|
|
@@ -279,15 +527,15 @@ function parseMoves(content) {
|
|
|
279
527
|
function listBuckets() {
|
|
280
528
|
const out = [];
|
|
281
529
|
const envDir = currentEnvDir();
|
|
282
|
-
if (!
|
|
530
|
+
if (!existsSync3(envDir)) {
|
|
283
531
|
return out;
|
|
284
532
|
}
|
|
285
533
|
for (const entry of readdirSync(envDir, { withFileTypes: true })) {
|
|
286
534
|
if (!entry.isDirectory()) {
|
|
287
535
|
continue;
|
|
288
536
|
}
|
|
289
|
-
const path =
|
|
290
|
-
if (
|
|
537
|
+
const path = join3(envDir, entry.name, JOURNAL_BASENAME);
|
|
538
|
+
if (existsSync3(path)) {
|
|
291
539
|
out.push({ bucket: entry.name, path });
|
|
292
540
|
}
|
|
293
541
|
}
|
|
@@ -345,7 +593,7 @@ function parseFlushingPid(name) {
|
|
|
345
593
|
return last !== void 0 && /^[0-9]+$/.test(last) ? Number(last) : void 0;
|
|
346
594
|
}
|
|
347
595
|
function listFlushingInDir(dir, bucket, options = {}) {
|
|
348
|
-
if (!
|
|
596
|
+
if (!existsSync3(dir)) {
|
|
349
597
|
return [];
|
|
350
598
|
}
|
|
351
599
|
const out = [];
|
|
@@ -359,7 +607,7 @@ function listFlushingInDir(dir, bucket, options = {}) {
|
|
|
359
607
|
entry = entries.readSync();
|
|
360
608
|
continue;
|
|
361
609
|
}
|
|
362
|
-
const path =
|
|
610
|
+
const path = join3(dir, entry.name);
|
|
363
611
|
let sample;
|
|
364
612
|
try {
|
|
365
613
|
sample = sampleJournalFile(path, remainingBytes);
|
|
@@ -386,7 +634,7 @@ function listFlushingInDir(dir, bucket, options = {}) {
|
|
|
386
634
|
}
|
|
387
635
|
function listFlushing(options = {}) {
|
|
388
636
|
const envDir = currentEnvDir();
|
|
389
|
-
if (!
|
|
637
|
+
if (!existsSync3(envDir)) {
|
|
390
638
|
return [];
|
|
391
639
|
}
|
|
392
640
|
const out = [];
|
|
@@ -397,7 +645,7 @@ function listFlushing(options = {}) {
|
|
|
397
645
|
break;
|
|
398
646
|
}
|
|
399
647
|
if (entry.isDirectory()) {
|
|
400
|
-
const found = listFlushingInDir(
|
|
648
|
+
const found = listFlushingInDir(join3(envDir, entry.name), entry.name, {
|
|
401
649
|
sampleBytes: remainingBytes,
|
|
402
650
|
maxFiles: maxFiles - out.length
|
|
403
651
|
});
|
|
@@ -501,7 +749,7 @@ async function drainFlushingPath(flushingPath, client2 = getClient()) {
|
|
|
501
749
|
async function drainPath(path) {
|
|
502
750
|
const tmpPath = `${path}.flushing.${String(Date.now())}.${String(process.pid)}`;
|
|
503
751
|
try {
|
|
504
|
-
|
|
752
|
+
renameSync2(path, tmpPath);
|
|
505
753
|
} catch (err) {
|
|
506
754
|
if (err.code === "ENOENT") {
|
|
507
755
|
return 0;
|
|
@@ -510,7 +758,7 @@ async function drainPath(path) {
|
|
|
510
758
|
}
|
|
511
759
|
return drainFlushingPath(tmpPath);
|
|
512
760
|
}
|
|
513
|
-
function
|
|
761
|
+
function processIsAlive2(pid) {
|
|
514
762
|
try {
|
|
515
763
|
process.kill(pid, 0);
|
|
516
764
|
return true;
|
|
@@ -520,7 +768,7 @@ function processIsAlive(pid) {
|
|
|
520
768
|
}
|
|
521
769
|
function selectRecoverable(files, now, opts = {}) {
|
|
522
770
|
const quarantineMs = opts.quarantineMs ?? ORPHAN_QUARANTINE_MS;
|
|
523
|
-
const isAlive = opts.isAlive ??
|
|
771
|
+
const isAlive = opts.isAlive ?? processIsAlive2;
|
|
524
772
|
return files.filter((f) => {
|
|
525
773
|
if (f.pid === void 0) {
|
|
526
774
|
return now - f.mtimeMs > quarantineMs;
|
|
@@ -592,14 +840,14 @@ function flush() {
|
|
|
592
840
|
// src/daemon/client.ts
|
|
593
841
|
import { createConnection } from "net";
|
|
594
842
|
import { homedir as homedir3 } from "os";
|
|
595
|
-
import { join as
|
|
596
|
-
var SOCK_PATH =
|
|
597
|
-
var
|
|
843
|
+
import { join as join4 } from "path";
|
|
844
|
+
var SOCK_PATH = join4(homedir3(), ".config", "prim", "sock");
|
|
845
|
+
var DEFAULT_TIMEOUT_MS2 = 250;
|
|
598
846
|
var nextRequestId = 1;
|
|
599
847
|
function daemonRequest(method, params = {}, opts = {}) {
|
|
600
848
|
const id = nextRequestId++;
|
|
601
849
|
const request2 = { id, method, params };
|
|
602
|
-
const timeoutMs = opts.timeoutMs ??
|
|
850
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
603
851
|
return new Promise((resolve2) => {
|
|
604
852
|
let settled = false;
|
|
605
853
|
let socket;
|
|
@@ -686,17 +934,17 @@ function assertCallerEnvMatches(callerEnv, boundEnv) {
|
|
|
686
934
|
}
|
|
687
935
|
|
|
688
936
|
// src/daemon/health.ts
|
|
689
|
-
import { chmodSync, existsSync as
|
|
937
|
+
import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync4, renameSync as renameSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
690
938
|
import { homedir as homedir4 } from "os";
|
|
691
|
-
import { dirname as
|
|
692
|
-
var
|
|
939
|
+
import { dirname as dirname4, join as join5 } from "path";
|
|
940
|
+
var CONFIG_DIR_MODE2 = 448;
|
|
693
941
|
var HEALTH_FILE_MODE = 384;
|
|
694
942
|
var INGESTION_SLA_MS = 3e4;
|
|
695
943
|
var HEARTBEAT_FRESH_MS = 9e4;
|
|
696
944
|
var HEARTBEAT_RETRY_CAP_MS = 3e4;
|
|
697
945
|
var INGESTION_RETRY_BASE_MS = 5e3;
|
|
698
946
|
var INGESTION_RETRY_CAP_MS = 15e3;
|
|
699
|
-
var DAEMON_HEALTH_PATH =
|
|
947
|
+
var DAEMON_HEALTH_PATH = join5(homedir4(), ".config", "prim", "daemon-health.json");
|
|
700
948
|
function createDaemonHealthState(version, pid, startedAt2) {
|
|
701
949
|
return {
|
|
702
950
|
schemaVersion: 1,
|
|
@@ -719,7 +967,7 @@ function createDaemonHealthState(version, pid, startedAt2) {
|
|
|
719
967
|
function refreshDaemonHealth(state, now) {
|
|
720
968
|
state.heartbeat.healthy = state.heartbeat.consecutiveFailures === 0 && state.heartbeat.lastSuccessAt !== void 0 && now - state.heartbeat.lastSuccessAt < HEARTBEAT_FRESH_MS;
|
|
721
969
|
state.ingestion.healthy = state.ingestion.consecutiveFailures === 0 && !state.ingestion.pendingSampled && (state.ingestion.pendingCount === 0 || state.ingestion.oldestPendingAt !== void 0 && now - state.ingestion.oldestPendingAt <= INGESTION_SLA_MS);
|
|
722
|
-
state.healthy = state.heartbeat.healthy && state.ingestion.healthy;
|
|
970
|
+
state.healthy = state.heartbeat.healthy && state.ingestion.healthy && !state.needsReauth;
|
|
723
971
|
}
|
|
724
972
|
function retryDelayMs(consecutiveFailures, capMs, random) {
|
|
725
973
|
const exponent = Math.max(0, consecutiveFailures - 1);
|
|
@@ -734,37 +982,37 @@ function ingestionRetryDelayMs(consecutiveFailures, random = Math.random) {
|
|
|
734
982
|
return retryDelayMs(consecutiveFailures, INGESTION_RETRY_CAP_MS, random);
|
|
735
983
|
}
|
|
736
984
|
function writeDaemonHealthState(state, path = DAEMON_HEALTH_PATH) {
|
|
737
|
-
const dir =
|
|
738
|
-
if (!
|
|
739
|
-
|
|
985
|
+
const dir = dirname4(path);
|
|
986
|
+
if (!existsSync4(dir)) {
|
|
987
|
+
mkdirSync4(dir, { recursive: true, mode: CONFIG_DIR_MODE2 });
|
|
740
988
|
}
|
|
741
|
-
const tmp =
|
|
742
|
-
|
|
989
|
+
const tmp = join5(dir, `.${process.pid}.daemon-health.tmp`);
|
|
990
|
+
writeFileSync3(tmp, `${JSON.stringify(state, null, 2)}
|
|
743
991
|
`, { mode: HEALTH_FILE_MODE });
|
|
744
|
-
|
|
745
|
-
|
|
992
|
+
chmodSync2(tmp, HEALTH_FILE_MODE);
|
|
993
|
+
renameSync3(tmp, path);
|
|
746
994
|
}
|
|
747
995
|
|
|
748
996
|
// src/daemon/instance-lock.ts
|
|
749
997
|
import { randomUUID } from "crypto";
|
|
750
998
|
import {
|
|
751
|
-
chmodSync as
|
|
752
|
-
existsSync as
|
|
753
|
-
mkdirSync as
|
|
754
|
-
readFileSync as
|
|
755
|
-
renameSync as
|
|
756
|
-
rmSync,
|
|
757
|
-
statSync,
|
|
999
|
+
chmodSync as chmodSync3,
|
|
1000
|
+
existsSync as existsSync5,
|
|
1001
|
+
mkdirSync as mkdirSync5,
|
|
1002
|
+
readFileSync as readFileSync4,
|
|
1003
|
+
renameSync as renameSync4,
|
|
1004
|
+
rmSync as rmSync3,
|
|
1005
|
+
statSync as statSync2,
|
|
758
1006
|
unlinkSync as unlinkSync2,
|
|
759
|
-
writeFileSync as
|
|
1007
|
+
writeFileSync as writeFileSync4
|
|
760
1008
|
} from "fs";
|
|
761
|
-
import { join as
|
|
1009
|
+
import { join as join6 } from "path";
|
|
762
1010
|
var DIR_MODE = 448;
|
|
763
|
-
var
|
|
1011
|
+
var FILE_MODE2 = 384;
|
|
764
1012
|
var DAEMON_STARTUP_GRACE_MS = 5e3;
|
|
765
1013
|
function readLockRecord(path) {
|
|
766
1014
|
try {
|
|
767
|
-
const value = JSON.parse(
|
|
1015
|
+
const value = JSON.parse(readFileSync4(path, "utf-8"));
|
|
768
1016
|
if (Number.isInteger(value.pid) && typeof value.instanceId === "string" && typeof value.startedAt === "number") {
|
|
769
1017
|
return value;
|
|
770
1018
|
}
|
|
@@ -774,7 +1022,7 @@ function readLockRecord(path) {
|
|
|
774
1022
|
}
|
|
775
1023
|
function numericPid(path) {
|
|
776
1024
|
try {
|
|
777
|
-
const pid = Number(
|
|
1025
|
+
const pid = Number(readFileSync4(path, "utf-8").trim());
|
|
778
1026
|
return Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
779
1027
|
} catch {
|
|
780
1028
|
return;
|
|
@@ -789,16 +1037,16 @@ function daemonProcessIsAlive(pid) {
|
|
|
789
1037
|
}
|
|
790
1038
|
}
|
|
791
1039
|
function readDaemonOwner(configDir) {
|
|
792
|
-
return readLockRecord(
|
|
1040
|
+
return readLockRecord(join6(configDir, "daemon.lock", "owner.json"));
|
|
793
1041
|
}
|
|
794
1042
|
function readDaemonPid(configDir) {
|
|
795
|
-
const path =
|
|
1043
|
+
const path = join6(configDir, "daemon.pid");
|
|
796
1044
|
const pid = numericPid(path);
|
|
797
1045
|
if (pid === void 0) {
|
|
798
1046
|
return;
|
|
799
1047
|
}
|
|
800
1048
|
try {
|
|
801
|
-
return { pid, mtimeMs:
|
|
1049
|
+
return { pid, mtimeMs: statSync2(path).mtimeMs };
|
|
802
1050
|
} catch {
|
|
803
1051
|
return;
|
|
804
1052
|
}
|
|
@@ -807,29 +1055,29 @@ function acquireDaemonOwnership(configDir, opts = {}) {
|
|
|
807
1055
|
const pid = opts.pid ?? process.pid;
|
|
808
1056
|
const now = opts.now ?? Date.now();
|
|
809
1057
|
const isAlive = opts.isAlive ?? daemonProcessIsAlive;
|
|
810
|
-
const lockDir =
|
|
811
|
-
const
|
|
812
|
-
const pidPath =
|
|
1058
|
+
const lockDir = join6(configDir, "daemon.lock");
|
|
1059
|
+
const ownerPath2 = join6(lockDir, "owner.json");
|
|
1060
|
+
const pidPath = join6(configDir, "daemon.pid");
|
|
813
1061
|
const instanceId = randomUUID();
|
|
814
|
-
if (!
|
|
815
|
-
|
|
1062
|
+
if (!existsSync5(configDir)) {
|
|
1063
|
+
mkdirSync5(configDir, { recursive: true, mode: DIR_MODE });
|
|
816
1064
|
}
|
|
817
1065
|
for (; ; ) {
|
|
818
1066
|
try {
|
|
819
|
-
|
|
1067
|
+
mkdirSync5(lockDir, { mode: DIR_MODE });
|
|
820
1068
|
break;
|
|
821
1069
|
} catch (err) {
|
|
822
1070
|
if (err.code !== "EEXIST") {
|
|
823
1071
|
throw err;
|
|
824
1072
|
}
|
|
825
|
-
const existing = readLockRecord(
|
|
1073
|
+
const existing = readLockRecord(ownerPath2);
|
|
826
1074
|
if (existing && isAlive(existing.pid)) {
|
|
827
1075
|
throw new Error(`daemon already running (pid=${String(existing.pid)})`);
|
|
828
1076
|
}
|
|
829
1077
|
if (!existing) {
|
|
830
1078
|
let mtimeMs;
|
|
831
1079
|
try {
|
|
832
|
-
mtimeMs =
|
|
1080
|
+
mtimeMs = statSync2(lockDir).mtimeMs;
|
|
833
1081
|
} catch (statErr) {
|
|
834
1082
|
if (statErr.code === "ENOENT") {
|
|
835
1083
|
continue;
|
|
@@ -842,8 +1090,8 @@ function acquireDaemonOwnership(configDir, opts = {}) {
|
|
|
842
1090
|
}
|
|
843
1091
|
const stale = `${lockDir}.stale.${String(pid)}.${instanceId}`;
|
|
844
1092
|
try {
|
|
845
|
-
|
|
846
|
-
|
|
1093
|
+
renameSync4(lockDir, stale);
|
|
1094
|
+
rmSync3(stale, { recursive: true, force: true });
|
|
847
1095
|
} catch (renameErr) {
|
|
848
1096
|
if (renameErr.code !== "ENOENT") {
|
|
849
1097
|
throw renameErr;
|
|
@@ -854,22 +1102,22 @@ function acquireDaemonOwnership(configDir, opts = {}) {
|
|
|
854
1102
|
const ownership2 = {
|
|
855
1103
|
configDir,
|
|
856
1104
|
lockDir,
|
|
857
|
-
ownerPath,
|
|
1105
|
+
ownerPath: ownerPath2,
|
|
858
1106
|
pidPath,
|
|
859
1107
|
pid,
|
|
860
1108
|
instanceId
|
|
861
1109
|
};
|
|
862
1110
|
const record = { pid, instanceId, startedAt: now };
|
|
863
|
-
|
|
864
|
-
`, { mode:
|
|
865
|
-
|
|
1111
|
+
writeFileSync4(ownerPath2, `${JSON.stringify(record)}
|
|
1112
|
+
`, { mode: FILE_MODE2 });
|
|
1113
|
+
chmodSync3(ownerPath2, FILE_MODE2);
|
|
866
1114
|
const legacyPid = numericPid(pidPath);
|
|
867
1115
|
if (legacyPid !== void 0 && legacyPid !== pid && isAlive(legacyPid)) {
|
|
868
|
-
|
|
1116
|
+
rmSync3(lockDir, { recursive: true, force: true });
|
|
869
1117
|
throw new Error(`daemon already running (pid=${String(legacyPid)})`);
|
|
870
1118
|
}
|
|
871
|
-
|
|
872
|
-
|
|
1119
|
+
writeFileSync4(pidPath, String(pid), { mode: FILE_MODE2 });
|
|
1120
|
+
chmodSync3(pidPath, FILE_MODE2);
|
|
873
1121
|
return ownership2;
|
|
874
1122
|
}
|
|
875
1123
|
function ownsDaemonInstance(ownership2) {
|
|
@@ -899,7 +1147,7 @@ function releaseDaemonOwnership(ownership2, socketPath) {
|
|
|
899
1147
|
}
|
|
900
1148
|
}
|
|
901
1149
|
}
|
|
902
|
-
|
|
1150
|
+
rmSync3(ownership2.lockDir, { recursive: true, force: true });
|
|
903
1151
|
if (cleanupError) {
|
|
904
1152
|
throw cleanupError;
|
|
905
1153
|
}
|
|
@@ -907,8 +1155,8 @@ function releaseDaemonOwnership(ownership2, socketPath) {
|
|
|
907
1155
|
}
|
|
908
1156
|
|
|
909
1157
|
// src/daemon/server.ts
|
|
910
|
-
var CONFIG_DIR =
|
|
911
|
-
var SOCK_PATH2 =
|
|
1158
|
+
var CONFIG_DIR = join7(homedir5(), ".config", "prim");
|
|
1159
|
+
var SOCK_PATH2 = join7(CONFIG_DIR, "sock");
|
|
912
1160
|
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
913
1161
|
var INGESTION_POLL_INTERVAL_MS = 15e3;
|
|
914
1162
|
var TOKEN_CHECK_INTERVAL_MS = 6e4;
|
|
@@ -935,13 +1183,14 @@ var heartbeatRerunRequested = false;
|
|
|
935
1183
|
var ownership;
|
|
936
1184
|
var socketServer;
|
|
937
1185
|
var shuttingDown = false;
|
|
1186
|
+
var reauthHold = false;
|
|
938
1187
|
function resolveRuntimeVersion() {
|
|
939
1188
|
if (process.env.PRIM_RUNTIME_VERSION) {
|
|
940
1189
|
return process.env.PRIM_RUNTIME_VERSION;
|
|
941
1190
|
}
|
|
942
1191
|
try {
|
|
943
1192
|
const here = fileURLToPath(new URL(".", import.meta.url));
|
|
944
|
-
const pkg = JSON.parse(
|
|
1193
|
+
const pkg = JSON.parse(readFileSync5(join7(here, "..", "..", "package.json"), "utf-8"));
|
|
945
1194
|
return pkg.version ?? "unknown";
|
|
946
1195
|
} catch {
|
|
947
1196
|
return "unknown";
|
|
@@ -967,6 +1216,42 @@ function updatePendingHealth() {
|
|
|
967
1216
|
daemonHealth.ingestion.oldestPendingAt = pending.oldestPendingAt;
|
|
968
1217
|
daemonHealth.ingestion.strandedCount = pending.strandedCount;
|
|
969
1218
|
}
|
|
1219
|
+
function enterReauthHold() {
|
|
1220
|
+
if (reauthHold) {
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
reauthHold = true;
|
|
1224
|
+
daemonHealth.needsReauth = true;
|
|
1225
|
+
if (heartbeatTimer) {
|
|
1226
|
+
clearTimeout(heartbeatTimer);
|
|
1227
|
+
heartbeatTimer = void 0;
|
|
1228
|
+
}
|
|
1229
|
+
if (ingestionTimer) {
|
|
1230
|
+
clearTimeout(ingestionTimer);
|
|
1231
|
+
ingestionTimer = void 0;
|
|
1232
|
+
}
|
|
1233
|
+
persistHealth();
|
|
1234
|
+
process.stderr.write(
|
|
1235
|
+
"[prim-daemon] authentication ended \u2014 halting heartbeat + ingestion until `prim auth login` (captures continue to the journal)\n"
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
function exitReauthHold() {
|
|
1239
|
+
if (!reauthHold) {
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
reauthHold = false;
|
|
1243
|
+
daemonHealth.needsReauth = false;
|
|
1244
|
+
daemonHealth.heartbeat.consecutiveFailures = 0;
|
|
1245
|
+
daemonHealth.ingestion.consecutiveFailures = 0;
|
|
1246
|
+
daemonHealth.heartbeat.lastError = void 0;
|
|
1247
|
+
daemonHealth.ingestion.lastError = void 0;
|
|
1248
|
+
persistHealth();
|
|
1249
|
+
process.stderr.write(
|
|
1250
|
+
"[prim-daemon] re-authentication detected \u2014 resuming heartbeat + ingestion\n"
|
|
1251
|
+
);
|
|
1252
|
+
void sendHeartbeat();
|
|
1253
|
+
void runIngestionLoop();
|
|
1254
|
+
}
|
|
970
1255
|
async function takeOwnership() {
|
|
971
1256
|
const now = Date.now();
|
|
972
1257
|
const recordedOwner = readDaemonOwner(CONFIG_DIR);
|
|
@@ -976,7 +1261,7 @@ async function takeOwnership() {
|
|
|
976
1261
|
(pid) => pid !== void 0 && daemonProcessIsAlive(pid)
|
|
977
1262
|
)
|
|
978
1263
|
);
|
|
979
|
-
if (
|
|
1264
|
+
if (existsSync6(SOCK_PATH2) || candidatePids.size > 0) {
|
|
980
1265
|
const snapshot = await daemonRequest(
|
|
981
1266
|
"status_snapshot",
|
|
982
1267
|
{},
|
|
@@ -1051,11 +1336,18 @@ async function performHeartbeat() {
|
|
|
1051
1336
|
daemonHealth.heartbeat.lastError = errorMessage(err);
|
|
1052
1337
|
daemonHealth.heartbeat.consecutiveFailures += 1;
|
|
1053
1338
|
persistHealth();
|
|
1339
|
+
if (isSessionEnded()) {
|
|
1340
|
+
enterReauthHold();
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1054
1343
|
process.stderr.write(`[prim-daemon] heartbeat error: ${errorMessage(err)}
|
|
1055
1344
|
`);
|
|
1056
1345
|
}
|
|
1057
1346
|
}
|
|
1058
1347
|
function sendHeartbeat() {
|
|
1348
|
+
if (shuttingDown || reauthHold) {
|
|
1349
|
+
return Promise.resolve();
|
|
1350
|
+
}
|
|
1059
1351
|
if (heartbeatInFlight) {
|
|
1060
1352
|
heartbeatRerunRequested = true;
|
|
1061
1353
|
return heartbeatInFlight;
|
|
@@ -1068,7 +1360,7 @@ function sendHeartbeat() {
|
|
|
1068
1360
|
do {
|
|
1069
1361
|
heartbeatRerunRequested = false;
|
|
1070
1362
|
await performHeartbeat();
|
|
1071
|
-
} while (heartbeatRerunRequested && !shuttingDown);
|
|
1363
|
+
} while (heartbeatRerunRequested && !(shuttingDown || reauthHold));
|
|
1072
1364
|
})().finally(() => {
|
|
1073
1365
|
heartbeatInFlight = void 0;
|
|
1074
1366
|
scheduleHeartbeat();
|
|
@@ -1076,7 +1368,7 @@ function sendHeartbeat() {
|
|
|
1076
1368
|
return heartbeatInFlight;
|
|
1077
1369
|
}
|
|
1078
1370
|
function scheduleHeartbeat() {
|
|
1079
|
-
if (shuttingDown) {
|
|
1371
|
+
if (shuttingDown || reauthHold) {
|
|
1080
1372
|
return;
|
|
1081
1373
|
}
|
|
1082
1374
|
const failures = daemonHealth.heartbeat.consecutiveFailures;
|
|
@@ -1089,6 +1381,9 @@ function scheduleHeartbeat() {
|
|
|
1089
1381
|
}, delay);
|
|
1090
1382
|
}
|
|
1091
1383
|
async function ensureTokenFresh() {
|
|
1384
|
+
if (resolveAuthCredential()?.source !== "token_file") {
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1092
1387
|
const expiresAt = getTokenExpiresAt();
|
|
1093
1388
|
if (!expiresAt) {
|
|
1094
1389
|
return;
|
|
@@ -1101,7 +1396,7 @@ async function ensureTokenFresh() {
|
|
|
1101
1396
|
}
|
|
1102
1397
|
}
|
|
1103
1398
|
function scheduleIngestion(delayMs) {
|
|
1104
|
-
if (shuttingDown) {
|
|
1399
|
+
if (shuttingDown || reauthHold) {
|
|
1105
1400
|
return;
|
|
1106
1401
|
}
|
|
1107
1402
|
ingestionTimer = setTimeout(() => {
|
|
@@ -1154,6 +1449,10 @@ async function runIngestionLoop() {
|
|
|
1154
1449
|
} catch {
|
|
1155
1450
|
}
|
|
1156
1451
|
persistHealth();
|
|
1452
|
+
if (isSessionEnded()) {
|
|
1453
|
+
enterReauthHold();
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1157
1456
|
process.stderr.write(`[prim-daemon] ingestion error: ${errorMessage(err)}
|
|
1158
1457
|
`);
|
|
1159
1458
|
scheduleIngestion(delay);
|
|
@@ -1198,6 +1497,7 @@ function handleStatusSnapshot(params) {
|
|
|
1198
1497
|
lastHeartbeatAt,
|
|
1199
1498
|
version: runtimeVersion,
|
|
1200
1499
|
healthy: daemonHealth.healthy,
|
|
1500
|
+
needsReauth: daemonHealth.needsReauth === true,
|
|
1201
1501
|
heartbeat: { ...daemonHealth.heartbeat },
|
|
1202
1502
|
ingestion: { ...daemonHealth.ingestion }
|
|
1203
1503
|
};
|
|
@@ -1332,13 +1632,26 @@ function startSocketServer() {
|
|
|
1332
1632
|
});
|
|
1333
1633
|
}
|
|
1334
1634
|
function startTimers() {
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1635
|
+
if (isSessionEnded()) {
|
|
1636
|
+
enterReauthHold();
|
|
1637
|
+
} else {
|
|
1638
|
+
persistHealth();
|
|
1639
|
+
void sendHeartbeat();
|
|
1640
|
+
void runIngestionLoop();
|
|
1641
|
+
}
|
|
1338
1642
|
void runTokenCheckLoop();
|
|
1339
1643
|
}
|
|
1340
1644
|
async function runTokenCheckLoop() {
|
|
1341
|
-
|
|
1645
|
+
if (reauthHold) {
|
|
1646
|
+
if (!isSessionEnded() && resolveAuthCredential()) {
|
|
1647
|
+
exitReauthHold();
|
|
1648
|
+
}
|
|
1649
|
+
} else {
|
|
1650
|
+
await ensureTokenFresh();
|
|
1651
|
+
if (isSessionEnded()) {
|
|
1652
|
+
enterReauthHold();
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1342
1655
|
if (!shuttingDown) {
|
|
1343
1656
|
tokenCheckTimer = setTimeout(() => {
|
|
1344
1657
|
tokenCheckTimer = void 0;
|