@primitive.ai/prim 0.1.0-alpha.45 → 0.1.0-alpha.47
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-ZWTZMTV5.js → chunk-5K725K35.js} +1 -1
- package/dist/{chunk-6TRRGGLD.js → chunk-CK75QBCB.js} +1 -1
- package/dist/chunk-TPROV45L.js +502 -0
- package/dist/{chunk-6MKLCOEH.js → chunk-WRPKAPVE.js} +1 -1
- package/dist/daemon/server.js +460 -221
- 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 +332 -286
- package/dist/statusline-main.js +45 -37
- package/package.json +1 -1
- package/dist/chunk-PI6LDYNJ.js +0 -220
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,112 +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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
262
|
+
function refreshFingerprint(refreshToken2) {
|
|
263
|
+
return createHash("sha256").update(refreshToken2).digest("hex");
|
|
264
|
+
}
|
|
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);
|
|
90
274
|
}
|
|
91
|
-
var _endedRefreshToken;
|
|
92
275
|
function isSessionEnded() {
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return false;
|
|
98
|
-
}
|
|
99
|
-
const current = readFileSync(REFRESH_TOKEN_PATH, "utf-8").trim();
|
|
100
|
-
return current === _endedRefreshToken;
|
|
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);
|
|
101
280
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
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");
|
|
109
289
|
}
|
|
110
|
-
|
|
111
|
-
|
|
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");
|
|
112
308
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
signal: options.signal
|
|
119
|
-
});
|
|
120
|
-
if (!response.ok) {
|
|
121
|
-
const detail = (await response.text().catch(() => "")).slice(0, 200);
|
|
122
|
-
if (detail.includes("invalid_grant") || detail.includes("Session has already ended")) {
|
|
123
|
-
_endedRefreshToken = refreshTokenValue;
|
|
124
|
-
}
|
|
125
|
-
if (!options.quiet) {
|
|
126
|
-
process.stderr.write(
|
|
127
|
-
`[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}` : ""}
|
|
128
314
|
`
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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)) {
|
|
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
|
+
);
|
|
144
374
|
}
|
|
145
375
|
var _refreshInFlight;
|
|
146
376
|
function refreshToken(options = {}) {
|
|
147
|
-
if (_refreshInFlight)
|
|
148
|
-
return _refreshInFlight;
|
|
149
|
-
}
|
|
377
|
+
if (_refreshInFlight) return _refreshInFlight;
|
|
150
378
|
const attempt = performTokenRefresh(options).then((token) => {
|
|
151
|
-
if (token) {
|
|
152
|
-
_cachedToken = token;
|
|
153
|
-
}
|
|
379
|
+
if (token) _cachedCredential = { token, source: "token_file" };
|
|
154
380
|
return token;
|
|
155
381
|
}).finally(() => {
|
|
156
|
-
if (_refreshInFlight === attempt)
|
|
157
|
-
_refreshInFlight = void 0;
|
|
158
|
-
}
|
|
382
|
+
if (_refreshInFlight === attempt) _refreshInFlight = void 0;
|
|
159
383
|
});
|
|
160
384
|
_refreshInFlight = attempt;
|
|
161
385
|
return attempt;
|
|
@@ -168,29 +392,29 @@ var HttpError = class extends Error {
|
|
|
168
392
|
this.status = status;
|
|
169
393
|
}
|
|
170
394
|
};
|
|
171
|
-
var
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
_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;
|
|
177
400
|
}
|
|
178
|
-
|
|
179
|
-
|
|
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({
|
|
180
410
|
signal: options?.signal,
|
|
181
411
|
quiet: options?.quietRefresh
|
|
182
412
|
});
|
|
183
|
-
if (
|
|
184
|
-
_cachedToken = newToken;
|
|
185
|
-
}
|
|
413
|
+
if (token) credential = { token, source: "token_file" };
|
|
186
414
|
}
|
|
187
|
-
const doFetch =
|
|
188
|
-
const headers = {
|
|
189
|
-
|
|
190
|
-
};
|
|
191
|
-
if (token) {
|
|
192
|
-
headers.Authorization = `Bearer ${token}`;
|
|
193
|
-
}
|
|
415
|
+
const doFetch = (token) => {
|
|
416
|
+
const headers = { "Content-Type": "application/json" };
|
|
417
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
194
418
|
return fetch(url, {
|
|
195
419
|
method,
|
|
196
420
|
headers,
|
|
@@ -198,26 +422,31 @@ async function request(method, path, body, options) {
|
|
|
198
422
|
signal: options?.signal
|
|
199
423
|
});
|
|
200
424
|
};
|
|
201
|
-
const tokenUsed =
|
|
202
|
-
let
|
|
203
|
-
if (
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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);
|
|
211
440
|
}
|
|
212
441
|
}
|
|
213
|
-
if (!
|
|
214
|
-
if (
|
|
442
|
+
if (!response.ok) {
|
|
443
|
+
if (response.status === 401) {
|
|
215
444
|
throw new HttpError(401, "Authentication expired. Run `prim auth login` to re-authenticate.");
|
|
216
445
|
}
|
|
217
|
-
const errorBody = await
|
|
218
|
-
throw new HttpError(
|
|
446
|
+
const errorBody = await response.json().catch(() => null);
|
|
447
|
+
throw new HttpError(response.status, errorBody?.error ?? `HTTP ${response.status}`);
|
|
219
448
|
}
|
|
220
|
-
return
|
|
449
|
+
return response.json();
|
|
221
450
|
}
|
|
222
451
|
function getClient() {
|
|
223
452
|
return {
|
|
@@ -227,7 +456,7 @@ function getClient() {
|
|
|
227
456
|
}
|
|
228
457
|
|
|
229
458
|
// src/flusher.ts
|
|
230
|
-
import { createReadStream, renameSync, unlinkSync } from "fs";
|
|
459
|
+
import { createReadStream, renameSync as renameSync2, unlinkSync } from "fs";
|
|
231
460
|
import { createInterface } from "readline";
|
|
232
461
|
|
|
233
462
|
// src/ingest-response.ts
|
|
@@ -257,18 +486,18 @@ function requireDurableIngestAcknowledgement(value, expected) {
|
|
|
257
486
|
import {
|
|
258
487
|
appendFileSync,
|
|
259
488
|
closeSync,
|
|
260
|
-
existsSync as
|
|
489
|
+
existsSync as existsSync3,
|
|
261
490
|
fstatSync,
|
|
262
|
-
mkdirSync,
|
|
491
|
+
mkdirSync as mkdirSync3,
|
|
263
492
|
openSync,
|
|
264
493
|
opendirSync,
|
|
265
|
-
readFileSync as
|
|
494
|
+
readFileSync as readFileSync3,
|
|
266
495
|
readSync,
|
|
267
496
|
readdirSync
|
|
268
497
|
} from "fs";
|
|
269
498
|
import { homedir as homedir2 } from "os";
|
|
270
|
-
import { dirname, join as
|
|
271
|
-
var JOURNAL_DIR =
|
|
499
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
500
|
+
var JOURNAL_DIR = join3(homedir2(), ".config", "prim", "moves");
|
|
272
501
|
var JOURNAL_BASENAME = "journal.ndjson";
|
|
273
502
|
var JOURNAL_STATS_SAMPLE_BYTES = 64 * 1024;
|
|
274
503
|
var PENDING_STATS_MAX_FILES = 128;
|
|
@@ -280,7 +509,7 @@ function envSlug(apiUrl) {
|
|
|
280
509
|
return slug;
|
|
281
510
|
}
|
|
282
511
|
function currentEnvDir() {
|
|
283
|
-
return
|
|
512
|
+
return join3(JOURNAL_DIR, envSlug(getSiteUrl()));
|
|
284
513
|
}
|
|
285
514
|
function parseMoves(content) {
|
|
286
515
|
const moves = [];
|
|
@@ -298,15 +527,15 @@ function parseMoves(content) {
|
|
|
298
527
|
function listBuckets() {
|
|
299
528
|
const out = [];
|
|
300
529
|
const envDir = currentEnvDir();
|
|
301
|
-
if (!
|
|
530
|
+
if (!existsSync3(envDir)) {
|
|
302
531
|
return out;
|
|
303
532
|
}
|
|
304
533
|
for (const entry of readdirSync(envDir, { withFileTypes: true })) {
|
|
305
534
|
if (!entry.isDirectory()) {
|
|
306
535
|
continue;
|
|
307
536
|
}
|
|
308
|
-
const path =
|
|
309
|
-
if (
|
|
537
|
+
const path = join3(envDir, entry.name, JOURNAL_BASENAME);
|
|
538
|
+
if (existsSync3(path)) {
|
|
310
539
|
out.push({ bucket: entry.name, path });
|
|
311
540
|
}
|
|
312
541
|
}
|
|
@@ -364,7 +593,7 @@ function parseFlushingPid(name) {
|
|
|
364
593
|
return last !== void 0 && /^[0-9]+$/.test(last) ? Number(last) : void 0;
|
|
365
594
|
}
|
|
366
595
|
function listFlushingInDir(dir, bucket, options = {}) {
|
|
367
|
-
if (!
|
|
596
|
+
if (!existsSync3(dir)) {
|
|
368
597
|
return [];
|
|
369
598
|
}
|
|
370
599
|
const out = [];
|
|
@@ -378,7 +607,7 @@ function listFlushingInDir(dir, bucket, options = {}) {
|
|
|
378
607
|
entry = entries.readSync();
|
|
379
608
|
continue;
|
|
380
609
|
}
|
|
381
|
-
const path =
|
|
610
|
+
const path = join3(dir, entry.name);
|
|
382
611
|
let sample;
|
|
383
612
|
try {
|
|
384
613
|
sample = sampleJournalFile(path, remainingBytes);
|
|
@@ -405,7 +634,7 @@ function listFlushingInDir(dir, bucket, options = {}) {
|
|
|
405
634
|
}
|
|
406
635
|
function listFlushing(options = {}) {
|
|
407
636
|
const envDir = currentEnvDir();
|
|
408
|
-
if (!
|
|
637
|
+
if (!existsSync3(envDir)) {
|
|
409
638
|
return [];
|
|
410
639
|
}
|
|
411
640
|
const out = [];
|
|
@@ -416,7 +645,7 @@ function listFlushing(options = {}) {
|
|
|
416
645
|
break;
|
|
417
646
|
}
|
|
418
647
|
if (entry.isDirectory()) {
|
|
419
|
-
const found = listFlushingInDir(
|
|
648
|
+
const found = listFlushingInDir(join3(envDir, entry.name), entry.name, {
|
|
420
649
|
sampleBytes: remainingBytes,
|
|
421
650
|
maxFiles: maxFiles - out.length
|
|
422
651
|
});
|
|
@@ -520,7 +749,7 @@ async function drainFlushingPath(flushingPath, client2 = getClient()) {
|
|
|
520
749
|
async function drainPath(path) {
|
|
521
750
|
const tmpPath = `${path}.flushing.${String(Date.now())}.${String(process.pid)}`;
|
|
522
751
|
try {
|
|
523
|
-
|
|
752
|
+
renameSync2(path, tmpPath);
|
|
524
753
|
} catch (err) {
|
|
525
754
|
if (err.code === "ENOENT") {
|
|
526
755
|
return 0;
|
|
@@ -529,7 +758,7 @@ async function drainPath(path) {
|
|
|
529
758
|
}
|
|
530
759
|
return drainFlushingPath(tmpPath);
|
|
531
760
|
}
|
|
532
|
-
function
|
|
761
|
+
function processIsAlive2(pid) {
|
|
533
762
|
try {
|
|
534
763
|
process.kill(pid, 0);
|
|
535
764
|
return true;
|
|
@@ -539,7 +768,7 @@ function processIsAlive(pid) {
|
|
|
539
768
|
}
|
|
540
769
|
function selectRecoverable(files, now, opts = {}) {
|
|
541
770
|
const quarantineMs = opts.quarantineMs ?? ORPHAN_QUARANTINE_MS;
|
|
542
|
-
const isAlive = opts.isAlive ??
|
|
771
|
+
const isAlive = opts.isAlive ?? processIsAlive2;
|
|
543
772
|
return files.filter((f) => {
|
|
544
773
|
if (f.pid === void 0) {
|
|
545
774
|
return now - f.mtimeMs > quarantineMs;
|
|
@@ -611,14 +840,14 @@ function flush() {
|
|
|
611
840
|
// src/daemon/client.ts
|
|
612
841
|
import { createConnection } from "net";
|
|
613
842
|
import { homedir as homedir3 } from "os";
|
|
614
|
-
import { join as
|
|
615
|
-
var SOCK_PATH =
|
|
616
|
-
var
|
|
843
|
+
import { join as join4 } from "path";
|
|
844
|
+
var SOCK_PATH = join4(homedir3(), ".config", "prim", "sock");
|
|
845
|
+
var DEFAULT_TIMEOUT_MS2 = 250;
|
|
617
846
|
var nextRequestId = 1;
|
|
618
847
|
function daemonRequest(method, params = {}, opts = {}) {
|
|
619
848
|
const id = nextRequestId++;
|
|
620
849
|
const request2 = { id, method, params };
|
|
621
|
-
const timeoutMs = opts.timeoutMs ??
|
|
850
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
622
851
|
return new Promise((resolve2) => {
|
|
623
852
|
let settled = false;
|
|
624
853
|
let socket;
|
|
@@ -705,17 +934,17 @@ function assertCallerEnvMatches(callerEnv, boundEnv) {
|
|
|
705
934
|
}
|
|
706
935
|
|
|
707
936
|
// src/daemon/health.ts
|
|
708
|
-
import { chmodSync, existsSync as
|
|
937
|
+
import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync4, renameSync as renameSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
709
938
|
import { homedir as homedir4 } from "os";
|
|
710
|
-
import { dirname as
|
|
711
|
-
var
|
|
939
|
+
import { dirname as dirname4, join as join5 } from "path";
|
|
940
|
+
var CONFIG_DIR_MODE2 = 448;
|
|
712
941
|
var HEALTH_FILE_MODE = 384;
|
|
713
942
|
var INGESTION_SLA_MS = 3e4;
|
|
714
943
|
var HEARTBEAT_FRESH_MS = 9e4;
|
|
715
944
|
var HEARTBEAT_RETRY_CAP_MS = 3e4;
|
|
716
945
|
var INGESTION_RETRY_BASE_MS = 5e3;
|
|
717
946
|
var INGESTION_RETRY_CAP_MS = 15e3;
|
|
718
|
-
var DAEMON_HEALTH_PATH =
|
|
947
|
+
var DAEMON_HEALTH_PATH = join5(homedir4(), ".config", "prim", "daemon-health.json");
|
|
719
948
|
function createDaemonHealthState(version, pid, startedAt2) {
|
|
720
949
|
return {
|
|
721
950
|
schemaVersion: 1,
|
|
@@ -753,37 +982,37 @@ function ingestionRetryDelayMs(consecutiveFailures, random = Math.random) {
|
|
|
753
982
|
return retryDelayMs(consecutiveFailures, INGESTION_RETRY_CAP_MS, random);
|
|
754
983
|
}
|
|
755
984
|
function writeDaemonHealthState(state, path = DAEMON_HEALTH_PATH) {
|
|
756
|
-
const dir =
|
|
757
|
-
if (!
|
|
758
|
-
|
|
985
|
+
const dir = dirname4(path);
|
|
986
|
+
if (!existsSync4(dir)) {
|
|
987
|
+
mkdirSync4(dir, { recursive: true, mode: CONFIG_DIR_MODE2 });
|
|
759
988
|
}
|
|
760
|
-
const tmp =
|
|
761
|
-
|
|
989
|
+
const tmp = join5(dir, `.${process.pid}.daemon-health.tmp`);
|
|
990
|
+
writeFileSync3(tmp, `${JSON.stringify(state, null, 2)}
|
|
762
991
|
`, { mode: HEALTH_FILE_MODE });
|
|
763
|
-
|
|
764
|
-
|
|
992
|
+
chmodSync2(tmp, HEALTH_FILE_MODE);
|
|
993
|
+
renameSync3(tmp, path);
|
|
765
994
|
}
|
|
766
995
|
|
|
767
996
|
// src/daemon/instance-lock.ts
|
|
768
997
|
import { randomUUID } from "crypto";
|
|
769
998
|
import {
|
|
770
|
-
chmodSync as
|
|
771
|
-
existsSync as
|
|
772
|
-
mkdirSync as
|
|
773
|
-
readFileSync as
|
|
774
|
-
renameSync as
|
|
775
|
-
rmSync,
|
|
776
|
-
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,
|
|
777
1006
|
unlinkSync as unlinkSync2,
|
|
778
|
-
writeFileSync as
|
|
1007
|
+
writeFileSync as writeFileSync4
|
|
779
1008
|
} from "fs";
|
|
780
|
-
import { join as
|
|
1009
|
+
import { join as join6 } from "path";
|
|
781
1010
|
var DIR_MODE = 448;
|
|
782
|
-
var
|
|
1011
|
+
var FILE_MODE2 = 384;
|
|
783
1012
|
var DAEMON_STARTUP_GRACE_MS = 5e3;
|
|
784
1013
|
function readLockRecord(path) {
|
|
785
1014
|
try {
|
|
786
|
-
const value = JSON.parse(
|
|
1015
|
+
const value = JSON.parse(readFileSync4(path, "utf-8"));
|
|
787
1016
|
if (Number.isInteger(value.pid) && typeof value.instanceId === "string" && typeof value.startedAt === "number") {
|
|
788
1017
|
return value;
|
|
789
1018
|
}
|
|
@@ -793,7 +1022,7 @@ function readLockRecord(path) {
|
|
|
793
1022
|
}
|
|
794
1023
|
function numericPid(path) {
|
|
795
1024
|
try {
|
|
796
|
-
const pid = Number(
|
|
1025
|
+
const pid = Number(readFileSync4(path, "utf-8").trim());
|
|
797
1026
|
return Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
798
1027
|
} catch {
|
|
799
1028
|
return;
|
|
@@ -808,16 +1037,16 @@ function daemonProcessIsAlive(pid) {
|
|
|
808
1037
|
}
|
|
809
1038
|
}
|
|
810
1039
|
function readDaemonOwner(configDir) {
|
|
811
|
-
return readLockRecord(
|
|
1040
|
+
return readLockRecord(join6(configDir, "daemon.lock", "owner.json"));
|
|
812
1041
|
}
|
|
813
1042
|
function readDaemonPid(configDir) {
|
|
814
|
-
const path =
|
|
1043
|
+
const path = join6(configDir, "daemon.pid");
|
|
815
1044
|
const pid = numericPid(path);
|
|
816
1045
|
if (pid === void 0) {
|
|
817
1046
|
return;
|
|
818
1047
|
}
|
|
819
1048
|
try {
|
|
820
|
-
return { pid, mtimeMs:
|
|
1049
|
+
return { pid, mtimeMs: statSync2(path).mtimeMs };
|
|
821
1050
|
} catch {
|
|
822
1051
|
return;
|
|
823
1052
|
}
|
|
@@ -826,29 +1055,29 @@ function acquireDaemonOwnership(configDir, opts = {}) {
|
|
|
826
1055
|
const pid = opts.pid ?? process.pid;
|
|
827
1056
|
const now = opts.now ?? Date.now();
|
|
828
1057
|
const isAlive = opts.isAlive ?? daemonProcessIsAlive;
|
|
829
|
-
const lockDir =
|
|
830
|
-
const
|
|
831
|
-
const pidPath =
|
|
1058
|
+
const lockDir = join6(configDir, "daemon.lock");
|
|
1059
|
+
const ownerPath2 = join6(lockDir, "owner.json");
|
|
1060
|
+
const pidPath = join6(configDir, "daemon.pid");
|
|
832
1061
|
const instanceId = randomUUID();
|
|
833
|
-
if (!
|
|
834
|
-
|
|
1062
|
+
if (!existsSync5(configDir)) {
|
|
1063
|
+
mkdirSync5(configDir, { recursive: true, mode: DIR_MODE });
|
|
835
1064
|
}
|
|
836
1065
|
for (; ; ) {
|
|
837
1066
|
try {
|
|
838
|
-
|
|
1067
|
+
mkdirSync5(lockDir, { mode: DIR_MODE });
|
|
839
1068
|
break;
|
|
840
1069
|
} catch (err) {
|
|
841
1070
|
if (err.code !== "EEXIST") {
|
|
842
1071
|
throw err;
|
|
843
1072
|
}
|
|
844
|
-
const existing = readLockRecord(
|
|
1073
|
+
const existing = readLockRecord(ownerPath2);
|
|
845
1074
|
if (existing && isAlive(existing.pid)) {
|
|
846
1075
|
throw new Error(`daemon already running (pid=${String(existing.pid)})`);
|
|
847
1076
|
}
|
|
848
1077
|
if (!existing) {
|
|
849
1078
|
let mtimeMs;
|
|
850
1079
|
try {
|
|
851
|
-
mtimeMs =
|
|
1080
|
+
mtimeMs = statSync2(lockDir).mtimeMs;
|
|
852
1081
|
} catch (statErr) {
|
|
853
1082
|
if (statErr.code === "ENOENT") {
|
|
854
1083
|
continue;
|
|
@@ -861,8 +1090,8 @@ function acquireDaemonOwnership(configDir, opts = {}) {
|
|
|
861
1090
|
}
|
|
862
1091
|
const stale = `${lockDir}.stale.${String(pid)}.${instanceId}`;
|
|
863
1092
|
try {
|
|
864
|
-
|
|
865
|
-
|
|
1093
|
+
renameSync4(lockDir, stale);
|
|
1094
|
+
rmSync3(stale, { recursive: true, force: true });
|
|
866
1095
|
} catch (renameErr) {
|
|
867
1096
|
if (renameErr.code !== "ENOENT") {
|
|
868
1097
|
throw renameErr;
|
|
@@ -873,22 +1102,22 @@ function acquireDaemonOwnership(configDir, opts = {}) {
|
|
|
873
1102
|
const ownership2 = {
|
|
874
1103
|
configDir,
|
|
875
1104
|
lockDir,
|
|
876
|
-
ownerPath,
|
|
1105
|
+
ownerPath: ownerPath2,
|
|
877
1106
|
pidPath,
|
|
878
1107
|
pid,
|
|
879
1108
|
instanceId
|
|
880
1109
|
};
|
|
881
1110
|
const record = { pid, instanceId, startedAt: now };
|
|
882
|
-
|
|
883
|
-
`, { mode:
|
|
884
|
-
|
|
1111
|
+
writeFileSync4(ownerPath2, `${JSON.stringify(record)}
|
|
1112
|
+
`, { mode: FILE_MODE2 });
|
|
1113
|
+
chmodSync3(ownerPath2, FILE_MODE2);
|
|
885
1114
|
const legacyPid = numericPid(pidPath);
|
|
886
1115
|
if (legacyPid !== void 0 && legacyPid !== pid && isAlive(legacyPid)) {
|
|
887
|
-
|
|
1116
|
+
rmSync3(lockDir, { recursive: true, force: true });
|
|
888
1117
|
throw new Error(`daemon already running (pid=${String(legacyPid)})`);
|
|
889
1118
|
}
|
|
890
|
-
|
|
891
|
-
|
|
1119
|
+
writeFileSync4(pidPath, String(pid), { mode: FILE_MODE2 });
|
|
1120
|
+
chmodSync3(pidPath, FILE_MODE2);
|
|
892
1121
|
return ownership2;
|
|
893
1122
|
}
|
|
894
1123
|
function ownsDaemonInstance(ownership2) {
|
|
@@ -918,7 +1147,7 @@ function releaseDaemonOwnership(ownership2, socketPath) {
|
|
|
918
1147
|
}
|
|
919
1148
|
}
|
|
920
1149
|
}
|
|
921
|
-
|
|
1150
|
+
rmSync3(ownership2.lockDir, { recursive: true, force: true });
|
|
922
1151
|
if (cleanupError) {
|
|
923
1152
|
throw cleanupError;
|
|
924
1153
|
}
|
|
@@ -926,8 +1155,8 @@ function releaseDaemonOwnership(ownership2, socketPath) {
|
|
|
926
1155
|
}
|
|
927
1156
|
|
|
928
1157
|
// src/daemon/server.ts
|
|
929
|
-
var CONFIG_DIR =
|
|
930
|
-
var SOCK_PATH2 =
|
|
1158
|
+
var CONFIG_DIR = join7(homedir5(), ".config", "prim");
|
|
1159
|
+
var SOCK_PATH2 = join7(CONFIG_DIR, "sock");
|
|
931
1160
|
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
932
1161
|
var INGESTION_POLL_INTERVAL_MS = 15e3;
|
|
933
1162
|
var TOKEN_CHECK_INTERVAL_MS = 6e4;
|
|
@@ -961,7 +1190,7 @@ function resolveRuntimeVersion() {
|
|
|
961
1190
|
}
|
|
962
1191
|
try {
|
|
963
1192
|
const here = fileURLToPath(new URL(".", import.meta.url));
|
|
964
|
-
const pkg = JSON.parse(
|
|
1193
|
+
const pkg = JSON.parse(readFileSync5(join7(here, "..", "..", "package.json"), "utf-8"));
|
|
965
1194
|
return pkg.version ?? "unknown";
|
|
966
1195
|
} catch {
|
|
967
1196
|
return "unknown";
|
|
@@ -1032,7 +1261,7 @@ async function takeOwnership() {
|
|
|
1032
1261
|
(pid) => pid !== void 0 && daemonProcessIsAlive(pid)
|
|
1033
1262
|
)
|
|
1034
1263
|
);
|
|
1035
|
-
if (
|
|
1264
|
+
if (existsSync6(SOCK_PATH2) || candidatePids.size > 0) {
|
|
1036
1265
|
const snapshot = await daemonRequest(
|
|
1037
1266
|
"status_snapshot",
|
|
1038
1267
|
{},
|
|
@@ -1116,6 +1345,9 @@ async function performHeartbeat() {
|
|
|
1116
1345
|
}
|
|
1117
1346
|
}
|
|
1118
1347
|
function sendHeartbeat() {
|
|
1348
|
+
if (shuttingDown || reauthHold) {
|
|
1349
|
+
return Promise.resolve();
|
|
1350
|
+
}
|
|
1119
1351
|
if (heartbeatInFlight) {
|
|
1120
1352
|
heartbeatRerunRequested = true;
|
|
1121
1353
|
return heartbeatInFlight;
|
|
@@ -1128,7 +1360,7 @@ function sendHeartbeat() {
|
|
|
1128
1360
|
do {
|
|
1129
1361
|
heartbeatRerunRequested = false;
|
|
1130
1362
|
await performHeartbeat();
|
|
1131
|
-
} while (heartbeatRerunRequested && !shuttingDown);
|
|
1363
|
+
} while (heartbeatRerunRequested && !(shuttingDown || reauthHold));
|
|
1132
1364
|
})().finally(() => {
|
|
1133
1365
|
heartbeatInFlight = void 0;
|
|
1134
1366
|
scheduleHeartbeat();
|
|
@@ -1149,6 +1381,9 @@ function scheduleHeartbeat() {
|
|
|
1149
1381
|
}, delay);
|
|
1150
1382
|
}
|
|
1151
1383
|
async function ensureTokenFresh() {
|
|
1384
|
+
if (resolveAuthCredential()?.source !== "token_file") {
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1152
1387
|
const expiresAt = getTokenExpiresAt();
|
|
1153
1388
|
if (!expiresAt) {
|
|
1154
1389
|
return;
|
|
@@ -1397,14 +1632,18 @@ function startSocketServer() {
|
|
|
1397
1632
|
});
|
|
1398
1633
|
}
|
|
1399
1634
|
function startTimers() {
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1635
|
+
if (isSessionEnded()) {
|
|
1636
|
+
enterReauthHold();
|
|
1637
|
+
} else {
|
|
1638
|
+
persistHealth();
|
|
1639
|
+
void sendHeartbeat();
|
|
1640
|
+
void runIngestionLoop();
|
|
1641
|
+
}
|
|
1403
1642
|
void runTokenCheckLoop();
|
|
1404
1643
|
}
|
|
1405
1644
|
async function runTokenCheckLoop() {
|
|
1406
1645
|
if (reauthHold) {
|
|
1407
|
-
if (!isSessionEnded()) {
|
|
1646
|
+
if (!isSessionEnded() && resolveAuthCredential()) {
|
|
1408
1647
|
exitReauthHold();
|
|
1409
1648
|
}
|
|
1410
1649
|
} else {
|