@primitive.ai/prim 0.1.0-alpha.45 → 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-ZWTZMTV5.js → chunk-ESLX7ISX.js} +1 -1
- package/dist/{chunk-6TRRGGLD.js → chunk-JT4Q333I.js} +1 -1
- package/dist/chunk-VQ2ZV2D5.js +502 -0
- package/dist/{chunk-6MKLCOEH.js → chunk-Z4VI6TNX.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/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
checkAffectedDecisions,
|
|
13
13
|
daemonOrDirectGet,
|
|
14
14
|
formatDecisionsWarning
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-Z4VI6TNX.js";
|
|
16
16
|
import {
|
|
17
17
|
JOURNAL_DIR,
|
|
18
18
|
SESSIONS_DIR,
|
|
@@ -21,10 +21,10 @@ import {
|
|
|
21
21
|
listFlushing,
|
|
22
22
|
pendingJournalStats,
|
|
23
23
|
readMovesFromPath
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-ESLX7ISX.js";
|
|
25
25
|
import {
|
|
26
26
|
fetchFeedbackCapability
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-JT4Q333I.js";
|
|
28
28
|
import {
|
|
29
29
|
inspectWorkspaceId
|
|
30
30
|
} from "./chunk-IMAIBPUC.js";
|
|
@@ -41,14 +41,18 @@ import {
|
|
|
41
41
|
import {
|
|
42
42
|
HttpError,
|
|
43
43
|
REFRESH_TOKEN_PATH,
|
|
44
|
-
TOKEN_EXPIRES_PATH,
|
|
45
44
|
TOKEN_FILE_PATH,
|
|
46
|
-
|
|
45
|
+
clearStoredCredentials,
|
|
46
|
+
commitCredentials,
|
|
47
47
|
getClient,
|
|
48
48
|
getSiteUrl,
|
|
49
49
|
getTokenExpiresAt,
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
isSessionEnded,
|
|
51
|
+
refreshToken,
|
|
52
|
+
resolveAuthCredential,
|
|
53
|
+
setStoredToken,
|
|
54
|
+
withFileLock
|
|
55
|
+
} from "./chunk-VQ2ZV2D5.js";
|
|
52
56
|
import {
|
|
53
57
|
daemonIsLive,
|
|
54
58
|
daemonRequest
|
|
@@ -56,7 +60,7 @@ import {
|
|
|
56
60
|
|
|
57
61
|
// src/index.ts
|
|
58
62
|
import { readFileSync as readFileSync12 } from "fs";
|
|
59
|
-
import { dirname as
|
|
63
|
+
import { dirname as dirname8, resolve as resolve6 } from "path";
|
|
60
64
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
61
65
|
import { Command } from "commander";
|
|
62
66
|
import updateNotifier from "update-notifier";
|
|
@@ -96,10 +100,9 @@ function registerActivationCommands(program2) {
|
|
|
96
100
|
// src/commands/auth.ts
|
|
97
101
|
import { exec } from "child_process";
|
|
98
102
|
import { createHash, randomBytes } from "crypto";
|
|
99
|
-
import { existsSync,
|
|
103
|
+
import { existsSync, readFileSync } from "fs";
|
|
100
104
|
import { createServer } from "http";
|
|
101
105
|
import { platform } from "os";
|
|
102
|
-
import { dirname } from "path";
|
|
103
106
|
|
|
104
107
|
// src/commands/auth-pages.ts
|
|
105
108
|
var FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="99.77 99.96 400.85 400.85" fill="currentColor">
|
|
@@ -248,12 +251,13 @@ var STATE_MISMATCH_HTML = renderPage({
|
|
|
248
251
|
});
|
|
249
252
|
|
|
250
253
|
// src/commands/auth.ts
|
|
251
|
-
var FILE_MODE = 384;
|
|
252
254
|
var LOCALHOST = "127.0.0.1";
|
|
253
255
|
var CALLBACK_PORT = 19876;
|
|
254
256
|
var CALLBACK_TIMEOUT_MS = 12e4;
|
|
255
257
|
var EXIT_OK = 0;
|
|
256
258
|
var EXIT_FAIL = 1;
|
|
259
|
+
var EXIT_UNREACHABLE = 2;
|
|
260
|
+
var AUTH_PROBE_TIMEOUT_MS = 1e4;
|
|
257
261
|
var HTML_HEADERS = { "Content-Type": "text/html; charset=utf-8" };
|
|
258
262
|
var BASE64_PLUS_RE = /\+/g;
|
|
259
263
|
var BASE64_SLASH_RE = /\//g;
|
|
@@ -271,13 +275,6 @@ function openBrowser(url) {
|
|
|
271
275
|
const cmd = os === "darwin" ? "open" : os === "win32" ? "start" : "xdg-open";
|
|
272
276
|
exec(`${cmd} "${url}"`);
|
|
273
277
|
}
|
|
274
|
-
function saveToken(token) {
|
|
275
|
-
const dir = dirname(TOKEN_FILE_PATH);
|
|
276
|
-
if (!existsSync(dir)) {
|
|
277
|
-
mkdirSync(dir, { recursive: true });
|
|
278
|
-
}
|
|
279
|
-
writeFileSync(TOKEN_FILE_PATH, token, { mode: FILE_MODE });
|
|
280
|
-
}
|
|
281
278
|
function resolveCallbackPage(params, expectedState) {
|
|
282
279
|
if (params.get("state") !== expectedState) {
|
|
283
280
|
return {
|
|
@@ -327,6 +324,123 @@ function reportSuccess() {
|
|
|
327
324
|
console.log(JSON.stringify({ authenticated: true, tokenFile: TOKEN_FILE_PATH }));
|
|
328
325
|
process.exitCode = EXIT_OK;
|
|
329
326
|
}
|
|
327
|
+
function hasStoredRefreshToken() {
|
|
328
|
+
if (!existsSync(REFRESH_TOKEN_PATH)) return false;
|
|
329
|
+
try {
|
|
330
|
+
return readFileSync(REFRESH_TOKEN_PATH, "utf-8").trim().length > 0;
|
|
331
|
+
} catch {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function authStatusResult(status, reason, source, refreshTokenPresent) {
|
|
336
|
+
const expiresAt = source === "token_file" ? getTokenExpiresAt() : void 0;
|
|
337
|
+
const expiresInMs = expiresAt === void 0 ? null : expiresAt - Date.now();
|
|
338
|
+
return {
|
|
339
|
+
authenticated: status === "valid",
|
|
340
|
+
status,
|
|
341
|
+
reason,
|
|
342
|
+
tokenFile: source === "token_file" ? TOKEN_FILE_PATH : null,
|
|
343
|
+
accessTokenExpiresInMs: expiresInMs,
|
|
344
|
+
accessTokenExpired: expiresInMs !== null && expiresInMs <= 0,
|
|
345
|
+
refreshTokenPresent,
|
|
346
|
+
warnings: source === "token_file" && !refreshTokenPresent ? ["no refresh token"] : []
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
async function verifyAuthStatus() {
|
|
350
|
+
const credential = resolveAuthCredential();
|
|
351
|
+
if (!credential) {
|
|
352
|
+
return authStatusResult("invalid", "missing_credentials", void 0, false);
|
|
353
|
+
}
|
|
354
|
+
const storedCredential = credential.source === "token_file";
|
|
355
|
+
const refreshPresent = storedCredential && hasStoredRefreshToken();
|
|
356
|
+
let accessToken = credential.token;
|
|
357
|
+
if (refreshPresent) {
|
|
358
|
+
try {
|
|
359
|
+
const refreshed = await refreshToken({
|
|
360
|
+
force: true,
|
|
361
|
+
signal: AbortSignal.timeout(AUTH_PROBE_TIMEOUT_MS)
|
|
362
|
+
});
|
|
363
|
+
if (!refreshed) {
|
|
364
|
+
const rejected = isSessionEnded();
|
|
365
|
+
return authStatusResult(
|
|
366
|
+
rejected ? "invalid" : "unreachable",
|
|
367
|
+
rejected ? "refresh_rejected" : "verification_unavailable",
|
|
368
|
+
credential.source,
|
|
369
|
+
true
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
accessToken = refreshed;
|
|
373
|
+
} catch {
|
|
374
|
+
const rejected = isSessionEnded();
|
|
375
|
+
return authStatusResult(
|
|
376
|
+
rejected ? "invalid" : "unreachable",
|
|
377
|
+
rejected ? "refresh_rejected" : "verification_unavailable",
|
|
378
|
+
credential.source,
|
|
379
|
+
true
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
const response = await fetch(`${getSiteUrl()}/api/cli/auth/status`, {
|
|
385
|
+
method: "GET",
|
|
386
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
387
|
+
signal: AbortSignal.timeout(AUTH_PROBE_TIMEOUT_MS)
|
|
388
|
+
});
|
|
389
|
+
if (response.ok) {
|
|
390
|
+
return authStatusResult("valid", "verified", credential.source, refreshPresent);
|
|
391
|
+
}
|
|
392
|
+
if (response.status === 401 || response.status === 403) {
|
|
393
|
+
return authStatusResult("invalid", "access_rejected", credential.source, refreshPresent);
|
|
394
|
+
}
|
|
395
|
+
return authStatusResult(
|
|
396
|
+
"unreachable",
|
|
397
|
+
"verification_unavailable",
|
|
398
|
+
credential.source,
|
|
399
|
+
refreshPresent
|
|
400
|
+
);
|
|
401
|
+
} catch {
|
|
402
|
+
return authStatusResult(
|
|
403
|
+
"unreachable",
|
|
404
|
+
"verification_unavailable",
|
|
405
|
+
credential.source,
|
|
406
|
+
refreshPresent
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function authStatusExitCode(status) {
|
|
411
|
+
if (status === "valid") return EXIT_OK;
|
|
412
|
+
return status === "invalid" ? EXIT_FAIL : EXIT_UNREACHABLE;
|
|
413
|
+
}
|
|
414
|
+
function writeHumanAuthStatus(result) {
|
|
415
|
+
if (result.status === "invalid") {
|
|
416
|
+
const detail = result.reason === "refresh_rejected" ? "The saved session was rejected." : result.reason === "access_rejected" ? "The selected access token was rejected." : "No credentials were found.";
|
|
417
|
+
console.log(`${detail} Run \`prim auth login\` to authenticate.`);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (result.status === "unreachable") {
|
|
421
|
+
console.log("Authentication could not be verified. Credentials were retained.");
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
console.log("Authenticated (verified).");
|
|
425
|
+
if (result.tokenFile) {
|
|
426
|
+
console.log(`Token file: ${result.tokenFile}`);
|
|
427
|
+
} else {
|
|
428
|
+
console.log("Token source: fixed environment credential");
|
|
429
|
+
}
|
|
430
|
+
if (result.accessTokenExpiresInMs === null) {
|
|
431
|
+
console.log("Access token expiry: unknown (no metadata)");
|
|
432
|
+
} else if (result.accessTokenExpiresInMs <= 0) {
|
|
433
|
+
console.log("Access token: expired");
|
|
434
|
+
} else {
|
|
435
|
+
const minutes = Math.floor(result.accessTokenExpiresInMs / 6e4);
|
|
436
|
+
const seconds = Math.floor(result.accessTokenExpiresInMs % 6e4 / 1e3);
|
|
437
|
+
console.log(`Access token expires in: ${minutes}m ${seconds}s`);
|
|
438
|
+
}
|
|
439
|
+
console.log(
|
|
440
|
+
`Refresh token: ${result.tokenFile ? result.refreshTokenPresent ? "present" : "missing" : "not applicable"}`
|
|
441
|
+
);
|
|
442
|
+
for (const warning of result.warnings) console.log(`Warning: ${warning}`);
|
|
443
|
+
}
|
|
330
444
|
function registerAuthCommands(program2) {
|
|
331
445
|
const auth = program2.command("auth").description("Manage CLI authentication");
|
|
332
446
|
auth.command("login").description("Authenticate via browser (WorkOS OAuth)").action(async () => {
|
|
@@ -417,27 +531,28 @@ ${authUrl.toString()}
|
|
|
417
531
|
return;
|
|
418
532
|
}
|
|
419
533
|
try {
|
|
420
|
-
const
|
|
421
|
-
|
|
534
|
+
const tokens = await exchangeCode(siteUrl, result.code, verifier, redirectUri);
|
|
535
|
+
await commitCredentials(tokens);
|
|
422
536
|
reportSuccess();
|
|
423
537
|
} catch (err) {
|
|
424
538
|
reportFailure("token_exchange_failed", err instanceof Error ? err.message : String(err));
|
|
425
539
|
}
|
|
426
540
|
});
|
|
427
|
-
auth.command("set-token <token>").description("Save a bearer token for authenticated CLI calls").action((token) => {
|
|
428
|
-
|
|
541
|
+
auth.command("set-token <token>").description("Save a bearer token for authenticated CLI calls").action(async (token) => {
|
|
542
|
+
await setStoredToken(token);
|
|
429
543
|
console.log(`Token saved to ${TOKEN_FILE_PATH}`);
|
|
430
544
|
});
|
|
431
545
|
auth.command("clear").description("Remove the saved authentication token").action(async () => {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
546
|
+
const removed = await clearStoredCredentials({
|
|
547
|
+
beforeClear: async (refreshTokenValue) => {
|
|
548
|
+
if (!refreshTokenValue) return;
|
|
435
549
|
try {
|
|
436
550
|
const siteUrl = getSiteUrl();
|
|
437
551
|
const res = await fetch(`${siteUrl}/mcp/broker/revoke`, {
|
|
438
552
|
method: "POST",
|
|
439
553
|
headers: { "Content-Type": "application/json" },
|
|
440
|
-
body: JSON.stringify({ refresh_token: refreshTokenValue })
|
|
554
|
+
body: JSON.stringify({ refresh_token: refreshTokenValue }),
|
|
555
|
+
signal: AbortSignal.timeout(AUTH_PROBE_TIMEOUT_MS)
|
|
441
556
|
});
|
|
442
557
|
if (res.ok) {
|
|
443
558
|
console.log("Server token revoked.");
|
|
@@ -451,62 +566,18 @@ ${authUrl.toString()}
|
|
|
451
566
|
console.warn("Could not reach server for revocation \u2014 clearing local files anyway.");
|
|
452
567
|
}
|
|
453
568
|
}
|
|
454
|
-
}
|
|
455
|
-
let removed = false;
|
|
456
|
-
for (const filePath of [TOKEN_FILE_PATH, REFRESH_TOKEN_PATH, TOKEN_EXPIRES_PATH]) {
|
|
457
|
-
if (existsSync(filePath)) {
|
|
458
|
-
rmSync(filePath);
|
|
459
|
-
removed = true;
|
|
460
|
-
}
|
|
461
|
-
}
|
|
569
|
+
});
|
|
462
570
|
if (removed) {
|
|
463
571
|
console.log("Local tokens removed.");
|
|
464
572
|
} else {
|
|
465
573
|
console.log("No saved tokens found.");
|
|
466
574
|
}
|
|
467
575
|
});
|
|
468
|
-
auth.command("status").description("Check authentication status and token expiry").option("--json", "Output as JSON").action((opts) => {
|
|
469
|
-
const
|
|
470
|
-
if (opts.json)
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const refreshPresent = existsSync(REFRESH_TOKEN_PATH);
|
|
474
|
-
printJson({
|
|
475
|
-
authenticated: !!token,
|
|
476
|
-
tokenFile: token ? TOKEN_FILE_PATH : null,
|
|
477
|
-
accessTokenExpiresInMs: expiresInMs,
|
|
478
|
-
accessTokenExpired: expiresInMs !== null && expiresInMs <= 0,
|
|
479
|
-
refreshTokenPresent: refreshPresent,
|
|
480
|
-
warnings: !token || refreshPresent ? [] : ["no refresh token"]
|
|
481
|
-
});
|
|
482
|
-
process.exit(token ? 0 : 1);
|
|
483
|
-
}
|
|
484
|
-
if (!token) {
|
|
485
|
-
console.log("Not authenticated. Run `prim auth login` to authenticate.");
|
|
486
|
-
process.exit(1);
|
|
487
|
-
}
|
|
488
|
-
console.log("Authenticated.");
|
|
489
|
-
console.log(`Token file: ${TOKEN_FILE_PATH}`);
|
|
490
|
-
const expiresAt = getTokenExpiresAt();
|
|
491
|
-
if (expiresAt) {
|
|
492
|
-
const remaining = expiresAt - Date.now();
|
|
493
|
-
if (remaining <= 0) {
|
|
494
|
-
console.log("Access token: expired");
|
|
495
|
-
} else {
|
|
496
|
-
const minutes = Math.floor(remaining / 6e4);
|
|
497
|
-
const seconds = Math.floor(remaining % 6e4 / 1e3);
|
|
498
|
-
console.log(`Access token expires in: ${minutes}m ${seconds}s`);
|
|
499
|
-
}
|
|
500
|
-
} else {
|
|
501
|
-
console.log("Access token expiry: unknown (no metadata)");
|
|
502
|
-
}
|
|
503
|
-
const hasRefresh = existsSync(REFRESH_TOKEN_PATH);
|
|
504
|
-
console.log(`Refresh token: ${hasRefresh ? "present" : "missing"}`);
|
|
505
|
-
if (!hasRefresh) {
|
|
506
|
-
console.log(
|
|
507
|
-
"Warning: No refresh token. Re-run `prim auth login` when access token expires."
|
|
508
|
-
);
|
|
509
|
-
}
|
|
576
|
+
auth.command("status").description("Check authentication status and token expiry").option("--json", "Output as JSON").action(async (opts) => {
|
|
577
|
+
const result = await verifyAuthStatus();
|
|
578
|
+
if (opts.json) printJson(result);
|
|
579
|
+
else writeHumanAuthStatus(result);
|
|
580
|
+
process.exit(authStatusExitCode(result.status));
|
|
510
581
|
});
|
|
511
582
|
}
|
|
512
583
|
async function exchangeCode(siteUrl, code, codeVerifier, redirectUri) {
|
|
@@ -524,19 +595,14 @@ async function exchangeCode(siteUrl, code, codeVerifier, redirectUri) {
|
|
|
524
595
|
throw new Error(`Token exchange failed (${response.status}): ${body}`);
|
|
525
596
|
}
|
|
526
597
|
const data = await response.json();
|
|
527
|
-
if (!data.access_token) {
|
|
528
|
-
throw new Error("
|
|
598
|
+
if (!data.access_token || !data.refresh_token) {
|
|
599
|
+
throw new Error("Token exchange did not return a complete credential pair");
|
|
529
600
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
}
|
|
536
|
-
writeFileSync(refreshPath, data.refresh_token, { mode: FILE_MODE });
|
|
537
|
-
}
|
|
538
|
-
saveTokenExpiry(data.access_token, data.expires_in);
|
|
539
|
-
return data.access_token;
|
|
601
|
+
return {
|
|
602
|
+
accessToken: data.access_token,
|
|
603
|
+
refreshToken: data.refresh_token,
|
|
604
|
+
expiresIn: data.expires_in
|
|
605
|
+
};
|
|
540
606
|
}
|
|
541
607
|
|
|
542
608
|
// src/commands/claude-install.ts
|
|
@@ -544,14 +610,14 @@ import {
|
|
|
544
610
|
closeSync,
|
|
545
611
|
existsSync as existsSync3,
|
|
546
612
|
fsyncSync,
|
|
547
|
-
mkdirSync as
|
|
613
|
+
mkdirSync as mkdirSync2,
|
|
548
614
|
openSync,
|
|
549
615
|
readFileSync as readFileSync3,
|
|
550
616
|
renameSync as renameSync2,
|
|
551
|
-
writeFileSync as
|
|
617
|
+
writeFileSync as writeFileSync2
|
|
552
618
|
} from "fs";
|
|
553
619
|
import { homedir as homedir2 } from "os";
|
|
554
|
-
import { dirname as
|
|
620
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
555
621
|
|
|
556
622
|
// src/daemon/launchd.ts
|
|
557
623
|
import { spawnSync } from "child_process";
|
|
@@ -560,17 +626,16 @@ import {
|
|
|
560
626
|
chmodSync,
|
|
561
627
|
copyFileSync,
|
|
562
628
|
existsSync as existsSync2,
|
|
563
|
-
mkdirSync
|
|
629
|
+
mkdirSync,
|
|
564
630
|
mkdtempSync,
|
|
565
631
|
readFileSync as readFileSync2,
|
|
566
632
|
renameSync,
|
|
567
|
-
rmSync
|
|
568
|
-
statSync,
|
|
633
|
+
rmSync,
|
|
569
634
|
symlinkSync,
|
|
570
|
-
writeFileSync
|
|
635
|
+
writeFileSync
|
|
571
636
|
} from "fs";
|
|
572
637
|
import { homedir } from "os";
|
|
573
|
-
import { dirname
|
|
638
|
+
import { dirname, isAbsolute, join, resolve } from "path";
|
|
574
639
|
var LAUNCHD_LABEL = "ai.getprimitive.prim-daemon";
|
|
575
640
|
var RUNTIME_SCHEMA_VERSION = 1;
|
|
576
641
|
var RUNTIME_DIR_MODE = 448;
|
|
@@ -580,9 +645,6 @@ var PLIST_FILE_MODE = 384;
|
|
|
580
645
|
var READY_TIMEOUT_MS = 15e3;
|
|
581
646
|
var READY_POLL_MS = 100;
|
|
582
647
|
var READY_PROBE_TIMEOUT_MS = 250;
|
|
583
|
-
var LIFECYCLE_LOCK_TIMEOUT_MS = 3e4;
|
|
584
|
-
var LIFECYCLE_LOCK_POLL_MS = 50;
|
|
585
|
-
var LIFECYCLE_LOCK_INIT_GRACE_MS = 2e3;
|
|
586
648
|
function xdgDataHome(options) {
|
|
587
649
|
const home = options.homeDir ?? homedir();
|
|
588
650
|
const configured = (options.env ?? process.env).XDG_DATA_HOME;
|
|
@@ -606,7 +668,7 @@ function runtimePaths(options = {}) {
|
|
|
606
668
|
};
|
|
607
669
|
}
|
|
608
670
|
function findPackageVersion(entryFile) {
|
|
609
|
-
let dir =
|
|
671
|
+
let dir = dirname(resolve(entryFile));
|
|
610
672
|
for (let depth = 0; depth < 6; depth++) {
|
|
611
673
|
const packagePath = join(dir, "package.json");
|
|
612
674
|
if (existsSync2(packagePath)) {
|
|
@@ -618,7 +680,7 @@ function findPackageVersion(entryFile) {
|
|
|
618
680
|
} catch {
|
|
619
681
|
}
|
|
620
682
|
}
|
|
621
|
-
const parent =
|
|
683
|
+
const parent = dirname(dir);
|
|
622
684
|
if (parent === dir) break;
|
|
623
685
|
dir = parent;
|
|
624
686
|
}
|
|
@@ -646,7 +708,7 @@ function atomicSymlink(target, linkPath) {
|
|
|
646
708
|
try {
|
|
647
709
|
renameSync(tempLink, linkPath);
|
|
648
710
|
} catch (error) {
|
|
649
|
-
|
|
711
|
+
rmSync(tempLink, { force: true });
|
|
650
712
|
throw error;
|
|
651
713
|
}
|
|
652
714
|
}
|
|
@@ -677,11 +739,11 @@ function stageRuntime(options = {}) {
|
|
|
677
739
|
RUNTIME_LAUNCHER_MODE
|
|
678
740
|
);
|
|
679
741
|
} else {
|
|
680
|
-
|
|
742
|
+
rmSync(paths.statuslineLauncher, { force: true });
|
|
681
743
|
}
|
|
682
744
|
return { changed: false, manifest: current, paths };
|
|
683
745
|
}
|
|
684
|
-
|
|
746
|
+
mkdirSync(paths.releasesDir, { recursive: true, mode: RUNTIME_DIR_MODE });
|
|
685
747
|
chmodSync(paths.runtimeDir, RUNTIME_DIR_MODE);
|
|
686
748
|
chmodSync(paths.releasesDir, RUNTIME_DIR_MODE);
|
|
687
749
|
const stagingDir = mkdtempSync(join(paths.releasesDir, ".stage-"));
|
|
@@ -696,7 +758,7 @@ function stageRuntime(options = {}) {
|
|
|
696
758
|
chmodSync(statuslineTarget, RUNTIME_FILE_MODE);
|
|
697
759
|
}
|
|
698
760
|
const manifestTarget = join(stagingDir, "manifest.json");
|
|
699
|
-
|
|
761
|
+
writeFileSync(manifestTarget, `${JSON.stringify(desired, null, 2)}
|
|
700
762
|
`, {
|
|
701
763
|
encoding: "utf8",
|
|
702
764
|
mode: RUNTIME_FILE_MODE,
|
|
@@ -715,10 +777,10 @@ function stageRuntime(options = {}) {
|
|
|
715
777
|
RUNTIME_LAUNCHER_MODE
|
|
716
778
|
);
|
|
717
779
|
} else {
|
|
718
|
-
|
|
780
|
+
rmSync(paths.statuslineLauncher, { force: true });
|
|
719
781
|
}
|
|
720
782
|
} catch (error) {
|
|
721
|
-
|
|
783
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
722
784
|
throw error;
|
|
723
785
|
}
|
|
724
786
|
return { changed: true, manifest: desired, paths };
|
|
@@ -815,13 +877,13 @@ function atomicWrite(path, content, mode) {
|
|
|
815
877
|
}
|
|
816
878
|
} catch {
|
|
817
879
|
}
|
|
818
|
-
|
|
880
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
819
881
|
const temp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
820
|
-
|
|
882
|
+
writeFileSync(temp, content, { encoding: "utf8", mode, flag: "wx" });
|
|
821
883
|
try {
|
|
822
884
|
renameSync(temp, path);
|
|
823
885
|
} catch (error) {
|
|
824
|
-
|
|
886
|
+
rmSync(temp, { force: true });
|
|
825
887
|
throw error;
|
|
826
888
|
}
|
|
827
889
|
return true;
|
|
@@ -849,95 +911,9 @@ function processIsAlive(pid) {
|
|
|
849
911
|
return error.code === "EPERM";
|
|
850
912
|
}
|
|
851
913
|
}
|
|
852
|
-
function lifecycleOwnerPath(lockDir) {
|
|
853
|
-
return join(lockDir, "owner.json");
|
|
854
|
-
}
|
|
855
|
-
function readLifecycleOwner(lockDir) {
|
|
856
|
-
try {
|
|
857
|
-
const owner = JSON.parse(
|
|
858
|
-
readFileSync2(lifecycleOwnerPath(lockDir), "utf8")
|
|
859
|
-
);
|
|
860
|
-
if (!Number.isInteger(owner.pid) || owner.pid <= 0 || typeof owner.nonce !== "string" || typeof owner.createdAt !== "number") {
|
|
861
|
-
return null;
|
|
862
|
-
}
|
|
863
|
-
return owner;
|
|
864
|
-
} catch {
|
|
865
|
-
return null;
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
function sameLifecycleOwner(a, b) {
|
|
869
|
-
return a?.pid === b.pid && a.nonce === b.nonce && a.createdAt === b.createdAt;
|
|
870
|
-
}
|
|
871
|
-
function tryTakeLifecycleLock(lockDir, now) {
|
|
872
|
-
try {
|
|
873
|
-
mkdirSync2(lockDir, { mode: RUNTIME_DIR_MODE });
|
|
874
|
-
} catch (error) {
|
|
875
|
-
if (error.code === "EEXIST") return null;
|
|
876
|
-
throw error;
|
|
877
|
-
}
|
|
878
|
-
const owner = {
|
|
879
|
-
pid: process.pid,
|
|
880
|
-
nonce: createHash2("sha256").update(`${process.pid}\0${now}\0${Math.random()}`).digest("hex"),
|
|
881
|
-
createdAt: now
|
|
882
|
-
};
|
|
883
|
-
try {
|
|
884
|
-
writeFileSync2(lifecycleOwnerPath(lockDir), `${JSON.stringify(owner)}
|
|
885
|
-
`, {
|
|
886
|
-
encoding: "utf8",
|
|
887
|
-
mode: RUNTIME_FILE_MODE,
|
|
888
|
-
flag: "wx"
|
|
889
|
-
});
|
|
890
|
-
return owner;
|
|
891
|
-
} catch (error) {
|
|
892
|
-
rmSync2(lockDir, { recursive: true, force: true });
|
|
893
|
-
throw error;
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
function recoverStaleLifecycleLock(lockDir, now, alive) {
|
|
897
|
-
const owner = readLifecycleOwner(lockDir);
|
|
898
|
-
if (owner) {
|
|
899
|
-
if (alive(owner.pid)) return false;
|
|
900
|
-
if (!sameLifecycleOwner(readLifecycleOwner(lockDir), owner)) return false;
|
|
901
|
-
rmSync2(lockDir, { recursive: true, force: true });
|
|
902
|
-
return true;
|
|
903
|
-
}
|
|
904
|
-
try {
|
|
905
|
-
const before = statSync(lockDir).mtimeMs;
|
|
906
|
-
if (now - before < LIFECYCLE_LOCK_INIT_GRACE_MS) return false;
|
|
907
|
-
if (readLifecycleOwner(lockDir)) return false;
|
|
908
|
-
if (statSync(lockDir).mtimeMs !== before) return false;
|
|
909
|
-
rmSync2(lockDir, { recursive: true, force: true });
|
|
910
|
-
return true;
|
|
911
|
-
} catch {
|
|
912
|
-
return true;
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
914
|
async function withDaemonLifecycleLock(operation, options = {}) {
|
|
916
915
|
const lockDir = runtimePaths(options).lifecycleLockDir;
|
|
917
|
-
|
|
918
|
-
const control = options.lifecycleLock;
|
|
919
|
-
const nowMs = control?.nowMs ?? Date.now;
|
|
920
|
-
const sleepFor = control?.sleep ?? defaultSleep;
|
|
921
|
-
const alive = control?.processAlive ?? processIsAlive;
|
|
922
|
-
const deadline = nowMs() + (control?.timeoutMs ?? LIFECYCLE_LOCK_TIMEOUT_MS);
|
|
923
|
-
let owner = null;
|
|
924
|
-
while (!owner) {
|
|
925
|
-
const now = nowMs();
|
|
926
|
-
owner = tryTakeLifecycleLock(lockDir, now);
|
|
927
|
-
if (owner) break;
|
|
928
|
-
recoverStaleLifecycleLock(lockDir, now, alive);
|
|
929
|
-
if (now >= deadline) {
|
|
930
|
-
throw new Error(`timed out waiting for daemon lifecycle lock ${lockDir}`);
|
|
931
|
-
}
|
|
932
|
-
await sleepFor(control?.pollMs ?? LIFECYCLE_LOCK_POLL_MS);
|
|
933
|
-
}
|
|
934
|
-
try {
|
|
935
|
-
return await operation();
|
|
936
|
-
} finally {
|
|
937
|
-
if (sameLifecycleOwner(readLifecycleOwner(lockDir), owner)) {
|
|
938
|
-
rmSync2(lockDir, { recursive: true, force: true });
|
|
939
|
-
}
|
|
940
|
-
}
|
|
916
|
+
return withFileLock(lockDir, operation, options.lifecycleLock);
|
|
941
917
|
}
|
|
942
918
|
async function stopVerifiedLegacyDaemon(homeDir) {
|
|
943
919
|
const pidPath = join(homeDir, ".config", "prim", "daemon.pid");
|
|
@@ -975,11 +951,11 @@ function daemonExplicitlyDisabled(options = {}) {
|
|
|
975
951
|
function setDaemonExplicitlyDisabled(disabled, options = {}) {
|
|
976
952
|
const marker = runtimePaths(options).disabledMarker;
|
|
977
953
|
if (!disabled) {
|
|
978
|
-
|
|
954
|
+
rmSync(marker, { force: true });
|
|
979
955
|
return;
|
|
980
956
|
}
|
|
981
|
-
|
|
982
|
-
|
|
957
|
+
mkdirSync(dirname(marker), { recursive: true, mode: RUNTIME_DIR_MODE });
|
|
958
|
+
writeFileSync(marker, "disabled by `prim daemon stop`\n", {
|
|
983
959
|
encoding: "utf8",
|
|
984
960
|
mode: RUNTIME_FILE_MODE
|
|
985
961
|
});
|
|
@@ -1002,8 +978,8 @@ async function ensureMacDaemonLocked(options) {
|
|
|
1002
978
|
if (options.explicitlyStarted) setDaemonExplicitlyDisabled(false, options);
|
|
1003
979
|
const runtime = stageRuntime(options);
|
|
1004
980
|
const servicePaths = launchdPaths(options);
|
|
1005
|
-
|
|
1006
|
-
|
|
981
|
+
mkdirSync(dirname(servicePaths.logPath), { recursive: true, mode: RUNTIME_DIR_MODE });
|
|
982
|
+
writeFileSync(servicePaths.logPath, "", { mode: RUNTIME_FILE_MODE, flag: "a" });
|
|
1007
983
|
chmodSync(servicePaths.logPath, RUNTIME_FILE_MODE);
|
|
1008
984
|
const plist = generateLaunchAgentPlist({
|
|
1009
985
|
nodePath: runtime.manifest.nodePath,
|
|
@@ -1318,12 +1294,12 @@ function isGateInstalled(settings) {
|
|
|
1318
1294
|
return (settings.hooks?.PreToolUse ?? []).some((e) => entryHasCommand(e, GATE_BIN));
|
|
1319
1295
|
}
|
|
1320
1296
|
function atomicWrite2(path, content) {
|
|
1321
|
-
const dir =
|
|
1297
|
+
const dir = dirname2(path);
|
|
1322
1298
|
if (!existsSync3(dir)) {
|
|
1323
|
-
|
|
1299
|
+
mkdirSync2(dir, { recursive: true });
|
|
1324
1300
|
}
|
|
1325
1301
|
const tmp = `${path}.tmp.${String(Date.now())}`;
|
|
1326
|
-
|
|
1302
|
+
writeFileSync2(tmp, `${JSON.stringify(content, null, JSON_INDENT)}
|
|
1327
1303
|
`, "utf-8");
|
|
1328
1304
|
const fd = openSync(tmp, "r+");
|
|
1329
1305
|
try {
|
|
@@ -1657,7 +1633,7 @@ ${line("project", result.project)}`);
|
|
|
1657
1633
|
|
|
1658
1634
|
// src/commands/daemon.ts
|
|
1659
1635
|
import { spawn } from "child_process";
|
|
1660
|
-
import { closeSync as closeSync2, existsSync as existsSync4, mkdirSync as
|
|
1636
|
+
import { closeSync as closeSync2, existsSync as existsSync4, mkdirSync as mkdirSync3, openSync as openSync2, readFileSync as readFileSync4, unlinkSync } from "fs";
|
|
1661
1637
|
import { homedir as homedir4 } from "os";
|
|
1662
1638
|
import { join as join4 } from "path";
|
|
1663
1639
|
|
|
@@ -1727,6 +1703,30 @@ var HEALTHY_POLL_MS = 250;
|
|
|
1727
1703
|
var EXIT_OK2 = 0;
|
|
1728
1704
|
var EXIT_NOT_RUNNING = 2;
|
|
1729
1705
|
var EXIT_BOOTING = 3;
|
|
1706
|
+
var MAX_HEALTH_ERROR_LENGTH = 240;
|
|
1707
|
+
function boundedHealthError(value) {
|
|
1708
|
+
if (!value) return void 0;
|
|
1709
|
+
const clean = stripControlChars(value).replace(/\s+/g, " ").trim();
|
|
1710
|
+
if (!clean) return void 0;
|
|
1711
|
+
return clean.length <= MAX_HEALTH_ERROR_LENGTH ? clean : `${clean.slice(0, MAX_HEALTH_ERROR_LENGTH - 1)}\u2026`;
|
|
1712
|
+
}
|
|
1713
|
+
function daemonDegradedReason(snapshot) {
|
|
1714
|
+
if (!snapshot || snapshot.healthy !== false) return void 0;
|
|
1715
|
+
if (snapshot.needsReauth) {
|
|
1716
|
+
return "authentication requires `prim auth login`";
|
|
1717
|
+
}
|
|
1718
|
+
if (snapshot.heartbeat?.healthy === false) {
|
|
1719
|
+
const detail = boundedHealthError(snapshot.heartbeat.lastError);
|
|
1720
|
+
return `heartbeat unhealthy${detail ? `: ${detail}` : ""}`;
|
|
1721
|
+
}
|
|
1722
|
+
if (snapshot.ingestion?.healthy === false) {
|
|
1723
|
+
const detail = boundedHealthError(snapshot.ingestion.lastError);
|
|
1724
|
+
const pending = snapshot.ingestion.pendingCount;
|
|
1725
|
+
const qualifier = snapshot.ingestion.pendingSampled ? "at least " : "";
|
|
1726
|
+
return `ingestion unhealthy${typeof pending === "number" ? ` (${qualifier}${String(pending)} pending)` : ""}${detail ? `: ${detail}` : ""}`;
|
|
1727
|
+
}
|
|
1728
|
+
return "health checks have not recovered";
|
|
1729
|
+
}
|
|
1730
1730
|
function readPidfile() {
|
|
1731
1731
|
if (!existsSync4(PID_PATH)) {
|
|
1732
1732
|
return null;
|
|
@@ -1765,7 +1765,7 @@ function spawnDaemon(options) {
|
|
|
1765
1765
|
}
|
|
1766
1766
|
function openDaemonLog(configDir = CONFIG_DIR) {
|
|
1767
1767
|
if (!existsSync4(configDir)) {
|
|
1768
|
-
|
|
1768
|
+
mkdirSync3(configDir, { recursive: true, mode: CONFIG_DIR_MODE });
|
|
1769
1769
|
}
|
|
1770
1770
|
return openSync2(join4(configDir, "daemon.log"), "a", LOG_FILE_MODE);
|
|
1771
1771
|
}
|
|
@@ -1805,6 +1805,15 @@ async function verifiedPid(existing) {
|
|
|
1805
1805
|
function daemonStartIsHealthy(serviceReady, snapshot, expectedVersion) {
|
|
1806
1806
|
return serviceReady && snapshot?.healthy === true && expectedVersion !== void 0 && snapshot.version === expectedVersion;
|
|
1807
1807
|
}
|
|
1808
|
+
function daemonStartHealthFields(healthy, snapshot) {
|
|
1809
|
+
if (healthy) return {};
|
|
1810
|
+
return {
|
|
1811
|
+
state: "degraded",
|
|
1812
|
+
needsReauth: snapshot?.needsReauth === true,
|
|
1813
|
+
heartbeat: snapshot?.heartbeat ?? null,
|
|
1814
|
+
ingestion: snapshot?.ingestion ?? null
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1808
1817
|
async function detachedDaemonStart(opts) {
|
|
1809
1818
|
const existing = readPidfile();
|
|
1810
1819
|
if (existing?.alive) {
|
|
@@ -1961,8 +1970,9 @@ async function macDaemonStart(forceRestart = false) {
|
|
|
1961
1970
|
`
|
|
1962
1971
|
);
|
|
1963
1972
|
} else {
|
|
1973
|
+
const reason = daemonDegradedReason(snapshot);
|
|
1964
1974
|
process.stderr.write(
|
|
1965
|
-
`[prim] \u2717 launchd daemon did not reach healthy heartbeat + ingestion state (see ${LOG_PATH})
|
|
1975
|
+
`[prim] \u2717 launchd daemon did not reach healthy heartbeat + ingestion state${reason ? ` \xB7 ${reason}` : ""} (see ${LOG_PATH})
|
|
1966
1976
|
`
|
|
1967
1977
|
);
|
|
1968
1978
|
if (!process.exitCode) process.exitCode = EXIT_NOT_RUNNING;
|
|
@@ -1977,6 +1987,7 @@ async function macDaemonStart(forceRestart = false) {
|
|
|
1977
1987
|
loaded: result.service.loaded,
|
|
1978
1988
|
responding: result.responding,
|
|
1979
1989
|
healthy,
|
|
1990
|
+
...daemonStartHealthFields(healthy, snapshot),
|
|
1980
1991
|
version: snapshot?.version,
|
|
1981
1992
|
expectedVersion
|
|
1982
1993
|
},
|
|
@@ -2138,8 +2149,9 @@ function writeLiveSnapshot(snapshot, supervised = false) {
|
|
|
2138
2149
|
}
|
|
2139
2150
|
const team = snapshot.onlineNames !== void 0 ? ` \xB7 team: ${formatTeammates(snapshot.onlineNames, Number.POSITIVE_INFINITY)}` : "";
|
|
2140
2151
|
if (snapshot.healthy === false) {
|
|
2152
|
+
const reason = daemonDegradedReason(snapshot);
|
|
2141
2153
|
process.stderr.write(
|
|
2142
|
-
`[prim] \u2717 daemon unhealthy${supervised ? " under launchd" : ""} \xB7 pid=${snapshot.pid}${team}
|
|
2154
|
+
`[prim] \u2717 daemon unhealthy${supervised ? " under launchd" : ""} \xB7 pid=${snapshot.pid}${team}${reason ? ` \xB7 ${reason}` : ""}
|
|
2143
2155
|
`
|
|
2144
2156
|
);
|
|
2145
2157
|
return;
|
|
@@ -3147,12 +3159,13 @@ function classifyDoctor(checks) {
|
|
|
3147
3159
|
exitCode: status === "fail" ? EXIT_UNHEALTHY : 0
|
|
3148
3160
|
};
|
|
3149
3161
|
}
|
|
3150
|
-
function
|
|
3151
|
-
if (!
|
|
3162
|
+
function classifyAuthCredential(credential, expiresAt, hasRefresh) {
|
|
3163
|
+
if (!credential) {
|
|
3152
3164
|
return { name: "auth", status: "fail", detail: "no token \u2014 run `prim auth login`" };
|
|
3153
3165
|
}
|
|
3154
|
-
|
|
3155
|
-
|
|
3166
|
+
if (credential.source !== "token_file") {
|
|
3167
|
+
return { name: "auth", status: "ok", detail: "valid fixed bearer credential" };
|
|
3168
|
+
}
|
|
3156
3169
|
if (expiresAt !== void 0 && Date.now() >= expiresAt) {
|
|
3157
3170
|
return hasRefresh ? { name: "auth", status: "warn", detail: "access token expired (refresh available)" } : {
|
|
3158
3171
|
name: "auth",
|
|
@@ -3166,6 +3179,15 @@ function checkAuth() {
|
|
|
3166
3179
|
const detail = expiresAt !== void 0 ? `valid (${String(Math.round((expiresAt - Date.now()) / MS_PER_SECOND))}s left)` : "valid";
|
|
3167
3180
|
return { name: "auth", status: "ok", detail };
|
|
3168
3181
|
}
|
|
3182
|
+
function checkAuth() {
|
|
3183
|
+
const credential = resolveAuthCredential();
|
|
3184
|
+
const storedCredential = credential?.source === "token_file";
|
|
3185
|
+
return classifyAuthCredential(
|
|
3186
|
+
credential,
|
|
3187
|
+
storedCredential ? getTokenExpiresAt() : void 0,
|
|
3188
|
+
storedCredential && existsSync5(REFRESH_TOKEN_PATH)
|
|
3189
|
+
);
|
|
3190
|
+
}
|
|
3169
3191
|
function classifyDaemonHealth(snapshot, options = {}) {
|
|
3170
3192
|
if (options.disabled) {
|
|
3171
3193
|
return {
|
|
@@ -3470,15 +3492,15 @@ import {
|
|
|
3470
3492
|
closeSync as closeSync3,
|
|
3471
3493
|
existsSync as existsSync6,
|
|
3472
3494
|
fsyncSync as fsyncSync2,
|
|
3473
|
-
mkdirSync as
|
|
3495
|
+
mkdirSync as mkdirSync4,
|
|
3474
3496
|
openSync as openSync3,
|
|
3475
3497
|
readFileSync as readFileSync5,
|
|
3476
3498
|
renameSync as renameSync3,
|
|
3477
|
-
rmSync as
|
|
3478
|
-
writeFileSync as
|
|
3499
|
+
rmSync as rmSync2,
|
|
3500
|
+
writeFileSync as writeFileSync3
|
|
3479
3501
|
} from "fs";
|
|
3480
3502
|
import { homedir as homedir5 } from "os";
|
|
3481
|
-
import { dirname as
|
|
3503
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
3482
3504
|
import { Document, parseDocument, stringify } from "yaml";
|
|
3483
3505
|
var CAPTURE_BIN3 = "prim-hook";
|
|
3484
3506
|
var GATE_BIN3 = "prim-pre-tool-use";
|
|
@@ -3667,12 +3689,12 @@ function setAutoAccept(raw) {
|
|
|
3667
3689
|
`;
|
|
3668
3690
|
}
|
|
3669
3691
|
function atomicWriteFile(path, content) {
|
|
3670
|
-
const dir =
|
|
3692
|
+
const dir = dirname3(path);
|
|
3671
3693
|
if (!existsSync6(dir)) {
|
|
3672
|
-
|
|
3694
|
+
mkdirSync4(dir, { recursive: true });
|
|
3673
3695
|
}
|
|
3674
3696
|
const tmp = `${path}.tmp.${String(process.pid)}`;
|
|
3675
|
-
|
|
3697
|
+
writeFileSync3(tmp, content, "utf-8");
|
|
3676
3698
|
const fd = openSync3(tmp, "r+");
|
|
3677
3699
|
try {
|
|
3678
3700
|
fsyncSync2(fd);
|
|
@@ -3692,15 +3714,15 @@ function assertMergeValid(before, after) {
|
|
|
3692
3714
|
}
|
|
3693
3715
|
function writeShim() {
|
|
3694
3716
|
const path = shimPath();
|
|
3695
|
-
const dir =
|
|
3717
|
+
const dir = dirname3(path);
|
|
3696
3718
|
if (!existsSync6(dir)) {
|
|
3697
|
-
|
|
3719
|
+
mkdirSync4(dir, { recursive: true });
|
|
3698
3720
|
}
|
|
3699
|
-
|
|
3721
|
+
writeFileSync3(path, SHIM_SCRIPT, "utf-8");
|
|
3700
3722
|
chmodSync2(path, SHIM_MODE);
|
|
3701
3723
|
}
|
|
3702
3724
|
function removeShim() {
|
|
3703
|
-
|
|
3725
|
+
rmSync2(shimPath(), { force: true });
|
|
3704
3726
|
}
|
|
3705
3727
|
function autoAcceptOf(raw) {
|
|
3706
3728
|
if (raw.trim().length === 0) {
|
|
@@ -3808,9 +3830,9 @@ function registerHermesCommands(program2) {
|
|
|
3808
3830
|
|
|
3809
3831
|
// src/commands/hooks.ts
|
|
3810
3832
|
import { execFileSync } from "child_process";
|
|
3811
|
-
import { existsSync as existsSync7, mkdirSync as
|
|
3833
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
3812
3834
|
import { homedir as homedir6 } from "os";
|
|
3813
|
-
import { dirname as
|
|
3835
|
+
import { dirname as dirname4, join as join6, resolve as resolve2 } from "path";
|
|
3814
3836
|
import { Option } from "commander";
|
|
3815
3837
|
var PRE_COMMIT = { hookName: "pre-commit", binName: "prim-pre-commit" };
|
|
3816
3838
|
var POST_COMMIT = { hookName: "post-commit", binName: "prim-post-commit" };
|
|
@@ -3863,12 +3885,12 @@ function mergePrimBlock(hookPath, block, binName) {
|
|
|
3863
3885
|
const existing = readFileSync6(hookPath, "utf-8");
|
|
3864
3886
|
if (containsPrimHook(existing, binName)) return false;
|
|
3865
3887
|
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
3866
|
-
|
|
3888
|
+
writeFileSync4(hookPath, `${existing}${separator}${block}
|
|
3867
3889
|
`, { mode: 493 });
|
|
3868
3890
|
return true;
|
|
3869
3891
|
}
|
|
3870
|
-
|
|
3871
|
-
|
|
3892
|
+
mkdirSync5(dirname4(hookPath), { recursive: true });
|
|
3893
|
+
writeFileSync4(hookPath, `#!/bin/sh
|
|
3872
3894
|
# ${PRIM_CREATED_MARK}
|
|
3873
3895
|
|
|
3874
3896
|
${block}
|
|
@@ -3931,7 +3953,7 @@ function installToDotGit(gitRoot, spec = PRE_COMMIT) {
|
|
|
3931
3953
|
const hooksDir = resolve2(gitRoot, ".git", "hooks");
|
|
3932
3954
|
const hookPath = resolve2(hooksDir, spec.hookName);
|
|
3933
3955
|
if (!existsSync7(hooksDir)) {
|
|
3934
|
-
|
|
3956
|
+
mkdirSync5(hooksDir, { recursive: true });
|
|
3935
3957
|
}
|
|
3936
3958
|
if (existsSync7(hookPath)) {
|
|
3937
3959
|
const existing = readFileSync6(hookPath, "utf-8");
|
|
@@ -3943,7 +3965,7 @@ function installToDotGit(gitRoot, spec = PRE_COMMIT) {
|
|
|
3943
3965
|
console.log("To replace it, run: prim hooks uninstall && prim hooks install");
|
|
3944
3966
|
return;
|
|
3945
3967
|
}
|
|
3946
|
-
|
|
3968
|
+
writeFileSync4(hookPath, dotGitScript(spec), { mode: 493 });
|
|
3947
3969
|
console.log(`Installed ${spec.hookName} hook at ${hookPath}`);
|
|
3948
3970
|
}
|
|
3949
3971
|
var PRIM_GIT_HOOKS_DIR = join6(homedir6(), ".config", "prim", "git-hooks");
|
|
@@ -4014,15 +4036,15 @@ function ownedHookNames() {
|
|
|
4014
4036
|
}
|
|
4015
4037
|
function writeOwnHooks() {
|
|
4016
4038
|
if (!existsSync7(PRIM_GIT_HOOKS_DIR)) {
|
|
4017
|
-
|
|
4039
|
+
mkdirSync5(PRIM_GIT_HOOKS_DIR, { recursive: true });
|
|
4018
4040
|
}
|
|
4019
4041
|
for (const spec of HOOKS) {
|
|
4020
|
-
|
|
4042
|
+
writeFileSync4(resolve2(PRIM_GIT_HOOKS_DIR, spec.hookName), globalHookScript(spec), {
|
|
4021
4043
|
mode: 493
|
|
4022
4044
|
});
|
|
4023
4045
|
}
|
|
4024
4046
|
for (const name of PASSTHROUGH_HOOKS) {
|
|
4025
|
-
|
|
4047
|
+
writeFileSync4(resolve2(PRIM_GIT_HOOKS_DIR, name), passThroughScript(name), { mode: 493 });
|
|
4026
4048
|
}
|
|
4027
4049
|
}
|
|
4028
4050
|
function appendPrimBlock(hookPath, spec) {
|
|
@@ -4042,7 +4064,7 @@ function stripPrimBlock(hookPath, spec) {
|
|
|
4042
4064
|
unlinkSync2(hookPath);
|
|
4043
4065
|
return;
|
|
4044
4066
|
}
|
|
4045
|
-
|
|
4067
|
+
writeFileSync4(hookPath, out, { mode: 493 });
|
|
4046
4068
|
}
|
|
4047
4069
|
function installGlobalHooks(opts = {}) {
|
|
4048
4070
|
const global = gitConfigGet("--global");
|
|
@@ -4190,7 +4212,7 @@ function registerHooksCommands(program2) {
|
|
|
4190
4212
|
}
|
|
4191
4213
|
|
|
4192
4214
|
// src/commands/moves.ts
|
|
4193
|
-
import { existsSync as existsSync8, mkdirSync as
|
|
4215
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
4194
4216
|
import { join as join7 } from "path";
|
|
4195
4217
|
|
|
4196
4218
|
// src/flusher.ts
|
|
@@ -4355,7 +4377,7 @@ var EVENT_COL_WIDTH = 20;
|
|
|
4355
4377
|
var BUCKET_COL_WIDTH = 20;
|
|
4356
4378
|
var TAIL_BUCKET_COL_WIDTH = 12;
|
|
4357
4379
|
var DIR_MODE = 448;
|
|
4358
|
-
var
|
|
4380
|
+
var FILE_MODE = 384;
|
|
4359
4381
|
var WORKSPACE_FILE = ".prim/workspace.json";
|
|
4360
4382
|
function registerMovesCommands(program2) {
|
|
4361
4383
|
const moves = program2.command("moves").description("Decision Event Pipeline \u2014 local journal");
|
|
@@ -4421,11 +4443,11 @@ function registerMovesCommands(program2) {
|
|
|
4421
4443
|
moves.command("bind").description("Pin the current directory to an org via .prim/workspace.json").requiredOption("--orgId <orgId>", "Convex organization id").action((opts) => {
|
|
4422
4444
|
const dir = join7(process.cwd(), ".prim");
|
|
4423
4445
|
if (!existsSync8(dir)) {
|
|
4424
|
-
|
|
4446
|
+
mkdirSync6(dir, { recursive: true, mode: DIR_MODE });
|
|
4425
4447
|
}
|
|
4426
4448
|
const file = join7(process.cwd(), WORKSPACE_FILE);
|
|
4427
|
-
|
|
4428
|
-
mode:
|
|
4449
|
+
writeFileSync5(file, JSON.stringify({ orgId: opts.orgId, boundAt: Date.now() }, null, 2), {
|
|
4450
|
+
mode: FILE_MODE
|
|
4429
4451
|
});
|
|
4430
4452
|
console.log(`[prim] bound ${process.cwd()} to org ${opts.orgId}`);
|
|
4431
4453
|
});
|
|
@@ -4525,18 +4547,18 @@ function registerReconcileCommands(program2) {
|
|
|
4525
4547
|
// src/commands/session.ts
|
|
4526
4548
|
import {
|
|
4527
4549
|
existsSync as existsSync9,
|
|
4528
|
-
mkdirSync as
|
|
4550
|
+
mkdirSync as mkdirSync7,
|
|
4529
4551
|
readFileSync as readFileSync7,
|
|
4530
4552
|
readdirSync,
|
|
4531
4553
|
unlinkSync as unlinkSync5,
|
|
4532
|
-
writeFileSync as
|
|
4554
|
+
writeFileSync as writeFileSync6
|
|
4533
4555
|
} from "fs";
|
|
4534
4556
|
import { join as join8 } from "path";
|
|
4535
4557
|
var DIR_MODE2 = 448;
|
|
4536
|
-
var
|
|
4558
|
+
var FILE_MODE2 = 384;
|
|
4537
4559
|
function ensureDir() {
|
|
4538
4560
|
if (!existsSync9(SESSIONS_DIR)) {
|
|
4539
|
-
|
|
4561
|
+
mkdirSync7(SESSIONS_DIR, { recursive: true, mode: DIR_MODE2 });
|
|
4540
4562
|
}
|
|
4541
4563
|
}
|
|
4542
4564
|
function markerPath(sessionId) {
|
|
@@ -4550,8 +4572,8 @@ function registerSessionCommands(program2) {
|
|
|
4550
4572
|
orgId: opts.orgId,
|
|
4551
4573
|
startedAt: Date.now()
|
|
4552
4574
|
};
|
|
4553
|
-
|
|
4554
|
-
mode:
|
|
4575
|
+
writeFileSync6(markerPath(sessionId), JSON.stringify(marker, null, 2), {
|
|
4576
|
+
mode: FILE_MODE2
|
|
4555
4577
|
});
|
|
4556
4578
|
console.log(`[prim] session ${sessionId} bound to org ${opts.orgId}`);
|
|
4557
4579
|
});
|
|
@@ -4691,7 +4713,17 @@ function resolveAgent(agentFlag, env) {
|
|
|
4691
4713
|
}
|
|
4692
4714
|
return { agent: detectAgent(env), detected: true };
|
|
4693
4715
|
}
|
|
4694
|
-
function
|
|
4716
|
+
function parseSetupAuthStatus(result) {
|
|
4717
|
+
try {
|
|
4718
|
+
const parsed = JSON.parse(result.stdout || "{}");
|
|
4719
|
+
if (parsed.status === "valid" || parsed.authenticated === true) return "valid";
|
|
4720
|
+
if (parsed.status === "unreachable") return "unreachable";
|
|
4721
|
+
if (parsed.status === "invalid") return "invalid";
|
|
4722
|
+
} catch {
|
|
4723
|
+
}
|
|
4724
|
+
return result.code === EXIT_USAGE3 ? "unreachable" : "invalid";
|
|
4725
|
+
}
|
|
4726
|
+
function registerSetupCommand(program2, dependencies = {}) {
|
|
4695
4727
|
program2.command("setup").description(
|
|
4696
4728
|
"Install everything in one shot (auth, session + git hooks, daemon, skill, welcome)"
|
|
4697
4729
|
).option("--agent <agent>", "claude, codex, or hermes (auto-detected when omitted)").option(
|
|
@@ -4708,50 +4740,64 @@ function registerSetupCommand(program2) {
|
|
|
4708
4740
|
`[prim] unknown --agent "${agentInput}" (expected claude, codex, or hermes)
|
|
4709
4741
|
`
|
|
4710
4742
|
);
|
|
4711
|
-
process.exit(EXIT_USAGE3);
|
|
4743
|
+
(dependencies.exit ?? process.exit)(EXIT_USAGE3);
|
|
4744
|
+
return;
|
|
4712
4745
|
}
|
|
4713
4746
|
if (opts.scope !== "project" && opts.scope !== "user") {
|
|
4714
4747
|
process.stderr.write(`[prim] unknown --scope "${opts.scope}" (expected project or user)
|
|
4715
4748
|
`);
|
|
4716
|
-
process.exit(EXIT_USAGE3);
|
|
4749
|
+
(dependencies.exit ?? process.exit)(EXIT_USAGE3);
|
|
4750
|
+
return;
|
|
4717
4751
|
}
|
|
4718
4752
|
const agent = agentInput;
|
|
4719
4753
|
const scope = opts.scope;
|
|
4720
4754
|
const self = process.argv[1];
|
|
4721
|
-
const run = (args, capture = false) => {
|
|
4755
|
+
const run = dependencies.run ?? ((args, capture = false) => {
|
|
4722
4756
|
const r = spawnSync2(process.execPath, [self, ...args], {
|
|
4723
4757
|
stdio: capture ? ["inherit", "pipe", "ignore"] : "inherit",
|
|
4724
4758
|
encoding: "utf-8"
|
|
4725
4759
|
});
|
|
4726
4760
|
return { code: r.status ?? 1, stdout: capture ? r.stdout ?? "" : "" };
|
|
4727
|
-
};
|
|
4761
|
+
});
|
|
4728
4762
|
const results = {};
|
|
4729
|
-
const note = (msg) => {
|
|
4763
|
+
const note = dependencies.note ?? ((msg) => {
|
|
4730
4764
|
process.stderr.write(`[prim] ${msg}
|
|
4731
4765
|
`);
|
|
4732
|
-
};
|
|
4733
|
-
const
|
|
4734
|
-
try {
|
|
4735
|
-
return JSON.parse(json || "{}").authenticated === true;
|
|
4736
|
-
} catch {
|
|
4737
|
-
return false;
|
|
4738
|
-
}
|
|
4739
|
-
};
|
|
4766
|
+
});
|
|
4767
|
+
const exit = dependencies.exit ?? process.exit;
|
|
4740
4768
|
if (detected && agent !== "claude") {
|
|
4741
4769
|
note(`agent \xB7 detected ${agent} session (override with --agent <agent>)`);
|
|
4742
4770
|
}
|
|
4771
|
+
let authStatus = parseSetupAuthStatus(run(["auth", "status", "--json"], true));
|
|
4772
|
+
if (authStatus === "unreachable") {
|
|
4773
|
+
note("auth \xB7 verification unavailable; no setup changes were made");
|
|
4774
|
+
exit(EXIT_USAGE3);
|
|
4775
|
+
return;
|
|
4776
|
+
}
|
|
4777
|
+
if (authStatus === "invalid") {
|
|
4778
|
+
note("auth \xB7 opening browser to authenticate\u2026");
|
|
4779
|
+
const login = run(["auth", "login"]);
|
|
4780
|
+
if (login.code !== 0) {
|
|
4781
|
+
note("auth \xB7 login failed; no integration changes were made");
|
|
4782
|
+
exit(EXIT_INCOMPLETE);
|
|
4783
|
+
return;
|
|
4784
|
+
}
|
|
4785
|
+
authStatus = parseSetupAuthStatus(run(["auth", "status", "--json"], true));
|
|
4786
|
+
if (authStatus !== "valid") {
|
|
4787
|
+
note(
|
|
4788
|
+
authStatus === "unreachable" ? "auth \xB7 login completed but verification is unavailable; no integration changes were made" : "auth \xB7 login did not produce valid credentials; no integration changes were made"
|
|
4789
|
+
);
|
|
4790
|
+
exit(authStatus === "unreachable" ? EXIT_USAGE3 : EXIT_INCOMPLETE);
|
|
4791
|
+
return;
|
|
4792
|
+
}
|
|
4793
|
+
} else {
|
|
4794
|
+
note("auth \xB7 already authenticated");
|
|
4795
|
+
}
|
|
4796
|
+
results.auth = "ok";
|
|
4743
4797
|
if (agent === "claude") {
|
|
4744
4798
|
note("pre-authorize \xB7 writing prim allow-rule (user scope)\u2026");
|
|
4745
4799
|
results.preauth = run(["claude", "preauth", "--scope", "user"]).code === 0 ? "ok" : "skipped";
|
|
4746
4800
|
}
|
|
4747
|
-
if (isAuthed(run(["auth", "status", "--json"], true).stdout)) {
|
|
4748
|
-
note("auth \xB7 already authenticated");
|
|
4749
|
-
results.auth = "ok";
|
|
4750
|
-
} else {
|
|
4751
|
-
note("auth \xB7 opening browser to authenticate\u2026");
|
|
4752
|
-
run(["auth", "login"]);
|
|
4753
|
-
results.auth = isAuthed(run(["auth", "status", "--json"], true).stdout) ? "ok" : "failed";
|
|
4754
|
-
}
|
|
4755
4801
|
for (const step of planSetupSteps({ agent, daemon: opts.daemon, scope })) {
|
|
4756
4802
|
note(`${step.label} \xB7 installing\u2026`);
|
|
4757
4803
|
const { code } = run(step.args);
|
|
@@ -4776,7 +4822,7 @@ function registerSetupCommand(program2) {
|
|
|
4776
4822
|
note(
|
|
4777
4823
|
`setup ${failed.length === 0 ? "complete" : `incomplete (failed: ${failed.join(", ")})`} \u2014 ${trail}`
|
|
4778
4824
|
);
|
|
4779
|
-
|
|
4825
|
+
exit(failed.length === 0 ? 0 : EXIT_INCOMPLETE);
|
|
4780
4826
|
});
|
|
4781
4827
|
}
|
|
4782
4828
|
|
|
@@ -4788,19 +4834,19 @@ import {
|
|
|
4788
4834
|
openSync as openSync4,
|
|
4789
4835
|
readFileSync as readFileSync10,
|
|
4790
4836
|
renameSync as renameSync5,
|
|
4791
|
-
writeFileSync as
|
|
4837
|
+
writeFileSync as writeFileSync7
|
|
4792
4838
|
} from "fs";
|
|
4793
4839
|
import { homedir as homedir8 } from "os";
|
|
4794
|
-
import { dirname as
|
|
4840
|
+
import { dirname as dirname6, join as join11, resolve as resolve4 } from "path";
|
|
4795
4841
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4796
4842
|
import { createPatch } from "diff";
|
|
4797
4843
|
|
|
4798
4844
|
// src/commands/claude-plugin.ts
|
|
4799
|
-
import { existsSync as existsSync11, mkdirSync as
|
|
4845
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync9, readdirSync as readdirSync2, rmSync as rmSync3, rmdirSync } from "fs";
|
|
4800
4846
|
import { homedir as homedir7 } from "os";
|
|
4801
|
-
import { dirname as
|
|
4847
|
+
import { dirname as dirname5, join as join10, resolve as resolve3 } from "path";
|
|
4802
4848
|
import { fileURLToPath } from "url";
|
|
4803
|
-
var __dirname =
|
|
4849
|
+
var __dirname = dirname5(fileURLToPath(import.meta.url));
|
|
4804
4850
|
var PLUGIN_DESCRIPTION = "Primitive decision-graph guidance for the prim CLI \u2014 a model-invoked skill installed by prim skill install.";
|
|
4805
4851
|
function resolvePluginDir(cwd, scope) {
|
|
4806
4852
|
if (scope && scope !== "user" && scope !== "project") {
|
|
@@ -4812,10 +4858,10 @@ function resolvePluginDir(cwd, scope) {
|
|
|
4812
4858
|
}
|
|
4813
4859
|
function packageVersion() {
|
|
4814
4860
|
let dir = __dirname;
|
|
4815
|
-
while (dir !==
|
|
4861
|
+
while (dir !== dirname5(dir)) {
|
|
4816
4862
|
const p = resolve3(dir, "package.json");
|
|
4817
4863
|
if (existsSync11(p)) return JSON.parse(readFileSync9(p, "utf-8")).version;
|
|
4818
|
-
dir =
|
|
4864
|
+
dir = dirname5(dir);
|
|
4819
4865
|
}
|
|
4820
4866
|
return "0.0.0";
|
|
4821
4867
|
}
|
|
@@ -4849,7 +4895,7 @@ function installClaudePlugin(cwd, opts) {
|
|
|
4849
4895
|
console.log(`Would write plugin to ${dir} (.claude-plugin/plugin.json + SKILL.md)`);
|
|
4850
4896
|
return 0;
|
|
4851
4897
|
}
|
|
4852
|
-
|
|
4898
|
+
mkdirSync8(join10(dir, ".claude-plugin"), { recursive: true });
|
|
4853
4899
|
atomicWrite3(manifestPath, manifest);
|
|
4854
4900
|
atomicWrite3(skillPath, skill);
|
|
4855
4901
|
console.log(`Installed prim skill plugin at ${dir}`);
|
|
@@ -4867,8 +4913,8 @@ function uninstallClaudePlugin(cwd, opts) {
|
|
|
4867
4913
|
console.log(`prim skill plugin not present at ${dir}`);
|
|
4868
4914
|
return 0;
|
|
4869
4915
|
}
|
|
4870
|
-
|
|
4871
|
-
|
|
4916
|
+
rmSync3(manifestPath, { force: true });
|
|
4917
|
+
rmSync3(skillPath, { force: true });
|
|
4872
4918
|
removeDirIfEmpty(join10(dir, ".claude-plugin"));
|
|
4873
4919
|
removeDirIfEmpty(dir);
|
|
4874
4920
|
console.log(`Removed prim skill plugin from ${dir}`);
|
|
@@ -4892,7 +4938,7 @@ function statusClaudePlugin(cwd, opts) {
|
|
|
4892
4938
|
}
|
|
4893
4939
|
|
|
4894
4940
|
// src/commands/skill.ts
|
|
4895
|
-
var __dirname2 =
|
|
4941
|
+
var __dirname2 = dirname6(fileURLToPath2(import.meta.url));
|
|
4896
4942
|
var SKILL_BEGIN = "<!-- BEGIN PRIM SKILL v1 -->";
|
|
4897
4943
|
var SKILL_END = "<!-- END PRIM SKILL v1 -->";
|
|
4898
4944
|
var TARGET_CANDIDATES = [
|
|
@@ -4919,10 +4965,10 @@ function userTargetFor(agent) {
|
|
|
4919
4965
|
}
|
|
4920
4966
|
function loadSkill() {
|
|
4921
4967
|
let dir = __dirname2;
|
|
4922
|
-
while (dir !==
|
|
4968
|
+
while (dir !== dirname6(dir)) {
|
|
4923
4969
|
const p = resolve4(dir, "SKILL.md");
|
|
4924
4970
|
if (existsSync12(p)) return readFileSync10(p, "utf-8");
|
|
4925
|
-
dir =
|
|
4971
|
+
dir = dirname6(dir);
|
|
4926
4972
|
}
|
|
4927
4973
|
throw new Error("SKILL.md not found in package");
|
|
4928
4974
|
}
|
|
@@ -4955,7 +5001,7 @@ function removeBlock(existing) {
|
|
|
4955
5001
|
}
|
|
4956
5002
|
function atomicWrite3(target, content) {
|
|
4957
5003
|
const tmp = `${target}.tmp`;
|
|
4958
|
-
|
|
5004
|
+
writeFileSync7(tmp, content);
|
|
4959
5005
|
const fd = openSync4(tmp, "r+");
|
|
4960
5006
|
try {
|
|
4961
5007
|
fsyncSync3(fd);
|
|
@@ -5076,13 +5122,13 @@ function registerSkillCommands(program2) {
|
|
|
5076
5122
|
|
|
5077
5123
|
// src/commands/statusline.ts
|
|
5078
5124
|
import { readFileSync as readFileSync11 } from "fs";
|
|
5079
|
-
import { dirname as
|
|
5125
|
+
import { dirname as dirname7, resolve as resolve5 } from "path";
|
|
5080
5126
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
5081
5127
|
var STATUSLINE_TIMEOUT_MS = 200;
|
|
5082
5128
|
var STATUSLINE_NAME_CAP = 3;
|
|
5083
5129
|
function readPackageVersion() {
|
|
5084
5130
|
try {
|
|
5085
|
-
const here =
|
|
5131
|
+
const here = dirname7(fileURLToPath3(import.meta.url));
|
|
5086
5132
|
const candidates = [
|
|
5087
5133
|
// The supervised runtime stages a tiny manifest beside the standalone
|
|
5088
5134
|
// statusline bundle, so it remains versioned without loading the package.
|
|
@@ -5275,7 +5321,7 @@ function registerWelcomeCommand(program2, deps = { getClient }) {
|
|
|
5275
5321
|
}
|
|
5276
5322
|
|
|
5277
5323
|
// src/index.ts
|
|
5278
|
-
var __dirname3 =
|
|
5324
|
+
var __dirname3 = dirname8(fileURLToPath4(import.meta.url));
|
|
5279
5325
|
var pkg = JSON.parse(readFileSync12(resolve6(__dirname3, "../package.json"), "utf-8"));
|
|
5280
5326
|
updateNotifier({ pkg }).notify();
|
|
5281
5327
|
var program = new Command();
|