@primitive.ai/prim 0.1.0-alpha.41 → 0.1.0-alpha.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -6
- package/dist/{chunk-UWAJCF7P.js → chunk-BSZGI5RL.js} +1 -1
- package/dist/{chunk-S2O4P4A3.js → chunk-F5QCFBJ6.js} +14 -3
- package/dist/{chunk-HSZPTVKU.js → chunk-F7O7V6ZM.js} +88 -12
- package/dist/chunk-IMAIBPUC.js +210 -0
- package/dist/{chunk-H4OR42TJ.js → chunk-OKUXEBPT.js} +149 -36
- package/dist/{chunk-AAGJFO7C.js → chunk-QY75BQMF.js} +25 -1
- package/dist/chunk-S5NUHFAD.js +55 -0
- package/dist/chunk-UISU3A7W.js +202 -0
- package/dist/{chunk-26VA3ADF.js → chunk-W6PAKEDO.js} +32 -6
- package/dist/daemon/server.js +1150 -77
- package/dist/hooks/post-commit.js +5 -5
- package/dist/hooks/post-tool-use.js +31 -19
- package/dist/hooks/pre-commit.js +2 -2
- package/dist/hooks/pre-tool-use.js +3 -5
- package/dist/hooks/prim-hook.js +77 -20
- package/dist/hooks/session-start.js +79 -24
- package/dist/index.js +1609 -320
- package/dist/statusline-main.js +280 -0
- package/package.json +6 -5
- package/dist/chunk-LUPD2JSH.js +0 -78
package/dist/index.js
CHANGED
|
@@ -4,34 +4,40 @@ import {
|
|
|
4
4
|
color,
|
|
5
5
|
colorForArea,
|
|
6
6
|
dim,
|
|
7
|
+
requireDurableIngestAcknowledgement,
|
|
7
8
|
stripAnsi,
|
|
8
9
|
stripControlChars
|
|
9
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-QY75BQMF.js";
|
|
10
11
|
import {
|
|
11
12
|
checkAffectedDecisions,
|
|
12
13
|
daemonOrDirectGet,
|
|
13
14
|
formatDecisionsWarning
|
|
14
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-BSZGI5RL.js";
|
|
15
16
|
import {
|
|
16
17
|
JOURNAL_DIR,
|
|
17
18
|
SESSIONS_DIR,
|
|
18
19
|
bucketStats,
|
|
19
20
|
listBuckets,
|
|
20
21
|
listFlushing,
|
|
22
|
+
pendingJournalStats,
|
|
21
23
|
readMovesFromPath
|
|
22
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-OKUXEBPT.js";
|
|
23
25
|
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
fetchFeedbackCapability
|
|
27
|
+
} from "./chunk-UISU3A7W.js";
|
|
28
|
+
import {
|
|
29
|
+
inspectWorkspaceId
|
|
30
|
+
} from "./chunk-IMAIBPUC.js";
|
|
28
31
|
import {
|
|
32
|
+
activateRepoBestEffort,
|
|
29
33
|
binFile,
|
|
30
34
|
commandMatchesBin,
|
|
31
35
|
detachedHookShimCommand,
|
|
36
|
+
gitToplevel,
|
|
32
37
|
hookShimCommand,
|
|
38
|
+
setRepoActive,
|
|
33
39
|
warmBinCache
|
|
34
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-F7O7V6ZM.js";
|
|
35
41
|
import {
|
|
36
42
|
HttpError,
|
|
37
43
|
REFRESH_TOKEN_PATH,
|
|
@@ -42,15 +48,15 @@ import {
|
|
|
42
48
|
getSiteUrl,
|
|
43
49
|
getTokenExpiresAt,
|
|
44
50
|
saveTokenExpiry
|
|
45
|
-
} from "./chunk-
|
|
51
|
+
} from "./chunk-W6PAKEDO.js";
|
|
46
52
|
import {
|
|
47
53
|
daemonIsLive,
|
|
48
54
|
daemonRequest
|
|
49
55
|
} from "./chunk-UTKQTZHL.js";
|
|
50
56
|
|
|
51
57
|
// src/index.ts
|
|
52
|
-
import { readFileSync as
|
|
53
|
-
import { dirname as
|
|
58
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
59
|
+
import { dirname as dirname9, resolve as resolve6 } from "path";
|
|
54
60
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
55
61
|
import { Command } from "commander";
|
|
56
62
|
import updateNotifier from "update-notifier";
|
|
@@ -344,8 +350,8 @@ function registerAuthCommands(program2) {
|
|
|
344
350
|
const state = base64url(randomBytes(16));
|
|
345
351
|
const redirectUri = `http://${LOCALHOST}:${CALLBACK_PORT}/callback`;
|
|
346
352
|
let settle;
|
|
347
|
-
const outcome = new Promise((
|
|
348
|
-
settle =
|
|
353
|
+
const outcome = new Promise((resolve7) => {
|
|
354
|
+
settle = resolve7;
|
|
349
355
|
});
|
|
350
356
|
const server = createServer((req, res) => {
|
|
351
357
|
const url = new URL(req.url ?? "/", `http://${LOCALHOST}`);
|
|
@@ -359,12 +365,12 @@ function registerAuthCommands(program2) {
|
|
|
359
365
|
res.end(page.html, () => settle(page.result));
|
|
360
366
|
});
|
|
361
367
|
try {
|
|
362
|
-
await new Promise((
|
|
368
|
+
await new Promise((resolve7, reject) => {
|
|
363
369
|
const onError = (err) => reject(err);
|
|
364
370
|
server.once("error", onError);
|
|
365
371
|
server.listen(CALLBACK_PORT, LOCALHOST, () => {
|
|
366
372
|
server.removeListener("error", onError);
|
|
367
|
-
|
|
373
|
+
resolve7();
|
|
368
374
|
});
|
|
369
375
|
});
|
|
370
376
|
} catch (err) {
|
|
@@ -536,16 +542,546 @@ async function exchangeCode(siteUrl, code, codeVerifier, redirectUri) {
|
|
|
536
542
|
// src/commands/claude-install.ts
|
|
537
543
|
import {
|
|
538
544
|
closeSync,
|
|
539
|
-
existsSync as
|
|
545
|
+
existsSync as existsSync3,
|
|
540
546
|
fsyncSync,
|
|
541
|
-
mkdirSync as
|
|
547
|
+
mkdirSync as mkdirSync3,
|
|
542
548
|
openSync,
|
|
549
|
+
readFileSync as readFileSync3,
|
|
550
|
+
renameSync as renameSync2,
|
|
551
|
+
writeFileSync as writeFileSync3
|
|
552
|
+
} from "fs";
|
|
553
|
+
import { homedir as homedir2 } from "os";
|
|
554
|
+
import { dirname as dirname3, join as join2 } from "path";
|
|
555
|
+
|
|
556
|
+
// src/daemon/launchd.ts
|
|
557
|
+
import { spawnSync } from "child_process";
|
|
558
|
+
import { createHash as createHash2 } from "crypto";
|
|
559
|
+
import {
|
|
560
|
+
chmodSync,
|
|
561
|
+
copyFileSync,
|
|
562
|
+
existsSync as existsSync2,
|
|
563
|
+
mkdirSync as mkdirSync2,
|
|
564
|
+
mkdtempSync,
|
|
543
565
|
readFileSync as readFileSync2,
|
|
544
566
|
renameSync,
|
|
567
|
+
rmSync as rmSync2,
|
|
568
|
+
statSync,
|
|
569
|
+
symlinkSync,
|
|
545
570
|
writeFileSync as writeFileSync2
|
|
546
571
|
} from "fs";
|
|
547
572
|
import { homedir } from "os";
|
|
548
|
-
import { dirname as dirname2, join } from "path";
|
|
573
|
+
import { dirname as dirname2, isAbsolute, join, resolve } from "path";
|
|
574
|
+
var LAUNCHD_LABEL = "ai.getprimitive.prim-daemon";
|
|
575
|
+
var RUNTIME_SCHEMA_VERSION = 1;
|
|
576
|
+
var RUNTIME_DIR_MODE = 448;
|
|
577
|
+
var RUNTIME_FILE_MODE = 384;
|
|
578
|
+
var RUNTIME_LAUNCHER_MODE = 448;
|
|
579
|
+
var PLIST_FILE_MODE = 384;
|
|
580
|
+
var READY_TIMEOUT_MS = 15e3;
|
|
581
|
+
var READY_POLL_MS = 100;
|
|
582
|
+
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
|
+
function xdgDataHome(options) {
|
|
587
|
+
const home = options.homeDir ?? homedir();
|
|
588
|
+
const configured = (options.env ?? process.env).XDG_DATA_HOME;
|
|
589
|
+
return configured && isAbsolute(configured) ? configured : join(home, ".local", "share");
|
|
590
|
+
}
|
|
591
|
+
function runtimePaths(options = {}) {
|
|
592
|
+
const dataDir = join(xdgDataHome(options), "prim");
|
|
593
|
+
const runtimeDir = join(dataDir, "runtime");
|
|
594
|
+
const currentLink = join(runtimeDir, "current");
|
|
595
|
+
return {
|
|
596
|
+
dataDir,
|
|
597
|
+
runtimeDir,
|
|
598
|
+
releasesDir: join(runtimeDir, "releases"),
|
|
599
|
+
currentLink,
|
|
600
|
+
daemonEntry: join(currentLink, "prim-daemon-server"),
|
|
601
|
+
statuslineEntry: join(currentLink, "prim-statusline"),
|
|
602
|
+
statuslineLauncher: join(runtimeDir, "prim-statusline"),
|
|
603
|
+
manifestPath: join(currentLink, "manifest.json"),
|
|
604
|
+
disabledMarker: join(dataDir, "daemon.disabled"),
|
|
605
|
+
lifecycleLockDir: join(dataDir, "daemon.lifecycle.lock")
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
function findPackageVersion(entryFile) {
|
|
609
|
+
let dir = dirname2(resolve(entryFile));
|
|
610
|
+
for (let depth = 0; depth < 6; depth++) {
|
|
611
|
+
const packagePath = join(dir, "package.json");
|
|
612
|
+
if (existsSync2(packagePath)) {
|
|
613
|
+
try {
|
|
614
|
+
const manifest = JSON.parse(readFileSync2(packagePath, "utf8"));
|
|
615
|
+
if (manifest.name === "@primitive.ai/prim" && manifest.version) {
|
|
616
|
+
return manifest.version;
|
|
617
|
+
}
|
|
618
|
+
} catch {
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
const parent = dirname2(dir);
|
|
622
|
+
if (parent === dir) break;
|
|
623
|
+
dir = parent;
|
|
624
|
+
}
|
|
625
|
+
throw new Error(`cannot determine @primitive.ai/prim version from ${entryFile}`);
|
|
626
|
+
}
|
|
627
|
+
function readRuntimeManifest(path) {
|
|
628
|
+
try {
|
|
629
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
630
|
+
if (parsed.schemaVersion !== RUNTIME_SCHEMA_VERSION || typeof parsed.version !== "string" || typeof parsed.nodePath !== "string" || parsed.daemonFile !== "prim-daemon-server") {
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
return parsed;
|
|
634
|
+
} catch {
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function sameRuntime(current, desired, paths) {
|
|
639
|
+
return Boolean(
|
|
640
|
+
current && current.version === desired.version && current.nodePath === desired.nodePath && current.statuslineFile === desired.statuslineFile && existsSync2(paths.daemonEntry) && (desired.statuslineFile === void 0 || existsSync2(paths.statuslineEntry))
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
function atomicSymlink(target, linkPath) {
|
|
644
|
+
const tempLink = `${linkPath}.tmp-${process.pid}-${createHash2("sha256").update(`${target}\0${Date.now()}\0${Math.random()}`).digest("hex").slice(0, 12)}`;
|
|
645
|
+
symlinkSync(target, tempLink);
|
|
646
|
+
try {
|
|
647
|
+
renameSync(tempLink, linkPath);
|
|
648
|
+
} catch (error) {
|
|
649
|
+
rmSync2(tempLink, { force: true });
|
|
650
|
+
throw error;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
function stageRuntime(options = {}) {
|
|
654
|
+
const paths = runtimePaths(options);
|
|
655
|
+
const daemonSource = options.daemonSource ?? binFile("prim-daemon-server");
|
|
656
|
+
if (!daemonSource || !existsSync2(daemonSource)) {
|
|
657
|
+
throw new Error("cannot stage runtime: prim-daemon-server bundle is unavailable");
|
|
658
|
+
}
|
|
659
|
+
const configuredStatusline = options.statuslineSource === void 0 ? binFile("prim-statusline") : options.statuslineSource;
|
|
660
|
+
const statuslineSource = configuredStatusline && existsSync2(configuredStatusline) ? configuredStatusline : null;
|
|
661
|
+
const nodePath = resolve(options.nodePath ?? process.execPath);
|
|
662
|
+
const version = options.version ?? findPackageVersion(daemonSource);
|
|
663
|
+
const desired = {
|
|
664
|
+
schemaVersion: RUNTIME_SCHEMA_VERSION,
|
|
665
|
+
version,
|
|
666
|
+
nodePath,
|
|
667
|
+
daemonFile: "prim-daemon-server",
|
|
668
|
+
...statuslineSource ? { statuslineFile: "prim-statusline" } : {},
|
|
669
|
+
stagedAt: (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
|
|
670
|
+
};
|
|
671
|
+
const current = readRuntimeManifest(paths.manifestPath);
|
|
672
|
+
if (sameRuntime(current, desired, paths)) {
|
|
673
|
+
if (current?.statuslineFile) {
|
|
674
|
+
atomicWrite(
|
|
675
|
+
paths.statuslineLauncher,
|
|
676
|
+
statuslineLauncherContent(paths, current.nodePath),
|
|
677
|
+
RUNTIME_LAUNCHER_MODE
|
|
678
|
+
);
|
|
679
|
+
} else {
|
|
680
|
+
rmSync2(paths.statuslineLauncher, { force: true });
|
|
681
|
+
}
|
|
682
|
+
return { changed: false, manifest: current, paths };
|
|
683
|
+
}
|
|
684
|
+
mkdirSync2(paths.releasesDir, { recursive: true, mode: RUNTIME_DIR_MODE });
|
|
685
|
+
chmodSync(paths.runtimeDir, RUNTIME_DIR_MODE);
|
|
686
|
+
chmodSync(paths.releasesDir, RUNTIME_DIR_MODE);
|
|
687
|
+
const stagingDir = mkdtempSync(join(paths.releasesDir, ".stage-"));
|
|
688
|
+
chmodSync(stagingDir, RUNTIME_DIR_MODE);
|
|
689
|
+
try {
|
|
690
|
+
const daemonTarget = join(stagingDir, desired.daemonFile);
|
|
691
|
+
copyFileSync(daemonSource, daemonTarget);
|
|
692
|
+
chmodSync(daemonTarget, RUNTIME_FILE_MODE);
|
|
693
|
+
if (statuslineSource && desired.statuslineFile) {
|
|
694
|
+
const statuslineTarget = join(stagingDir, desired.statuslineFile);
|
|
695
|
+
copyFileSync(statuslineSource, statuslineTarget);
|
|
696
|
+
chmodSync(statuslineTarget, RUNTIME_FILE_MODE);
|
|
697
|
+
}
|
|
698
|
+
const manifestTarget = join(stagingDir, "manifest.json");
|
|
699
|
+
writeFileSync2(manifestTarget, `${JSON.stringify(desired, null, 2)}
|
|
700
|
+
`, {
|
|
701
|
+
encoding: "utf8",
|
|
702
|
+
mode: RUNTIME_FILE_MODE,
|
|
703
|
+
flag: "wx"
|
|
704
|
+
});
|
|
705
|
+
const releaseName = `release-${createHash2("sha256").update(
|
|
706
|
+
`${version}\0${nodePath}\0${desired.statuslineFile ?? ""}\0${desired.stagedAt}\0${process.pid}\0${Math.random()}`
|
|
707
|
+
).digest("hex").slice(0, 16)}`;
|
|
708
|
+
const releaseDir = join(paths.releasesDir, releaseName);
|
|
709
|
+
renameSync(stagingDir, releaseDir);
|
|
710
|
+
atomicSymlink(join("releases", releaseName), paths.currentLink);
|
|
711
|
+
if (desired.statuslineFile) {
|
|
712
|
+
atomicWrite(
|
|
713
|
+
paths.statuslineLauncher,
|
|
714
|
+
statuslineLauncherContent(paths, desired.nodePath),
|
|
715
|
+
RUNTIME_LAUNCHER_MODE
|
|
716
|
+
);
|
|
717
|
+
} else {
|
|
718
|
+
rmSync2(paths.statuslineLauncher, { force: true });
|
|
719
|
+
}
|
|
720
|
+
} catch (error) {
|
|
721
|
+
rmSync2(stagingDir, { recursive: true, force: true });
|
|
722
|
+
throw error;
|
|
723
|
+
}
|
|
724
|
+
return { changed: true, manifest: desired, paths };
|
|
725
|
+
}
|
|
726
|
+
function shellQuote(value) {
|
|
727
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
728
|
+
}
|
|
729
|
+
function statuslineLauncherContent(paths, nodePath) {
|
|
730
|
+
return `#!/bin/sh
|
|
731
|
+
exec ${shellQuote(nodePath)} ${shellQuote(paths.statuslineEntry)}
|
|
732
|
+
`;
|
|
733
|
+
}
|
|
734
|
+
function runtimeStatuslineCommand(options = {}) {
|
|
735
|
+
return shellQuote(runtimePaths(options).statuslineLauncher);
|
|
736
|
+
}
|
|
737
|
+
function xmlEscape(value) {
|
|
738
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
739
|
+
}
|
|
740
|
+
function generateLaunchAgentPlist(config) {
|
|
741
|
+
const apiUrl = config.apiUrl ? `
|
|
742
|
+
<key>PRIM_API_URL</key>
|
|
743
|
+
<string>${xmlEscape(config.apiUrl)}</string>` : "";
|
|
744
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
745
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
746
|
+
<plist version="1.0">
|
|
747
|
+
<dict>
|
|
748
|
+
<key>Label</key>
|
|
749
|
+
<string>${LAUNCHD_LABEL}</string>
|
|
750
|
+
<key>ProgramArguments</key>
|
|
751
|
+
<array>
|
|
752
|
+
<string>${xmlEscape(config.nodePath)}</string>
|
|
753
|
+
<string>${xmlEscape(config.daemonPath)}</string>
|
|
754
|
+
</array>
|
|
755
|
+
<key>RunAtLoad</key>
|
|
756
|
+
<true/>
|
|
757
|
+
<key>KeepAlive</key>
|
|
758
|
+
<true/>
|
|
759
|
+
<key>ThrottleInterval</key>
|
|
760
|
+
<integer>10</integer>
|
|
761
|
+
<key>ProcessType</key>
|
|
762
|
+
<string>Background</string>
|
|
763
|
+
<key>Umask</key>
|
|
764
|
+
<integer>63</integer>
|
|
765
|
+
<key>WorkingDirectory</key>
|
|
766
|
+
<string>${xmlEscape(config.workingDirectory)}</string>
|
|
767
|
+
<key>StandardOutPath</key>
|
|
768
|
+
<string>${xmlEscape(config.logPath)}</string>
|
|
769
|
+
<key>StandardErrorPath</key>
|
|
770
|
+
<string>${xmlEscape(config.logPath)}</string>
|
|
771
|
+
<key>EnvironmentVariables</key>
|
|
772
|
+
<dict>
|
|
773
|
+
<key>PRIM_RUNTIME_VERSION</key>
|
|
774
|
+
<string>${xmlEscape(config.runtimeVersion)}</string>${apiUrl}
|
|
775
|
+
</dict>
|
|
776
|
+
</dict>
|
|
777
|
+
</plist>
|
|
778
|
+
`;
|
|
779
|
+
}
|
|
780
|
+
function launchdPaths(options = {}) {
|
|
781
|
+
const home = options.homeDir ?? homedir();
|
|
782
|
+
const uid = options.uid ?? process.getuid?.();
|
|
783
|
+
if (uid === void 0) throw new Error("cannot determine uid for launchd user domain");
|
|
784
|
+
return {
|
|
785
|
+
domainTarget: `gui/${uid}`,
|
|
786
|
+
serviceTarget: `gui/${uid}/${LAUNCHD_LABEL}`,
|
|
787
|
+
plistPath: join(home, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`),
|
|
788
|
+
logPath: join(home, ".config", "prim", "daemon.log")
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
var runLaunchctl = (args) => {
|
|
792
|
+
const result = spawnSync("/bin/launchctl", args, { encoding: "utf8" });
|
|
793
|
+
return {
|
|
794
|
+
status: result.status,
|
|
795
|
+
stdout: result.stdout ?? "",
|
|
796
|
+
stderr: result.error?.message ?? result.stderr ?? ""
|
|
797
|
+
};
|
|
798
|
+
};
|
|
799
|
+
function parseLaunchdService(result) {
|
|
800
|
+
if (result.status !== 0) return { loaded: false };
|
|
801
|
+
const state = /^\s*state = (.+?)\s*$/m.exec(result.stdout)?.[1];
|
|
802
|
+
const rawPid = /^\s*pid = (\d+)\s*$/m.exec(result.stdout)?.[1];
|
|
803
|
+
const pid = rawPid ? Number(rawPid) : void 0;
|
|
804
|
+
return { loaded: true, state, pid, raw: result.stdout };
|
|
805
|
+
}
|
|
806
|
+
function getLaunchdService(options = {}) {
|
|
807
|
+
const paths = launchdPaths(options);
|
|
808
|
+
return parseLaunchdService((options.runner ?? runLaunchctl)(["print", paths.serviceTarget]));
|
|
809
|
+
}
|
|
810
|
+
function atomicWrite(path, content, mode) {
|
|
811
|
+
try {
|
|
812
|
+
if (readFileSync2(path, "utf8") === content) {
|
|
813
|
+
chmodSync(path, mode);
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
} catch {
|
|
817
|
+
}
|
|
818
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
819
|
+
const temp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
820
|
+
writeFileSync2(temp, content, { encoding: "utf8", mode, flag: "wx" });
|
|
821
|
+
try {
|
|
822
|
+
renameSync(temp, path);
|
|
823
|
+
} catch (error) {
|
|
824
|
+
rmSync2(temp, { force: true });
|
|
825
|
+
throw error;
|
|
826
|
+
}
|
|
827
|
+
return true;
|
|
828
|
+
}
|
|
829
|
+
function launchctlOrThrow(runner, args, operation) {
|
|
830
|
+
const result = runner(args);
|
|
831
|
+
if (result.status !== 0) {
|
|
832
|
+
const detail = result.stderr.trim() || result.stdout.trim() || `exit ${String(result.status)}`;
|
|
833
|
+
throw new Error(`launchctl ${operation} failed: ${detail}`);
|
|
834
|
+
}
|
|
835
|
+
return result;
|
|
836
|
+
}
|
|
837
|
+
function defaultSleep(ms) {
|
|
838
|
+
return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
839
|
+
}
|
|
840
|
+
async function identifyDaemon(timeoutMs) {
|
|
841
|
+
const snapshot = await daemonRequest("status_snapshot", {}, { timeoutMs });
|
|
842
|
+
return typeof snapshot?.pid === "number" ? snapshot.pid : null;
|
|
843
|
+
}
|
|
844
|
+
function processIsAlive(pid) {
|
|
845
|
+
try {
|
|
846
|
+
process.kill(pid, 0);
|
|
847
|
+
return true;
|
|
848
|
+
} catch (error) {
|
|
849
|
+
return error.code === "EPERM";
|
|
850
|
+
}
|
|
851
|
+
}
|
|
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
|
+
async function withDaemonLifecycleLock(operation, options = {}) {
|
|
916
|
+
const lockDir = runtimePaths(options).lifecycleLockDir;
|
|
917
|
+
mkdirSync2(dirname2(lockDir), { recursive: true, mode: RUNTIME_DIR_MODE });
|
|
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
|
+
}
|
|
941
|
+
}
|
|
942
|
+
async function stopVerifiedLegacyDaemon(homeDir) {
|
|
943
|
+
const pidPath = join(homeDir, ".config", "prim", "daemon.pid");
|
|
944
|
+
let pid;
|
|
945
|
+
try {
|
|
946
|
+
pid = Number(readFileSync2(pidPath, "utf8").trim());
|
|
947
|
+
} catch {
|
|
948
|
+
throw new Error("an unsupervised daemon answered, but it has no verifiable pidfile");
|
|
949
|
+
}
|
|
950
|
+
const snapshot = await daemonRequest("status_snapshot", {}, { timeoutMs: 500 });
|
|
951
|
+
if (!Number.isInteger(pid) || pid <= 0 || !processIsAlive(pid) || snapshot?.pid !== pid) {
|
|
952
|
+
throw new Error("refusing to signal an unsupervised daemon whose pid cannot be verified");
|
|
953
|
+
}
|
|
954
|
+
process.kill(pid, "SIGTERM");
|
|
955
|
+
const deadline = Date.now() + 5e3;
|
|
956
|
+
while (Date.now() < deadline && processIsAlive(pid)) {
|
|
957
|
+
await defaultSleep(100);
|
|
958
|
+
}
|
|
959
|
+
if (processIsAlive(pid)) {
|
|
960
|
+
throw new Error(`verified legacy daemon pid=${pid} did not stop within 5000ms`);
|
|
961
|
+
}
|
|
962
|
+
return true;
|
|
963
|
+
}
|
|
964
|
+
async function waitForReady(probe, sleep2) {
|
|
965
|
+
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
966
|
+
while (Date.now() < deadline) {
|
|
967
|
+
if (await probe(READY_PROBE_TIMEOUT_MS)) return true;
|
|
968
|
+
await sleep2(READY_POLL_MS);
|
|
969
|
+
}
|
|
970
|
+
return probe(READY_PROBE_TIMEOUT_MS);
|
|
971
|
+
}
|
|
972
|
+
function daemonExplicitlyDisabled(options = {}) {
|
|
973
|
+
return existsSync2(runtimePaths(options).disabledMarker);
|
|
974
|
+
}
|
|
975
|
+
function setDaemonExplicitlyDisabled(disabled, options = {}) {
|
|
976
|
+
const marker = runtimePaths(options).disabledMarker;
|
|
977
|
+
if (!disabled) {
|
|
978
|
+
rmSync2(marker, { force: true });
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
mkdirSync2(dirname2(marker), { recursive: true, mode: RUNTIME_DIR_MODE });
|
|
982
|
+
writeFileSync2(marker, "disabled by `prim daemon stop`\n", {
|
|
983
|
+
encoding: "utf8",
|
|
984
|
+
mode: RUNTIME_FILE_MODE
|
|
985
|
+
});
|
|
986
|
+
chmodSync(marker, RUNTIME_FILE_MODE);
|
|
987
|
+
}
|
|
988
|
+
async function ensureMacDaemon(options = {}) {
|
|
989
|
+
return withDaemonLifecycleLock(() => ensureMacDaemonLocked(options), options);
|
|
990
|
+
}
|
|
991
|
+
async function ensureMacDaemonLocked(options) {
|
|
992
|
+
if (!options.explicitlyStarted && daemonExplicitlyDisabled(options)) {
|
|
993
|
+
return {
|
|
994
|
+
state: "disabled",
|
|
995
|
+
action: "none",
|
|
996
|
+
runtimeChanged: false,
|
|
997
|
+
plistChanged: false,
|
|
998
|
+
responding: false,
|
|
999
|
+
service: getLaunchdService(options)
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
if (options.explicitlyStarted) setDaemonExplicitlyDisabled(false, options);
|
|
1003
|
+
const runtime = stageRuntime(options);
|
|
1004
|
+
const servicePaths = launchdPaths(options);
|
|
1005
|
+
mkdirSync2(dirname2(servicePaths.logPath), { recursive: true, mode: RUNTIME_DIR_MODE });
|
|
1006
|
+
writeFileSync2(servicePaths.logPath, "", { mode: RUNTIME_FILE_MODE, flag: "a" });
|
|
1007
|
+
chmodSync(servicePaths.logPath, RUNTIME_FILE_MODE);
|
|
1008
|
+
const plist = generateLaunchAgentPlist({
|
|
1009
|
+
nodePath: runtime.manifest.nodePath,
|
|
1010
|
+
daemonPath: runtime.paths.daemonEntry,
|
|
1011
|
+
runtimeVersion: runtime.manifest.version,
|
|
1012
|
+
logPath: servicePaths.logPath,
|
|
1013
|
+
workingDirectory: options.homeDir ?? homedir(),
|
|
1014
|
+
apiUrl: (options.env ?? process.env).PRIM_API_URL
|
|
1015
|
+
});
|
|
1016
|
+
const plistChanged = atomicWrite(servicePaths.plistPath, plist, PLIST_FILE_MODE);
|
|
1017
|
+
const runner = options.runner ?? runLaunchctl;
|
|
1018
|
+
let service = getLaunchdService({ ...options, runner });
|
|
1019
|
+
let action = "none";
|
|
1020
|
+
if (service.loaded && plistChanged) {
|
|
1021
|
+
launchctlOrThrow(runner, ["bootout", servicePaths.serviceTarget], "bootout");
|
|
1022
|
+
launchctlOrThrow(
|
|
1023
|
+
runner,
|
|
1024
|
+
["bootstrap", servicePaths.domainTarget, servicePaths.plistPath],
|
|
1025
|
+
"bootstrap"
|
|
1026
|
+
);
|
|
1027
|
+
action = "reload";
|
|
1028
|
+
} else if (!service.loaded) {
|
|
1029
|
+
if (await (options.probe ?? daemonIsLive)(READY_PROBE_TIMEOUT_MS)) {
|
|
1030
|
+
await (options.migrateLegacy ?? (() => stopVerifiedLegacyDaemon(options.homeDir ?? homedir())))();
|
|
1031
|
+
}
|
|
1032
|
+
launchctlOrThrow(
|
|
1033
|
+
runner,
|
|
1034
|
+
["bootstrap", servicePaths.domainTarget, servicePaths.plistPath],
|
|
1035
|
+
"bootstrap"
|
|
1036
|
+
);
|
|
1037
|
+
action = "bootstrap";
|
|
1038
|
+
} else {
|
|
1039
|
+
const healthy = await (options.probe ?? daemonIsLive)(READY_PROBE_TIMEOUT_MS);
|
|
1040
|
+
const socketPid2 = healthy ? await (options.identify ?? identifyDaemon)(READY_PROBE_TIMEOUT_MS) : null;
|
|
1041
|
+
const ownsSocket = service.pid !== void 0 && socketPid2 !== null && service.pid === socketPid2;
|
|
1042
|
+
if (healthy && socketPid2 !== null && !ownsSocket) {
|
|
1043
|
+
await (options.migrateLegacy ?? (() => stopVerifiedLegacyDaemon(options.homeDir ?? homedir())))();
|
|
1044
|
+
}
|
|
1045
|
+
if (runtime.changed || options.forceRestart || !healthy || !ownsSocket) {
|
|
1046
|
+
launchctlOrThrow(runner, ["kickstart", "-k", servicePaths.serviceTarget], "kickstart");
|
|
1047
|
+
action = "kickstart";
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
const responding = await waitForReady(
|
|
1051
|
+
options.probe ?? daemonIsLive,
|
|
1052
|
+
options.sleep ?? defaultSleep
|
|
1053
|
+
);
|
|
1054
|
+
service = getLaunchdService({ ...options, runner });
|
|
1055
|
+
const socketPid = responding ? await (options.identify ?? identifyDaemon)(READY_PROBE_TIMEOUT_MS) : null;
|
|
1056
|
+
const running = responding && service.loaded && service.pid !== void 0 && socketPid !== null && service.pid === socketPid;
|
|
1057
|
+
return {
|
|
1058
|
+
state: running ? "running" : "unhealthy",
|
|
1059
|
+
action,
|
|
1060
|
+
runtimeChanged: runtime.changed,
|
|
1061
|
+
plistChanged,
|
|
1062
|
+
responding,
|
|
1063
|
+
...socketPid === null ? {} : { socketPid },
|
|
1064
|
+
service,
|
|
1065
|
+
runtime
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
async function bootoutMacDaemon(options = {}) {
|
|
1069
|
+
return withDaemonLifecycleLock(() => bootoutMacDaemonLocked(options), options);
|
|
1070
|
+
}
|
|
1071
|
+
async function bootoutMacDaemonLocked(options) {
|
|
1072
|
+
setDaemonExplicitlyDisabled(true, options);
|
|
1073
|
+
const runner = options.runner ?? runLaunchctl;
|
|
1074
|
+
const paths = launchdPaths(options);
|
|
1075
|
+
const service = getLaunchdService({ ...options, runner });
|
|
1076
|
+
if (service.loaded) {
|
|
1077
|
+
launchctlOrThrow(runner, ["bootout", paths.serviceTarget], "bootout");
|
|
1078
|
+
return { wasLoaded: true, legacyStopped: false, service };
|
|
1079
|
+
}
|
|
1080
|
+
const legacyStopped = await (options.probe ?? daemonIsLive)(READY_PROBE_TIMEOUT_MS) ? await (options.migrateLegacy ?? (() => stopVerifiedLegacyDaemon(options.homeDir ?? homedir())))() : false;
|
|
1081
|
+
return { wasLoaded: false, legacyStopped, service };
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// src/commands/claude-install.ts
|
|
549
1085
|
var CAPTURE_BIN = "prim-hook";
|
|
550
1086
|
var GATE_BIN = "prim-pre-tool-use";
|
|
551
1087
|
var POST_TOOL_USE_BIN = "prim-post-tool-use";
|
|
@@ -563,11 +1099,11 @@ var PRIM_BINS = [
|
|
|
563
1099
|
SESSION_END_BIN
|
|
564
1100
|
];
|
|
565
1101
|
var JSON_INDENT = 2;
|
|
566
|
-
var USER_SCOPE_PATH =
|
|
1102
|
+
var USER_SCOPE_PATH = join2(homedir2(), ".claude", "settings.json");
|
|
567
1103
|
function projectRoot() {
|
|
568
1104
|
return gitToplevel() ?? process.cwd();
|
|
569
1105
|
}
|
|
570
|
-
var projectScopePath = () =>
|
|
1106
|
+
var projectScopePath = () => join2(projectRoot(), ".claude", "settings.json");
|
|
571
1107
|
var CAPTURE_EVENTS = [
|
|
572
1108
|
"SessionStart",
|
|
573
1109
|
"UserPromptSubmit",
|
|
@@ -609,10 +1145,10 @@ function settingsPathFor(scope) {
|
|
|
609
1145
|
return scope === "user" ? USER_SCOPE_PATH : projectScopePath();
|
|
610
1146
|
}
|
|
611
1147
|
function readSettings(path) {
|
|
612
|
-
if (!
|
|
1148
|
+
if (!existsSync3(path)) {
|
|
613
1149
|
return {};
|
|
614
1150
|
}
|
|
615
|
-
const raw =
|
|
1151
|
+
const raw = readFileSync3(path, "utf-8");
|
|
616
1152
|
try {
|
|
617
1153
|
return JSON.parse(raw);
|
|
618
1154
|
} catch (err) {
|
|
@@ -645,15 +1181,24 @@ function ensureRegistration(list, reg, force) {
|
|
|
645
1181
|
return [...stripCommand(list, reg.bin), canonicalEntry(reg)];
|
|
646
1182
|
}
|
|
647
1183
|
var LEGACY_STATUSLINE_COMMAND = "prim statusline";
|
|
1184
|
+
var RUNTIME_STATUSLINE_SUFFIXES = [
|
|
1185
|
+
"/prim/runtime/prim-statusline",
|
|
1186
|
+
// One pre-launcher build persisted the JS entry directly. Keep recognizing
|
|
1187
|
+
// it so reinstall migrates and uninstall removes that older staged form.
|
|
1188
|
+
"/prim/runtime/current/prim-statusline"
|
|
1189
|
+
];
|
|
648
1190
|
function isPrimStatusLine(settings) {
|
|
649
1191
|
const s = settings.statusLine;
|
|
650
1192
|
if (s?.type !== "command") {
|
|
651
1193
|
return false;
|
|
652
1194
|
}
|
|
653
1195
|
const c = (s.command ?? "").trim();
|
|
654
|
-
return c === LEGACY_STATUSLINE_COMMAND || c.includes("@primitive.ai/prim") && c.includes("statusline");
|
|
1196
|
+
return c === LEGACY_STATUSLINE_COMMAND || c.includes("@primitive.ai/prim") && c.includes("statusline") || RUNTIME_STATUSLINE_SUFFIXES.some((suffix) => c.includes(suffix));
|
|
655
1197
|
}
|
|
656
|
-
function applyStatusLine(settings) {
|
|
1198
|
+
function applyStatusLine(settings, command = STATUSLINE_COMMAND) {
|
|
1199
|
+
if (command === false) {
|
|
1200
|
+
return isPrimStatusLine(settings) ? { ...settings, statusLine: void 0 } : settings;
|
|
1201
|
+
}
|
|
657
1202
|
if (settings.statusLine && !isPrimStatusLine(settings)) {
|
|
658
1203
|
return settings;
|
|
659
1204
|
}
|
|
@@ -661,7 +1206,7 @@ function applyStatusLine(settings) {
|
|
|
661
1206
|
...settings,
|
|
662
1207
|
statusLine: {
|
|
663
1208
|
type: "command",
|
|
664
|
-
command
|
|
1209
|
+
command,
|
|
665
1210
|
refreshInterval: STATUSLINE_REFRESH_SECONDS
|
|
666
1211
|
}
|
|
667
1212
|
};
|
|
@@ -721,7 +1266,12 @@ function applyInstall(settings, options = {}) {
|
|
|
721
1266
|
for (const reg of REGISTRATIONS) {
|
|
722
1267
|
hooks[reg.event] = ensureRegistration(hooks[reg.event] ?? [], reg, options.force ?? false);
|
|
723
1268
|
}
|
|
724
|
-
return applyPermissions(
|
|
1269
|
+
return applyPermissions(
|
|
1270
|
+
applyStatusLine(
|
|
1271
|
+
{ ...settings, hooks },
|
|
1272
|
+
options.installStatusline === false ? false : options.statuslineCommand ?? STATUSLINE_COMMAND
|
|
1273
|
+
)
|
|
1274
|
+
);
|
|
725
1275
|
}
|
|
726
1276
|
function applyUninstall(settings) {
|
|
727
1277
|
const source = settings.hooks ?? {};
|
|
@@ -746,19 +1296,34 @@ function captureInstalled(settings) {
|
|
|
746
1296
|
(event) => (settings.hooks?.[event] ?? []).some((e) => entryHasCommand(e, CAPTURE_BIN))
|
|
747
1297
|
);
|
|
748
1298
|
}
|
|
1299
|
+
function feedbackInstalled(settings) {
|
|
1300
|
+
const stopCapture = (settings.hooks?.Stop ?? []).some(
|
|
1301
|
+
(entry) => entryHasCommand(entry, CAPTURE_BIN)
|
|
1302
|
+
);
|
|
1303
|
+
const sessionStart = (settings.hooks?.SessionStart ?? []).some(
|
|
1304
|
+
(entry) => entryHasCommand(entry, SESSION_START_BIN)
|
|
1305
|
+
);
|
|
1306
|
+
return stopCapture && sessionStart;
|
|
1307
|
+
}
|
|
749
1308
|
function statuslineInstalled(settings) {
|
|
750
1309
|
return isPrimStatusLine(settings);
|
|
751
1310
|
}
|
|
1311
|
+
function statuslineState(settings) {
|
|
1312
|
+
if (isPrimStatusLine(settings)) {
|
|
1313
|
+
return "primitive";
|
|
1314
|
+
}
|
|
1315
|
+
return settings.statusLine ? "custom" : "empty";
|
|
1316
|
+
}
|
|
752
1317
|
function isGateInstalled(settings) {
|
|
753
1318
|
return (settings.hooks?.PreToolUse ?? []).some((e) => entryHasCommand(e, GATE_BIN));
|
|
754
1319
|
}
|
|
755
|
-
function
|
|
756
|
-
const dir =
|
|
757
|
-
if (!
|
|
758
|
-
|
|
1320
|
+
function atomicWrite2(path, content) {
|
|
1321
|
+
const dir = dirname3(path);
|
|
1322
|
+
if (!existsSync3(dir)) {
|
|
1323
|
+
mkdirSync3(dir, { recursive: true });
|
|
759
1324
|
}
|
|
760
1325
|
const tmp = `${path}.tmp.${String(Date.now())}`;
|
|
761
|
-
|
|
1326
|
+
writeFileSync3(tmp, `${JSON.stringify(content, null, JSON_INDENT)}
|
|
762
1327
|
`, "utf-8");
|
|
763
1328
|
const fd = openSync(tmp, "r+");
|
|
764
1329
|
try {
|
|
@@ -766,15 +1331,42 @@ function atomicWrite(path, content) {
|
|
|
766
1331
|
} finally {
|
|
767
1332
|
closeSync(fd);
|
|
768
1333
|
}
|
|
769
|
-
|
|
1334
|
+
renameSync2(tmp, path);
|
|
1335
|
+
}
|
|
1336
|
+
function userStatuslineBlocksProjectInstall(scope, userSettings) {
|
|
1337
|
+
return scope === "project" && statuslineState(userSettings) === "custom";
|
|
770
1338
|
}
|
|
771
1339
|
function performInstall(scope, force) {
|
|
772
1340
|
const path = settingsPathFor(scope);
|
|
773
1341
|
const before = readSettings(path);
|
|
774
|
-
const
|
|
1342
|
+
const blockedByUserStatusline = userStatuslineBlocksProjectInstall(
|
|
1343
|
+
scope,
|
|
1344
|
+
scope === "project" ? readSettings(USER_SCOPE_PATH) : {}
|
|
1345
|
+
);
|
|
1346
|
+
let statuslineCommand = STATUSLINE_COMMAND;
|
|
1347
|
+
if (!blockedByUserStatusline && process.platform === "darwin" && (!before.statusLine || isPrimStatusLine(before))) {
|
|
1348
|
+
try {
|
|
1349
|
+
const runtime = stageRuntime();
|
|
1350
|
+
if (!runtime.manifest.statuslineFile) {
|
|
1351
|
+
throw new Error("standalone prim-statusline entry is unavailable");
|
|
1352
|
+
}
|
|
1353
|
+
statuslineCommand = runtimeStatuslineCommand();
|
|
1354
|
+
} catch (error) {
|
|
1355
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1356
|
+
process.stderr.write(
|
|
1357
|
+
`[prim] \u26A0 could not stage the durable statusline runtime (${detail}); using the package resolver
|
|
1358
|
+
`
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
const after = applyInstall(before, {
|
|
1363
|
+
force,
|
|
1364
|
+
statuslineCommand,
|
|
1365
|
+
installStatusline: !blockedByUserStatusline
|
|
1366
|
+
});
|
|
775
1367
|
const changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
776
1368
|
if (changed) {
|
|
777
|
-
|
|
1369
|
+
atomicWrite2(path, after);
|
|
778
1370
|
}
|
|
779
1371
|
if (scope === "project") {
|
|
780
1372
|
activateRepoBestEffort(process.cwd());
|
|
@@ -784,7 +1376,9 @@ function performInstall(scope, force) {
|
|
|
784
1376
|
path,
|
|
785
1377
|
gate: isGateInstalled(after),
|
|
786
1378
|
capture: captureInstalled(after),
|
|
1379
|
+
feedback: feedbackInstalled(after),
|
|
787
1380
|
statusline: statuslineInstalled(after),
|
|
1381
|
+
statuslineState: statuslineState(after),
|
|
788
1382
|
changed
|
|
789
1383
|
};
|
|
790
1384
|
}
|
|
@@ -794,14 +1388,16 @@ function performUninstall(scope) {
|
|
|
794
1388
|
const after = applyUninstall(before);
|
|
795
1389
|
const changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
796
1390
|
if (changed) {
|
|
797
|
-
|
|
1391
|
+
atomicWrite2(path, after);
|
|
798
1392
|
}
|
|
799
1393
|
return {
|
|
800
1394
|
scope,
|
|
801
1395
|
path,
|
|
802
1396
|
gate: isGateInstalled(after),
|
|
803
1397
|
capture: captureInstalled(after),
|
|
1398
|
+
feedback: feedbackInstalled(after),
|
|
804
1399
|
statusline: statuslineInstalled(after),
|
|
1400
|
+
statuslineState: statuslineState(after),
|
|
805
1401
|
changed
|
|
806
1402
|
};
|
|
807
1403
|
}
|
|
@@ -811,7 +1407,7 @@ function performPermissionInstall(scope) {
|
|
|
811
1407
|
const after = applyAutoModeTrust(applyPermissions(before));
|
|
812
1408
|
const changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
813
1409
|
if (changed) {
|
|
814
|
-
|
|
1410
|
+
atomicWrite2(path, after);
|
|
815
1411
|
}
|
|
816
1412
|
return {
|
|
817
1413
|
scope,
|
|
@@ -828,7 +1424,9 @@ function performStatus() {
|
|
|
828
1424
|
path,
|
|
829
1425
|
gate: isGateInstalled(settings),
|
|
830
1426
|
capture: captureInstalled(settings),
|
|
831
|
-
|
|
1427
|
+
feedback: feedbackInstalled(settings),
|
|
1428
|
+
statusline: statuslineInstalled(settings),
|
|
1429
|
+
statuslineState: statuslineState(settings)
|
|
832
1430
|
};
|
|
833
1431
|
};
|
|
834
1432
|
return { user: statusFor(USER_SCOPE_PATH), project: statusFor(projectScopePath()) };
|
|
@@ -860,6 +1458,14 @@ function registerClaudeCommands(program2) {
|
|
|
860
1458
|
`[prim] Claude Code integration already present at ${result.path} (no changes)`
|
|
861
1459
|
);
|
|
862
1460
|
}
|
|
1461
|
+
const projectState = scope === "user" ? performStatus().project.statuslineState : void 0;
|
|
1462
|
+
const userState = scope === "project" ? performStatus().user.statuslineState : void 0;
|
|
1463
|
+
if (result.statuslineState === "custom" || projectState === "custom" || userState === "custom") {
|
|
1464
|
+
const owner = result.statuslineState === "custom" ? result.path : projectState === "custom" ? projectScopePath() : USER_SCOPE_PATH;
|
|
1465
|
+
console.error(
|
|
1466
|
+
`[prim] \u26A0 statusline skipped: an existing custom statusLine is preserved at ${owner}; run \`prim daemon status\` for Primitive health`
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
863
1469
|
console.log(JSON.stringify(result, null, JSON_INDENT));
|
|
864
1470
|
});
|
|
865
1471
|
claude.command("preauth").description(
|
|
@@ -889,11 +1495,11 @@ function registerClaudeCommands(program2) {
|
|
|
889
1495
|
console.log(JSON.stringify(result, null, JSON_INDENT));
|
|
890
1496
|
});
|
|
891
1497
|
claude.command("status").description(
|
|
892
|
-
"Report whether each prim surface (gate, capture, statusline) is installed per scope"
|
|
1498
|
+
"Report whether each prim surface (gate, capture, feedback, statusline) is installed per scope"
|
|
893
1499
|
).action(() => {
|
|
894
1500
|
const result = performStatus();
|
|
895
1501
|
const mark = (b) => b ? "\u2713" : "\u2717";
|
|
896
|
-
const line = (label, s) => `[prim] ${label}: gate ${mark(s.gate)} \xB7 capture ${mark(s.capture)} \xB7
|
|
1502
|
+
const line = (label, s) => `[prim] ${label}: gate ${mark(s.gate)} \xB7 capture ${mark(s.capture)} \xB7 feedback ${mark(s.feedback)} \xB7 statusline ${s.statuslineState} (${s.path})`;
|
|
897
1503
|
console.error(`${line("user", result.user)}
|
|
898
1504
|
${line("project", result.project)}`);
|
|
899
1505
|
console.log(JSON.stringify(result, null, JSON_INDENT));
|
|
@@ -901,8 +1507,8 @@ ${line("project", result.project)}`);
|
|
|
901
1507
|
}
|
|
902
1508
|
|
|
903
1509
|
// src/commands/codex-install.ts
|
|
904
|
-
import { homedir as
|
|
905
|
-
import { join as
|
|
1510
|
+
import { homedir as homedir3 } from "os";
|
|
1511
|
+
import { join as join3 } from "path";
|
|
906
1512
|
var CAPTURE_BIN2 = "prim-hook";
|
|
907
1513
|
var GATE_BIN2 = "prim-pre-tool-use";
|
|
908
1514
|
var POST_TOOL_USE_BIN2 = "prim-post-tool-use";
|
|
@@ -926,8 +1532,8 @@ var CODEX_REGISTRATIONS = [
|
|
|
926
1532
|
// refresh the bin cache the other hooks read. See lib/bin-cache.ts.
|
|
927
1533
|
makeRegistration("SessionStart", "*", SESSION_START_BIN2, CODEX_ARGS, { cacheRead: false })
|
|
928
1534
|
];
|
|
929
|
-
var USER_SCOPE_PATH2 =
|
|
930
|
-
var projectScopePath2 = () =>
|
|
1535
|
+
var USER_SCOPE_PATH2 = join3(homedir3(), ".codex", "hooks.json");
|
|
1536
|
+
var projectScopePath2 = () => join3(projectRoot(), ".codex", "hooks.json");
|
|
931
1537
|
function settingsPathFor2(scope) {
|
|
932
1538
|
return scope === "user" ? USER_SCOPE_PATH2 : projectScopePath2();
|
|
933
1539
|
}
|
|
@@ -975,7 +1581,7 @@ function performInstall2(scope, force) {
|
|
|
975
1581
|
const after = applyInstall2(before, { force });
|
|
976
1582
|
const changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
977
1583
|
if (changed) {
|
|
978
|
-
|
|
1584
|
+
atomicWrite2(path, after);
|
|
979
1585
|
}
|
|
980
1586
|
if (scope === "project") {
|
|
981
1587
|
activateRepoBestEffort(process.cwd());
|
|
@@ -988,7 +1594,7 @@ function performUninstall2(scope) {
|
|
|
988
1594
|
const after = applyUninstall2(before);
|
|
989
1595
|
const changed = JSON.stringify(before) !== JSON.stringify(after);
|
|
990
1596
|
if (changed) {
|
|
991
|
-
|
|
1597
|
+
atomicWrite2(path, after);
|
|
992
1598
|
}
|
|
993
1599
|
return resultFor(scope, path, after, changed);
|
|
994
1600
|
}
|
|
@@ -1051,11 +1657,30 @@ ${line("project", result.project)}`);
|
|
|
1051
1657
|
|
|
1052
1658
|
// src/commands/daemon.ts
|
|
1053
1659
|
import { spawn } from "child_process";
|
|
1054
|
-
import { closeSync as closeSync2, existsSync as
|
|
1055
|
-
import { homedir as
|
|
1056
|
-
import { join as
|
|
1660
|
+
import { closeSync as closeSync2, existsSync as existsSync4, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync4, unlinkSync } from "fs";
|
|
1661
|
+
import { homedir as homedir4 } from "os";
|
|
1662
|
+
import { join as join4 } from "path";
|
|
1057
1663
|
|
|
1058
1664
|
// src/lib/presence.ts
|
|
1665
|
+
var DECISION_ORIGIN = "https://app.getprimitive.ai";
|
|
1666
|
+
function validDecisionUrl(value) {
|
|
1667
|
+
if (typeof value !== "string" || stripControlChars(value) !== value) {
|
|
1668
|
+
return void 0;
|
|
1669
|
+
}
|
|
1670
|
+
try {
|
|
1671
|
+
const url = new URL(value);
|
|
1672
|
+
if (url.origin !== DECISION_ORIGIN || url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "" || !/^\/decisions\/[^/]+$/u.test(url.pathname)) {
|
|
1673
|
+
return void 0;
|
|
1674
|
+
}
|
|
1675
|
+
return url.href;
|
|
1676
|
+
} catch {
|
|
1677
|
+
return void 0;
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
function decisionLink(label, decisionUrl) {
|
|
1681
|
+
const url = validDecisionUrl(decisionUrl);
|
|
1682
|
+
return url ? `\x1B]8;;${url}\x07${label}\x1B]8;;\x07` : label;
|
|
1683
|
+
}
|
|
1059
1684
|
function formatLabeled(labels, cap) {
|
|
1060
1685
|
if (labels === void 0) {
|
|
1061
1686
|
return "\u2014";
|
|
@@ -1075,7 +1700,8 @@ function formatTeammatesWithArea(teammates, cap) {
|
|
|
1075
1700
|
return formatLabeled(
|
|
1076
1701
|
teammates?.map((t) => {
|
|
1077
1702
|
const area = t.area?.trim();
|
|
1078
|
-
|
|
1703
|
+
const label = area ? `${t.name} - ${area}` : t.name;
|
|
1704
|
+
return decisionLink(label, t.decisionUrl);
|
|
1079
1705
|
}),
|
|
1080
1706
|
cap
|
|
1081
1707
|
);
|
|
@@ -1083,33 +1709,35 @@ function formatTeammatesWithArea(teammates, cap) {
|
|
|
1083
1709
|
|
|
1084
1710
|
// src/commands/daemon.ts
|
|
1085
1711
|
var DAEMON_BIN = "prim-daemon-server";
|
|
1086
|
-
var CONFIG_DIR =
|
|
1087
|
-
var PID_PATH =
|
|
1088
|
-
var SOCK_PATH =
|
|
1089
|
-
var LOG_PATH =
|
|
1712
|
+
var CONFIG_DIR = join4(homedir4(), ".config", "prim");
|
|
1713
|
+
var PID_PATH = join4(CONFIG_DIR, "daemon.pid");
|
|
1714
|
+
var SOCK_PATH = join4(CONFIG_DIR, "sock");
|
|
1715
|
+
var LOG_PATH = join4(CONFIG_DIR, "daemon.log");
|
|
1090
1716
|
var CONFIG_DIR_MODE = 448;
|
|
1091
1717
|
var LOG_FILE_MODE = 384;
|
|
1092
1718
|
var STOP_TIMEOUT_MS = 5e3;
|
|
1093
1719
|
var STOP_POLL_MS = 100;
|
|
1094
1720
|
var STATUS_PROBE_TIMEOUT_MS = 500;
|
|
1095
|
-
var
|
|
1096
|
-
var
|
|
1097
|
-
var
|
|
1721
|
+
var READY_TIMEOUT_MS2 = 5e3;
|
|
1722
|
+
var READY_POLL_MS2 = 100;
|
|
1723
|
+
var READY_PROBE_TIMEOUT_MS2 = 250;
|
|
1724
|
+
var HEALTHY_TIMEOUT_MS = 3e4;
|
|
1725
|
+
var HEALTHY_POLL_MS = 250;
|
|
1098
1726
|
var EXIT_OK2 = 0;
|
|
1099
1727
|
var EXIT_NOT_RUNNING = 2;
|
|
1100
1728
|
var EXIT_BOOTING = 3;
|
|
1101
1729
|
function readPidfile() {
|
|
1102
|
-
if (!
|
|
1730
|
+
if (!existsSync4(PID_PATH)) {
|
|
1103
1731
|
return null;
|
|
1104
1732
|
}
|
|
1105
|
-
const raw =
|
|
1733
|
+
const raw = readFileSync4(PID_PATH, "utf-8").trim();
|
|
1106
1734
|
const pid = Number(raw);
|
|
1107
1735
|
if (!Number.isInteger(pid) || pid <= 0) {
|
|
1108
1736
|
return null;
|
|
1109
1737
|
}
|
|
1110
|
-
return { pid, alive:
|
|
1738
|
+
return { pid, alive: processIsAlive2(pid) };
|
|
1111
1739
|
}
|
|
1112
|
-
function
|
|
1740
|
+
function processIsAlive2(pid) {
|
|
1113
1741
|
try {
|
|
1114
1742
|
process.kill(pid, 0);
|
|
1115
1743
|
return true;
|
|
@@ -1128,40 +1756,89 @@ function clearStaleArtifacts() {
|
|
|
1128
1756
|
}
|
|
1129
1757
|
}
|
|
1130
1758
|
function sleep(ms) {
|
|
1131
|
-
return new Promise((
|
|
1132
|
-
const timer = setTimeout(resolve6, ms);
|
|
1133
|
-
timer.unref();
|
|
1134
|
-
});
|
|
1759
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
1135
1760
|
}
|
|
1136
1761
|
function spawnDaemon(options) {
|
|
1137
1762
|
const file = binFile(DAEMON_BIN);
|
|
1138
1763
|
return file ? spawn(process.execPath, [file], options) : spawn(DAEMON_BIN, [], options);
|
|
1139
1764
|
}
|
|
1140
1765
|
function openDaemonLog(configDir = CONFIG_DIR) {
|
|
1141
|
-
if (!
|
|
1142
|
-
|
|
1766
|
+
if (!existsSync4(configDir)) {
|
|
1767
|
+
mkdirSync4(configDir, { recursive: true, mode: CONFIG_DIR_MODE });
|
|
1143
1768
|
}
|
|
1144
|
-
return openSync2(
|
|
1769
|
+
return openSync2(join4(configDir, "daemon.log"), "a", LOG_FILE_MODE);
|
|
1145
1770
|
}
|
|
1146
|
-
async function
|
|
1147
|
-
const deadline = Date.now() +
|
|
1771
|
+
async function waitForReady2() {
|
|
1772
|
+
const deadline = Date.now() + READY_TIMEOUT_MS2;
|
|
1148
1773
|
while (Date.now() < deadline) {
|
|
1149
|
-
if (await daemonIsLive(
|
|
1774
|
+
if (await daemonIsLive(READY_PROBE_TIMEOUT_MS2)) {
|
|
1150
1775
|
return true;
|
|
1151
1776
|
}
|
|
1152
|
-
await sleep(
|
|
1777
|
+
await sleep(READY_POLL_MS2);
|
|
1153
1778
|
}
|
|
1154
|
-
return daemonIsLive(
|
|
1779
|
+
return daemonIsLive(READY_PROBE_TIMEOUT_MS2);
|
|
1155
1780
|
}
|
|
1156
|
-
async function
|
|
1781
|
+
async function waitForHealthySnapshot(expectedVersion) {
|
|
1782
|
+
const deadline = Date.now() + HEALTHY_TIMEOUT_MS;
|
|
1783
|
+
let snapshot = null;
|
|
1784
|
+
while (Date.now() < deadline) {
|
|
1785
|
+
snapshot = await daemonRequest(
|
|
1786
|
+
"status_snapshot",
|
|
1787
|
+
{},
|
|
1788
|
+
{ timeoutMs: STATUS_PROBE_TIMEOUT_MS }
|
|
1789
|
+
);
|
|
1790
|
+
if (snapshot?.healthy === true && snapshot.version === expectedVersion) return snapshot;
|
|
1791
|
+
await sleep(HEALTHY_POLL_MS);
|
|
1792
|
+
}
|
|
1793
|
+
return snapshot;
|
|
1794
|
+
}
|
|
1795
|
+
async function verifiedPid(existing) {
|
|
1796
|
+
if (!existing.alive) return null;
|
|
1797
|
+
const snapshot = await daemonRequest(
|
|
1798
|
+
"status_snapshot",
|
|
1799
|
+
{},
|
|
1800
|
+
{ timeoutMs: STATUS_PROBE_TIMEOUT_MS }
|
|
1801
|
+
);
|
|
1802
|
+
return snapshot?.pid === existing.pid ? snapshot : null;
|
|
1803
|
+
}
|
|
1804
|
+
function daemonStartIsHealthy(serviceReady, snapshot, expectedVersion) {
|
|
1805
|
+
return serviceReady && snapshot?.healthy === true && expectedVersion !== void 0 && snapshot.version === expectedVersion;
|
|
1806
|
+
}
|
|
1807
|
+
async function detachedDaemonStart(opts) {
|
|
1157
1808
|
const existing = readPidfile();
|
|
1158
1809
|
if (existing?.alive) {
|
|
1159
|
-
|
|
1810
|
+
const snapshot = await verifiedPid(existing);
|
|
1811
|
+
if (snapshot) {
|
|
1812
|
+
process.stderr.write(`[prim] daemon already running (pid=${existing.pid})
|
|
1160
1813
|
`);
|
|
1161
|
-
|
|
1814
|
+
console.log(JSON.stringify({ started: false, pid: existing.pid }, null, 2));
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
process.stderr.write(
|
|
1818
|
+
`[prim] refusing to replace live pid=${existing.pid}: daemon ownership could not be verified over ${SOCK_PATH}
|
|
1819
|
+
`
|
|
1820
|
+
);
|
|
1821
|
+
console.log(JSON.stringify({ started: false, pid: existing.pid, verified: false }, null, 2));
|
|
1822
|
+
if (!process.exitCode) process.exitCode = EXIT_BOOTING;
|
|
1162
1823
|
return;
|
|
1163
1824
|
}
|
|
1164
1825
|
if (existing && !existing.alive) {
|
|
1826
|
+
const socketOwner = await daemonRequest(
|
|
1827
|
+
"status_snapshot",
|
|
1828
|
+
{},
|
|
1829
|
+
{ timeoutMs: STATUS_PROBE_TIMEOUT_MS }
|
|
1830
|
+
);
|
|
1831
|
+
if (socketOwner) {
|
|
1832
|
+
process.stderr.write(
|
|
1833
|
+
`[prim] refusing to clear stale pidfile: socket is owned by pid=${socketOwner.pid}
|
|
1834
|
+
`
|
|
1835
|
+
);
|
|
1836
|
+
console.log(
|
|
1837
|
+
JSON.stringify({ started: false, pid: socketOwner.pid, verified: false }, null, 2)
|
|
1838
|
+
);
|
|
1839
|
+
if (!process.exitCode) process.exitCode = EXIT_BOOTING;
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1165
1842
|
clearStaleArtifacts();
|
|
1166
1843
|
}
|
|
1167
1844
|
if (opts.foreground) {
|
|
@@ -1185,7 +1862,7 @@ async function daemonStart(opts) {
|
|
|
1185
1862
|
if (logFd !== void 0) {
|
|
1186
1863
|
closeSync2(logFd);
|
|
1187
1864
|
}
|
|
1188
|
-
const live = await
|
|
1865
|
+
const live = await waitForReady2();
|
|
1189
1866
|
if (live) {
|
|
1190
1867
|
const after = readPidfile();
|
|
1191
1868
|
process.stderr.write(
|
|
@@ -1196,7 +1873,7 @@ async function daemonStart(opts) {
|
|
|
1196
1873
|
return;
|
|
1197
1874
|
}
|
|
1198
1875
|
process.stderr.write(
|
|
1199
|
-
`[prim] \u2717 daemon start: spawned but the socket did not respond within ${
|
|
1876
|
+
`[prim] \u2717 daemon start: spawned but the socket did not respond within ${READY_TIMEOUT_MS2}ms (check that \`${DAEMON_BIN}\` resolves, and see ${LOG_PATH})
|
|
1200
1877
|
`
|
|
1201
1878
|
);
|
|
1202
1879
|
console.log(JSON.stringify({ started: false }, null, 2));
|
|
@@ -1204,7 +1881,7 @@ async function daemonStart(opts) {
|
|
|
1204
1881
|
process.exitCode = EXIT_NOT_RUNNING;
|
|
1205
1882
|
}
|
|
1206
1883
|
}
|
|
1207
|
-
async function
|
|
1884
|
+
async function detachedDaemonStop() {
|
|
1208
1885
|
const existing = readPidfile();
|
|
1209
1886
|
if (!existing) {
|
|
1210
1887
|
process.stderr.write("[prim] daemon not running (no pidfile)\n");
|
|
@@ -1212,11 +1889,36 @@ async function daemonStop() {
|
|
|
1212
1889
|
return;
|
|
1213
1890
|
}
|
|
1214
1891
|
if (!existing.alive) {
|
|
1892
|
+
const socketOwner = await daemonRequest(
|
|
1893
|
+
"status_snapshot",
|
|
1894
|
+
{},
|
|
1895
|
+
{ timeoutMs: STATUS_PROBE_TIMEOUT_MS }
|
|
1896
|
+
);
|
|
1897
|
+
if (socketOwner) {
|
|
1898
|
+
process.stderr.write(
|
|
1899
|
+
`[prim] refusing to clear stale pidfile: socket is owned by pid=${socketOwner.pid}
|
|
1900
|
+
`
|
|
1901
|
+
);
|
|
1902
|
+
console.log(
|
|
1903
|
+
JSON.stringify({ stopped: false, pid: socketOwner.pid, verified: false }, null, 2)
|
|
1904
|
+
);
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1215
1907
|
clearStaleArtifacts();
|
|
1216
1908
|
process.stderr.write("[prim] daemon not running (cleared stale pidfile)\n");
|
|
1217
1909
|
console.log(JSON.stringify({ stopped: false, wasRunning: false }, null, 2));
|
|
1218
1910
|
return;
|
|
1219
1911
|
}
|
|
1912
|
+
const snapshot = await verifiedPid(existing);
|
|
1913
|
+
if (!snapshot) {
|
|
1914
|
+
process.stderr.write(
|
|
1915
|
+
`[prim] refusing to signal live pid=${existing.pid}: daemon ownership could not be verified over ${SOCK_PATH}
|
|
1916
|
+
`
|
|
1917
|
+
);
|
|
1918
|
+
console.log(JSON.stringify({ stopped: false, pid: existing.pid, verified: false }, null, 2));
|
|
1919
|
+
if (!process.exitCode) process.exitCode = EXIT_BOOTING;
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1220
1922
|
try {
|
|
1221
1923
|
process.kill(existing.pid, "SIGTERM");
|
|
1222
1924
|
} catch (err) {
|
|
@@ -1229,7 +1931,7 @@ async function daemonStop() {
|
|
|
1229
1931
|
}
|
|
1230
1932
|
const deadline = Date.now() + STOP_TIMEOUT_MS;
|
|
1231
1933
|
while (Date.now() < deadline) {
|
|
1232
|
-
if (!
|
|
1934
|
+
if (!processIsAlive2(existing.pid)) {
|
|
1233
1935
|
clearStaleArtifacts();
|
|
1234
1936
|
process.stderr.write(`[prim] daemon stopped (pid=${existing.pid})
|
|
1235
1937
|
`);
|
|
@@ -1243,23 +1945,212 @@ async function daemonStop() {
|
|
|
1243
1945
|
`
|
|
1244
1946
|
);
|
|
1245
1947
|
console.log(JSON.stringify({ stopped: false, pid: existing.pid }, null, 2));
|
|
1948
|
+
if (!process.exitCode) process.exitCode = EXIT_BOOTING;
|
|
1949
|
+
}
|
|
1950
|
+
async function macDaemonStart(forceRestart = false) {
|
|
1951
|
+
const result = await ensureMacDaemon({ explicitlyStarted: true, forceRestart });
|
|
1952
|
+
const serviceReady = result.state === "running";
|
|
1953
|
+
const expectedVersion = result.runtime?.manifest.version;
|
|
1954
|
+
const snapshot = serviceReady && expectedVersion ? await waitForHealthySnapshot(expectedVersion) : null;
|
|
1955
|
+
const healthy = daemonStartIsHealthy(serviceReady, snapshot, expectedVersion);
|
|
1956
|
+
if (healthy) {
|
|
1957
|
+
const verb = result.action === "none" ? "already running" : "started";
|
|
1958
|
+
process.stderr.write(
|
|
1959
|
+
`[prim] \u2713 daemon ${verb} under launchd (pid=${snapshot?.pid ?? result.service.pid ?? "?"})
|
|
1960
|
+
`
|
|
1961
|
+
);
|
|
1962
|
+
} else {
|
|
1963
|
+
process.stderr.write(
|
|
1964
|
+
`[prim] \u2717 launchd daemon did not reach healthy heartbeat + ingestion state (see ${LOG_PATH})
|
|
1965
|
+
`
|
|
1966
|
+
);
|
|
1967
|
+
if (!process.exitCode) process.exitCode = EXIT_NOT_RUNNING;
|
|
1968
|
+
}
|
|
1969
|
+
console.log(
|
|
1970
|
+
JSON.stringify(
|
|
1971
|
+
{
|
|
1972
|
+
started: healthy,
|
|
1973
|
+
supervised: true,
|
|
1974
|
+
action: result.action,
|
|
1975
|
+
pid: snapshot?.pid ?? result.service.pid,
|
|
1976
|
+
loaded: result.service.loaded,
|
|
1977
|
+
responding: result.responding,
|
|
1978
|
+
healthy,
|
|
1979
|
+
version: snapshot?.version,
|
|
1980
|
+
expectedVersion
|
|
1981
|
+
},
|
|
1982
|
+
null,
|
|
1983
|
+
2
|
|
1984
|
+
)
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
async function daemonStart(opts) {
|
|
1988
|
+
if (process.platform === "darwin") {
|
|
1989
|
+
if (!opts.foreground) {
|
|
1990
|
+
await macDaemonStart(false);
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
await withDaemonLifecycleLock(async () => {
|
|
1995
|
+
setDaemonExplicitlyDisabled(false);
|
|
1996
|
+
await detachedDaemonStart(opts);
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
async function daemonStop() {
|
|
2000
|
+
if (process.platform !== "darwin") {
|
|
2001
|
+
await withDaemonLifecycleLock(async () => {
|
|
2002
|
+
setDaemonExplicitlyDisabled(true);
|
|
2003
|
+
await detachedDaemonStop();
|
|
2004
|
+
});
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
const result = await bootoutMacDaemon();
|
|
2008
|
+
process.stderr.write(
|
|
2009
|
+
result.wasLoaded ? "[prim] daemon stopped and explicitly disabled\n" : result.legacyStopped ? "[prim] legacy daemon stopped and explicitly disabled\n" : "[prim] daemon was not loaded; explicitly disabled\n"
|
|
2010
|
+
);
|
|
2011
|
+
console.log(
|
|
2012
|
+
JSON.stringify(
|
|
2013
|
+
{
|
|
2014
|
+
stopped: result.wasLoaded || result.legacyStopped,
|
|
2015
|
+
wasRunning: result.wasLoaded || result.legacyStopped,
|
|
2016
|
+
supervised: true,
|
|
2017
|
+
disabled: true
|
|
2018
|
+
},
|
|
2019
|
+
null,
|
|
2020
|
+
2
|
|
2021
|
+
)
|
|
2022
|
+
);
|
|
1246
2023
|
}
|
|
1247
2024
|
function classifyStatus(pidAlive, responding, snapshot, pid) {
|
|
1248
2025
|
if (!pidAlive) {
|
|
1249
2026
|
return { json: { running: false }, exitCode: EXIT_NOT_RUNNING };
|
|
1250
2027
|
}
|
|
1251
|
-
if (!responding) {
|
|
2028
|
+
if (!responding) {
|
|
2029
|
+
return {
|
|
2030
|
+
json: { running: true, responding: false, state: "starting", pid },
|
|
2031
|
+
exitCode: EXIT_BOOTING
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
2034
|
+
if (!snapshot) {
|
|
2035
|
+
return { json: { running: true, responding: true }, exitCode: EXIT_OK2 };
|
|
2036
|
+
}
|
|
2037
|
+
if (snapshot.healthy === false) {
|
|
2038
|
+
return {
|
|
2039
|
+
json: { running: true, responding: true, state: "degraded", ...snapshot },
|
|
2040
|
+
exitCode: EXIT_BOOTING
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
2043
|
+
return { json: { running: true, responding: true, ...snapshot }, exitCode: EXIT_OK2 };
|
|
2044
|
+
}
|
|
2045
|
+
function classifyLaunchdStatus(service, responding, snapshot, disabled) {
|
|
2046
|
+
if (!service.loaded) {
|
|
2047
|
+
if (responding) {
|
|
2048
|
+
return {
|
|
2049
|
+
json: {
|
|
2050
|
+
running: true,
|
|
2051
|
+
responding: true,
|
|
2052
|
+
supervised: false,
|
|
2053
|
+
state: "unsupervised",
|
|
2054
|
+
disabled,
|
|
2055
|
+
...snapshot
|
|
2056
|
+
},
|
|
2057
|
+
exitCode: EXIT_BOOTING
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
return {
|
|
2061
|
+
json: { running: false, supervised: true, loaded: false, disabled },
|
|
2062
|
+
exitCode: EXIT_NOT_RUNNING
|
|
2063
|
+
};
|
|
2064
|
+
}
|
|
2065
|
+
if (!responding) {
|
|
2066
|
+
return {
|
|
2067
|
+
json: {
|
|
2068
|
+
running: true,
|
|
2069
|
+
responding: false,
|
|
2070
|
+
supervised: true,
|
|
2071
|
+
state: service.state ?? "starting",
|
|
2072
|
+
pid: service.pid,
|
|
2073
|
+
disabled
|
|
2074
|
+
},
|
|
2075
|
+
exitCode: EXIT_BOOTING
|
|
2076
|
+
};
|
|
2077
|
+
}
|
|
2078
|
+
if (!snapshot || service.pid === void 0) {
|
|
1252
2079
|
return {
|
|
1253
|
-
json: {
|
|
2080
|
+
json: {
|
|
2081
|
+
running: true,
|
|
2082
|
+
responding: true,
|
|
2083
|
+
supervised: true,
|
|
2084
|
+
state: "ownership_unverified",
|
|
2085
|
+
pid: service.pid,
|
|
2086
|
+
socketPid: snapshot?.pid,
|
|
2087
|
+
disabled
|
|
2088
|
+
},
|
|
2089
|
+
exitCode: EXIT_BOOTING
|
|
2090
|
+
};
|
|
2091
|
+
}
|
|
2092
|
+
if (service.pid !== snapshot.pid) {
|
|
2093
|
+
return {
|
|
2094
|
+
json: {
|
|
2095
|
+
running: true,
|
|
2096
|
+
responding: true,
|
|
2097
|
+
supervised: true,
|
|
2098
|
+
state: "pid_mismatch",
|
|
2099
|
+
pid: service.pid,
|
|
2100
|
+
socketPid: snapshot.pid,
|
|
2101
|
+
disabled
|
|
2102
|
+
},
|
|
1254
2103
|
exitCode: EXIT_BOOTING
|
|
1255
2104
|
};
|
|
1256
2105
|
}
|
|
2106
|
+
if (snapshot?.healthy === false) {
|
|
2107
|
+
return {
|
|
2108
|
+
json: {
|
|
2109
|
+
running: true,
|
|
2110
|
+
responding: true,
|
|
2111
|
+
supervised: true,
|
|
2112
|
+
state: "degraded",
|
|
2113
|
+
disabled,
|
|
2114
|
+
...snapshot
|
|
2115
|
+
},
|
|
2116
|
+
exitCode: EXIT_BOOTING
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
return {
|
|
2120
|
+
json: {
|
|
2121
|
+
running: true,
|
|
2122
|
+
responding: true,
|
|
2123
|
+
supervised: true,
|
|
2124
|
+
state: service.state ?? "running",
|
|
2125
|
+
disabled,
|
|
2126
|
+
...snapshot
|
|
2127
|
+
},
|
|
2128
|
+
exitCode: EXIT_OK2
|
|
2129
|
+
};
|
|
2130
|
+
}
|
|
2131
|
+
function writeLiveSnapshot(snapshot, supervised = false) {
|
|
1257
2132
|
if (!snapshot) {
|
|
1258
|
-
|
|
2133
|
+
process.stderr.write(
|
|
2134
|
+
supervised ? "[prim] \u2713 daemon live under launchd (no snapshot)\n" : "[prim] \u2713 daemon live\n"
|
|
2135
|
+
);
|
|
2136
|
+
return;
|
|
1259
2137
|
}
|
|
1260
|
-
|
|
2138
|
+
const team = snapshot.onlineNames !== void 0 ? ` \xB7 team: ${formatTeammates(snapshot.onlineNames, Number.POSITIVE_INFINITY)}` : "";
|
|
2139
|
+
if (snapshot.healthy === false) {
|
|
2140
|
+
process.stderr.write(
|
|
2141
|
+
`[prim] \u2717 daemon unhealthy${supervised ? " under launchd" : ""} \xB7 pid=${snapshot.pid}${team}
|
|
2142
|
+
`
|
|
2143
|
+
);
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
process.stderr.write(
|
|
2147
|
+
`[prim] \u2713 daemon live${supervised ? " under launchd" : ""} \xB7 pid=${snapshot.pid} \xB7 uptime=${Math.round(
|
|
2148
|
+
snapshot.uptimeMs / 1e3
|
|
2149
|
+
)}s \xB7 session=${snapshot.sessionId}${team}
|
|
2150
|
+
`
|
|
2151
|
+
);
|
|
1261
2152
|
}
|
|
1262
|
-
async function
|
|
2153
|
+
async function detachedDaemonStatus() {
|
|
1263
2154
|
const pid = readPidfile();
|
|
1264
2155
|
const pidAlive = pid?.alive ?? false;
|
|
1265
2156
|
const responding = pidAlive ? await daemonIsLive(STATUS_PROBE_TIMEOUT_MS) : false;
|
|
@@ -1274,38 +2165,127 @@ async function daemonStatus() {
|
|
|
1274
2165
|
} else if (!responding) {
|
|
1275
2166
|
process.stderr.write(`[prim] \u25CC daemon pid=${pid?.pid} starting (socket not responding yet)
|
|
1276
2167
|
`);
|
|
1277
|
-
} else if (!snapshot) {
|
|
1278
|
-
process.stderr.write("[prim] \u2713 daemon live (no snapshot)\n");
|
|
1279
2168
|
} else {
|
|
1280
|
-
|
|
2169
|
+
writeLiveSnapshot(snapshot);
|
|
2170
|
+
}
|
|
2171
|
+
console.log(JSON.stringify(json, null, 2));
|
|
2172
|
+
if (exitCode !== EXIT_OK2 && !process.exitCode) {
|
|
2173
|
+
process.exitCode = exitCode;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
async function macDaemonStatus() {
|
|
2177
|
+
const service = getLaunchdService();
|
|
2178
|
+
const responding = await daemonIsLive(STATUS_PROBE_TIMEOUT_MS);
|
|
2179
|
+
const snapshot = responding ? await daemonRequest(
|
|
2180
|
+
"status_snapshot",
|
|
2181
|
+
{},
|
|
2182
|
+
{ timeoutMs: STATUS_PROBE_TIMEOUT_MS }
|
|
2183
|
+
) : null;
|
|
2184
|
+
const disabled = daemonExplicitlyDisabled();
|
|
2185
|
+
const { json, exitCode } = classifyLaunchdStatus(service, responding, snapshot, disabled);
|
|
2186
|
+
if (!service.loaded && responding) {
|
|
2187
|
+
process.stderr.write(
|
|
2188
|
+
`[prim] \u25CC daemon pid=${snapshot?.pid ?? "?"} is live but not supervised by launchd
|
|
2189
|
+
`
|
|
2190
|
+
);
|
|
2191
|
+
} else if (!service.loaded) {
|
|
2192
|
+
process.stderr.write(`[prim] \u2717 daemon down${disabled ? " (explicitly disabled)" : ""}
|
|
2193
|
+
`);
|
|
2194
|
+
} else if (!responding) {
|
|
2195
|
+
process.stderr.write(
|
|
2196
|
+
`[prim] \u25CC launchd service ${service.state ?? "loaded"}; socket is not responding yet
|
|
2197
|
+
`
|
|
2198
|
+
);
|
|
2199
|
+
} else if (!snapshot || service.pid === void 0) {
|
|
2200
|
+
process.stderr.write("[prim] \u2717 launchd daemon socket ownership could not be verified\n");
|
|
2201
|
+
} else if (service.pid !== snapshot.pid) {
|
|
1281
2202
|
process.stderr.write(
|
|
1282
|
-
`[prim] \
|
|
2203
|
+
`[prim] \u2717 launchd pid=${service.pid} does not own the daemon socket (pid=${snapshot.pid})
|
|
1283
2204
|
`
|
|
1284
2205
|
);
|
|
2206
|
+
} else {
|
|
2207
|
+
writeLiveSnapshot(snapshot, true);
|
|
1285
2208
|
}
|
|
1286
2209
|
console.log(JSON.stringify(json, null, 2));
|
|
1287
|
-
if (exitCode !== EXIT_OK2 && !process.exitCode)
|
|
1288
|
-
|
|
2210
|
+
if (exitCode !== EXIT_OK2 && !process.exitCode) process.exitCode = exitCode;
|
|
2211
|
+
}
|
|
2212
|
+
async function daemonStatus() {
|
|
2213
|
+
if (process.platform === "darwin") {
|
|
2214
|
+
await macDaemonStatus();
|
|
2215
|
+
return;
|
|
1289
2216
|
}
|
|
2217
|
+
await detachedDaemonStatus();
|
|
1290
2218
|
}
|
|
1291
2219
|
async function daemonRestart(opts) {
|
|
2220
|
+
if (process.platform === "darwin" && !opts.foreground) {
|
|
2221
|
+
await macDaemonStart(true);
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
if (process.platform !== "darwin") {
|
|
2225
|
+
await withDaemonLifecycleLock(async () => {
|
|
2226
|
+
setDaemonExplicitlyDisabled(false);
|
|
2227
|
+
await detachedDaemonStop();
|
|
2228
|
+
await detachedDaemonStart(opts);
|
|
2229
|
+
});
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
1292
2232
|
await daemonStop();
|
|
1293
2233
|
await daemonStart(opts);
|
|
1294
2234
|
}
|
|
2235
|
+
async function daemonEnsure() {
|
|
2236
|
+
if (process.platform !== "darwin") {
|
|
2237
|
+
const disabled = await withDaemonLifecycleLock(async () => {
|
|
2238
|
+
if (daemonExplicitlyDisabled()) return true;
|
|
2239
|
+
await detachedDaemonStart({});
|
|
2240
|
+
return false;
|
|
2241
|
+
});
|
|
2242
|
+
if (disabled) {
|
|
2243
|
+
process.stderr.write("[prim] daemon remains explicitly disabled\n");
|
|
2244
|
+
console.log(JSON.stringify({ ensured: false, disabled: true, supervised: false }, null, 2));
|
|
2245
|
+
}
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
const result = await ensureMacDaemon();
|
|
2249
|
+
if (result.state === "disabled") {
|
|
2250
|
+
process.stderr.write("[prim] daemon remains explicitly disabled\n");
|
|
2251
|
+
} else if (result.state === "running") {
|
|
2252
|
+
process.stderr.write(`[prim] \u2713 daemon ensured under launchd (${result.action})
|
|
2253
|
+
`);
|
|
2254
|
+
} else {
|
|
2255
|
+
process.stderr.write(`[prim] \u2717 daemon ensure failed; see ${LOG_PATH}
|
|
2256
|
+
`);
|
|
2257
|
+
if (!process.exitCode) process.exitCode = EXIT_NOT_RUNNING;
|
|
2258
|
+
}
|
|
2259
|
+
console.log(
|
|
2260
|
+
JSON.stringify(
|
|
2261
|
+
{
|
|
2262
|
+
ensured: result.state === "running",
|
|
2263
|
+
disabled: result.state === "disabled",
|
|
2264
|
+
supervised: true,
|
|
2265
|
+
action: result.action
|
|
2266
|
+
},
|
|
2267
|
+
null,
|
|
2268
|
+
2
|
|
2269
|
+
)
|
|
2270
|
+
);
|
|
2271
|
+
}
|
|
1295
2272
|
function registerDaemonCommands(program2) {
|
|
1296
2273
|
const daemon = program2.command("daemon").description("Manage the prim companion daemon (latency unlock + presence + broadcast)");
|
|
1297
|
-
daemon.command("start").description("
|
|
2274
|
+
daemon.command("start").description("Start the daemon (installs a supervised LaunchAgent on macOS)").option("--foreground", "Run in the foreground (inherit stdio); use under launchd / systemd").action(async (opts) => {
|
|
1298
2275
|
await daemonStart(opts);
|
|
1299
2276
|
});
|
|
1300
|
-
daemon.command("stop").description("
|
|
2277
|
+
daemon.command("stop").description("Stop and explicitly disable the daemon").action(async () => {
|
|
1301
2278
|
await daemonStop();
|
|
1302
2279
|
});
|
|
1303
2280
|
daemon.command("status").description("Report daemon liveness + a snapshot if responding").action(async () => {
|
|
1304
2281
|
await daemonStatus();
|
|
1305
2282
|
});
|
|
1306
|
-
daemon.command("restart").description("
|
|
2283
|
+
daemon.command("restart").description("Restart the daemon and clear any explicit disable marker").option("--foreground", "Restart in the foreground").action(async (opts) => {
|
|
1307
2284
|
await daemonRestart(opts);
|
|
1308
2285
|
});
|
|
2286
|
+
daemon.command("ensure").description("Idempotently install, upgrade, and heal the daemon unless explicitly disabled").action(async () => {
|
|
2287
|
+
await daemonEnsure();
|
|
2288
|
+
});
|
|
1309
2289
|
}
|
|
1310
2290
|
|
|
1311
2291
|
// src/decisions/cascade-renderer.ts
|
|
@@ -2153,11 +3133,11 @@ function registerDecisionsCommands(program2) {
|
|
|
2153
3133
|
}
|
|
2154
3134
|
|
|
2155
3135
|
// src/commands/doctor.ts
|
|
2156
|
-
import { existsSync as
|
|
3136
|
+
import { existsSync as existsSync5 } from "fs";
|
|
2157
3137
|
var DAEMON_PROBE_TIMEOUT_MS = 500;
|
|
2158
3138
|
var CONNECTIVITY_TIMEOUT_MS = 3e3;
|
|
2159
3139
|
var MS_PER_SECOND = 1e3;
|
|
2160
|
-
var STALE_PENDING_MS =
|
|
3140
|
+
var STALE_PENDING_MS = 3e4;
|
|
2161
3141
|
var EXIT_UNHEALTHY = 1;
|
|
2162
3142
|
function classifyDoctor(checks) {
|
|
2163
3143
|
const status = checks.some((c) => c.status === "fail") ? "fail" : checks.some((c) => c.status === "warn") ? "warn" : "ok";
|
|
@@ -2171,7 +3151,7 @@ function checkAuth() {
|
|
|
2171
3151
|
return { name: "auth", status: "fail", detail: "no token \u2014 run `prim auth login`" };
|
|
2172
3152
|
}
|
|
2173
3153
|
const expiresAt = getTokenExpiresAt();
|
|
2174
|
-
const hasRefresh =
|
|
3154
|
+
const hasRefresh = existsSync5(REFRESH_TOKEN_PATH);
|
|
2175
3155
|
if (expiresAt !== void 0 && Date.now() >= expiresAt) {
|
|
2176
3156
|
return hasRefresh ? { name: "auth", status: "warn", detail: "access token expired (refresh available)" } : {
|
|
2177
3157
|
name: "auth",
|
|
@@ -2185,62 +3165,269 @@ function checkAuth() {
|
|
|
2185
3165
|
const detail = expiresAt !== void 0 ? `valid (${String(Math.round((expiresAt - Date.now()) / MS_PER_SECOND))}s left)` : "valid";
|
|
2186
3166
|
return { name: "auth", status: "ok", detail };
|
|
2187
3167
|
}
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
3168
|
+
function classifyDaemonHealth(snapshot, options = {}) {
|
|
3169
|
+
if (options.disabled) {
|
|
3170
|
+
return {
|
|
3171
|
+
name: "daemon",
|
|
3172
|
+
status: "fail",
|
|
3173
|
+
detail: "explicitly stopped \u2014 run `prim daemon start`"
|
|
3174
|
+
};
|
|
3175
|
+
}
|
|
3176
|
+
if (options.service && !options.service.loaded) {
|
|
3177
|
+
return {
|
|
3178
|
+
name: "daemon",
|
|
3179
|
+
status: "fail",
|
|
3180
|
+
detail: "launchd service is not loaded \u2014 run `prim daemon start`"
|
|
3181
|
+
};
|
|
3182
|
+
}
|
|
3183
|
+
if (!snapshot) {
|
|
3184
|
+
return {
|
|
3185
|
+
name: "daemon",
|
|
3186
|
+
status: "fail",
|
|
3187
|
+
detail: "socket unavailable \u2014 run `prim daemon start`"
|
|
3188
|
+
};
|
|
3189
|
+
}
|
|
3190
|
+
if (options.service && (!Number.isInteger(options.service.pid) || (options.service.pid ?? 0) <= 0 || !Number.isInteger(snapshot.pid) || (snapshot.pid ?? 0) <= 0 || options.service.pid !== snapshot.pid)) {
|
|
3191
|
+
return {
|
|
3192
|
+
name: "daemon",
|
|
3193
|
+
status: "fail",
|
|
3194
|
+
detail: `launchd does not own the daemon socket (launchd ${String(options.service.pid ?? "none")} \xB7 socket ${String(snapshot.pid ?? "none")})`
|
|
3195
|
+
};
|
|
3196
|
+
}
|
|
3197
|
+
if (!snapshot.heartbeat?.healthy) {
|
|
3198
|
+
return {
|
|
3199
|
+
name: "daemon",
|
|
3200
|
+
status: "fail",
|
|
3201
|
+
detail: `heartbeat unhealthy${snapshot.heartbeat?.lastError ? ` \u2014 ${snapshot.heartbeat.lastError}` : ""}`
|
|
3202
|
+
};
|
|
3203
|
+
}
|
|
3204
|
+
if (!snapshot.ingestion?.healthy) {
|
|
3205
|
+
const pending = snapshot.ingestion?.pendingCount ?? 0;
|
|
3206
|
+
const pendingLabel = snapshot.ingestion?.pendingSampled ? `at least ${String(pending)}` : String(pending);
|
|
3207
|
+
return {
|
|
3208
|
+
name: "daemon",
|
|
3209
|
+
status: "fail",
|
|
3210
|
+
detail: `ingestion unhealthy \xB7 ${pendingLabel} pending${snapshot.ingestion?.lastError ? ` \u2014 ${snapshot.ingestion.lastError}` : ""}`
|
|
3211
|
+
};
|
|
3212
|
+
}
|
|
3213
|
+
if (snapshot.healthy !== true) {
|
|
3214
|
+
return { name: "daemon", status: "fail", detail: "health state is not ready" };
|
|
3215
|
+
}
|
|
3216
|
+
return {
|
|
2191
3217
|
name: "daemon",
|
|
2192
|
-
status: "
|
|
2193
|
-
detail:
|
|
3218
|
+
status: "ok",
|
|
3219
|
+
detail: `supervised and healthy${snapshot.version ? ` \xB7 v${snapshot.version}` : ""}`
|
|
2194
3220
|
};
|
|
2195
3221
|
}
|
|
3222
|
+
async function checkDaemon() {
|
|
3223
|
+
let service;
|
|
3224
|
+
if (process.platform === "darwin") {
|
|
3225
|
+
try {
|
|
3226
|
+
service = getLaunchdService();
|
|
3227
|
+
} catch {
|
|
3228
|
+
service = { loaded: false };
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
const snapshot = await daemonRequest(
|
|
3232
|
+
"status_snapshot",
|
|
3233
|
+
{},
|
|
3234
|
+
{ timeoutMs: DAEMON_PROBE_TIMEOUT_MS }
|
|
3235
|
+
);
|
|
3236
|
+
return classifyDaemonHealth(snapshot, {
|
|
3237
|
+
disabled: daemonExplicitlyDisabled(),
|
|
3238
|
+
service
|
|
3239
|
+
});
|
|
3240
|
+
}
|
|
2196
3241
|
function checkJournal() {
|
|
2197
|
-
const stats =
|
|
2198
|
-
const pending = stats.
|
|
3242
|
+
const stats = pendingJournalStats();
|
|
3243
|
+
const pending = stats.pendingCount;
|
|
3244
|
+
const pendingLabel = stats.sampled ? `at least ${String(pending)}` : String(pending);
|
|
2199
3245
|
if (pending === 0) {
|
|
3246
|
+
if (stats.sampled) {
|
|
3247
|
+
return {
|
|
3248
|
+
name: "journal",
|
|
3249
|
+
status: "fail",
|
|
3250
|
+
detail: "bounded journal sample could not prove the queue is empty"
|
|
3251
|
+
};
|
|
3252
|
+
}
|
|
2200
3253
|
return { name: "journal", status: "ok", detail: "no pending moves" };
|
|
2201
3254
|
}
|
|
2202
|
-
|
|
3255
|
+
if (stats.oldestPendingAt === void 0) {
|
|
3256
|
+
return {
|
|
3257
|
+
name: "journal",
|
|
3258
|
+
status: "fail",
|
|
3259
|
+
detail: `${pendingLabel} pending with no readable capture timestamp`
|
|
3260
|
+
};
|
|
3261
|
+
}
|
|
3262
|
+
const oldestMs = Date.now() - stats.oldestPendingAt;
|
|
2203
3263
|
const oldestS = Math.round(oldestMs / MS_PER_SECOND);
|
|
2204
3264
|
if (oldestMs > STALE_PENDING_MS) {
|
|
2205
3265
|
return {
|
|
2206
3266
|
name: "journal",
|
|
2207
|
-
status: "
|
|
2208
|
-
detail: `${
|
|
3267
|
+
status: "fail",
|
|
3268
|
+
detail: `${pendingLabel} pending, oldest observed ${String(oldestS)}s \u2014 30s delivery SLA missed`
|
|
3269
|
+
};
|
|
3270
|
+
}
|
|
3271
|
+
if (stats.sampled) {
|
|
3272
|
+
return {
|
|
3273
|
+
name: "journal",
|
|
3274
|
+
status: "fail",
|
|
3275
|
+
detail: `${pendingLabel} pending in bounded sample; 30s delivery SLA cannot be proven`
|
|
2209
3276
|
};
|
|
2210
3277
|
}
|
|
2211
3278
|
return { name: "journal", status: "ok", detail: `${String(pending)} pending, draining` };
|
|
2212
3279
|
}
|
|
2213
3280
|
function checkStranded() {
|
|
2214
|
-
const
|
|
2215
|
-
if (
|
|
3281
|
+
const stats = pendingJournalStats();
|
|
3282
|
+
if (stats.strandedFileCount === 0 && !stats.strandedSampled) {
|
|
2216
3283
|
return { name: "stranded", status: "ok", detail: "none" };
|
|
2217
3284
|
}
|
|
2218
|
-
const
|
|
3285
|
+
const qualifier = stats.strandedSampled ? "at least " : "";
|
|
2219
3286
|
return {
|
|
2220
3287
|
name: "stranded",
|
|
2221
3288
|
status: "warn",
|
|
2222
|
-
detail: `${String(
|
|
3289
|
+
detail: `${qualifier}${String(stats.strandedCount)} move(s) in ${qualifier}${String(stats.strandedFileCount)} file(s) \u2014 run \`prim moves flush\``
|
|
3290
|
+
};
|
|
3291
|
+
}
|
|
3292
|
+
function checkWorkspaceIdentity() {
|
|
3293
|
+
const identity = inspectWorkspaceId();
|
|
3294
|
+
switch (identity.status) {
|
|
3295
|
+
case "ready":
|
|
3296
|
+
return { name: "feedback-id", status: "ok", detail: "stable worktree identity ready" };
|
|
3297
|
+
case "missing":
|
|
3298
|
+
return {
|
|
3299
|
+
name: "feedback-id",
|
|
3300
|
+
status: "warn",
|
|
3301
|
+
detail: "not initialized \u2014 the next active hook will create it"
|
|
3302
|
+
};
|
|
3303
|
+
case "not_git":
|
|
3304
|
+
return {
|
|
3305
|
+
name: "feedback-id",
|
|
3306
|
+
status: "warn",
|
|
3307
|
+
detail: "not in a Git worktree \u2014 capture falls back to legacy V1"
|
|
3308
|
+
};
|
|
3309
|
+
case "corrupt":
|
|
3310
|
+
return {
|
|
3311
|
+
name: "feedback-id",
|
|
3312
|
+
status: "warn",
|
|
3313
|
+
detail: "identity is corrupt \u2014 not rotated; capture falls back to legacy V1"
|
|
3314
|
+
};
|
|
3315
|
+
case "unavailable":
|
|
3316
|
+
return {
|
|
3317
|
+
name: "feedback-id",
|
|
3318
|
+
status: "warn",
|
|
3319
|
+
detail: `identity unavailable during ${identity.operation} \u2014 capture falls back to legacy V1`
|
|
3320
|
+
};
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
function checkFeedbackHooks() {
|
|
3324
|
+
try {
|
|
3325
|
+
const status = performStatus();
|
|
3326
|
+
if (status.project.feedback || status.user.feedback) {
|
|
3327
|
+
return {
|
|
3328
|
+
name: "feedback-hooks",
|
|
3329
|
+
status: "ok",
|
|
3330
|
+
detail: status.project.feedback ? "project handlers ready" : "user handlers ready"
|
|
3331
|
+
};
|
|
3332
|
+
}
|
|
3333
|
+
return {
|
|
3334
|
+
name: "feedback-hooks",
|
|
3335
|
+
status: "warn",
|
|
3336
|
+
detail: "Stop + SessionStart handlers missing \u2014 run `prim claude install`"
|
|
3337
|
+
};
|
|
3338
|
+
} catch (error) {
|
|
3339
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3340
|
+
return { name: "feedback-hooks", status: "warn", detail: message.slice(0, 80) };
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
function parseMovesStatus(value) {
|
|
3344
|
+
if (!value || typeof value !== "object") {
|
|
3345
|
+
throw new Error("moves status returned a non-object response");
|
|
3346
|
+
}
|
|
3347
|
+
const status = value;
|
|
3348
|
+
const nullableNumber = (candidate) => candidate === null || typeof candidate === "number" && Number.isFinite(candidate);
|
|
3349
|
+
if (status.captureState !== "enabled" && status.captureState !== "disabled" || !nullableNumber(status.latestIngestAt) || !nullableNumber(status.latestClassificationAt) || !nullableNumber(status.highWaterMark) || typeof status.pendingSessionCount !== "number" || !Number.isInteger(status.pendingSessionCount) || status.pendingSessionCount < 0 || typeof status.sampled !== "boolean") {
|
|
3350
|
+
throw new Error("moves status returned an invalid response");
|
|
3351
|
+
}
|
|
3352
|
+
return status;
|
|
3353
|
+
}
|
|
3354
|
+
function classifyMovesStatus(status) {
|
|
3355
|
+
const capture = status.captureState === "enabled" ? { name: "capture", status: "ok", detail: "enabled; ingest endpoint durable" } : {
|
|
3356
|
+
name: "capture",
|
|
3357
|
+
status: "fail",
|
|
3358
|
+
detail: "disabled for the current organization; local Moves are retained"
|
|
2223
3359
|
};
|
|
3360
|
+
let classification;
|
|
3361
|
+
if (status.pendingSessionCount > 0) {
|
|
3362
|
+
classification = {
|
|
3363
|
+
name: "classification",
|
|
3364
|
+
status: "warn",
|
|
3365
|
+
detail: `${String(status.pendingSessionCount)} session(s) pending${status.sampled ? " in a bounded sample" : ""}`
|
|
3366
|
+
};
|
|
3367
|
+
} else if (status.sampled) {
|
|
3368
|
+
classification = {
|
|
3369
|
+
name: "classification",
|
|
3370
|
+
status: "warn",
|
|
3371
|
+
detail: "caught up in the bounded sample; older sessions were not inspected"
|
|
3372
|
+
};
|
|
3373
|
+
} else {
|
|
3374
|
+
classification = {
|
|
3375
|
+
name: "classification",
|
|
3376
|
+
status: "ok",
|
|
3377
|
+
detail: status.latestClassificationAt === null ? "no sessions awaiting classification" : `caught up \xB7 last ${new Date(status.latestClassificationAt).toISOString()}`
|
|
3378
|
+
};
|
|
3379
|
+
}
|
|
3380
|
+
return [capture, classification];
|
|
2224
3381
|
}
|
|
2225
|
-
async function
|
|
3382
|
+
async function checkBackend() {
|
|
2226
3383
|
try {
|
|
2227
|
-
await getClient().get("/api/cli/
|
|
3384
|
+
const response = await getClient().get("/api/cli/moves/status", {
|
|
2228
3385
|
signal: AbortSignal.timeout(CONNECTIVITY_TIMEOUT_MS)
|
|
2229
3386
|
});
|
|
2230
|
-
|
|
3387
|
+
const status = parseMovesStatus(response);
|
|
3388
|
+
return [
|
|
3389
|
+
{ name: "connectivity", status: "ok", detail: "server reachable and authenticated" },
|
|
3390
|
+
...classifyMovesStatus(status)
|
|
3391
|
+
];
|
|
2231
3392
|
} catch (err) {
|
|
2232
3393
|
const message = err instanceof Error ? err.message : String(err);
|
|
2233
|
-
|
|
2234
|
-
|
|
3394
|
+
return [{ name: "connectivity", status: "fail", detail: message.slice(0, 120) }];
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
async function checkFeedbackCapability() {
|
|
3398
|
+
try {
|
|
3399
|
+
const capability = await fetchFeedbackCapability(AbortSignal.timeout(CONNECTIVITY_TIMEOUT_MS));
|
|
3400
|
+
return capability.status === "available" ? { name: "feedback-api", status: "ok", detail: "server supports decision feedback" } : {
|
|
3401
|
+
name: "feedback-api",
|
|
3402
|
+
status: "warn",
|
|
3403
|
+
detail: "available after binding this CLI to an organization"
|
|
3404
|
+
};
|
|
3405
|
+
} catch (error) {
|
|
3406
|
+
if (error instanceof HttpError && error.status === 404) {
|
|
3407
|
+
return {
|
|
3408
|
+
name: "feedback-api",
|
|
3409
|
+
status: "warn",
|
|
3410
|
+
detail: "server does not support decision feedback yet"
|
|
3411
|
+
};
|
|
3412
|
+
}
|
|
3413
|
+
if (error instanceof HttpError && error.status === 401) {
|
|
3414
|
+
return { name: "feedback-api", status: "fail", detail: error.message.slice(0, 80) };
|
|
3415
|
+
}
|
|
3416
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3417
|
+
return { name: "feedback-api", status: "warn", detail: message.slice(0, 80) };
|
|
2235
3418
|
}
|
|
2236
3419
|
}
|
|
2237
3420
|
async function collectChecks() {
|
|
3421
|
+
const backend = await checkBackend();
|
|
2238
3422
|
return [
|
|
2239
3423
|
checkAuth(),
|
|
2240
3424
|
await checkDaemon(),
|
|
2241
3425
|
checkJournal(),
|
|
2242
3426
|
checkStranded(),
|
|
2243
|
-
|
|
3427
|
+
checkFeedbackHooks(),
|
|
3428
|
+
checkWorkspaceIdentity(),
|
|
3429
|
+
...backend,
|
|
3430
|
+
await checkFeedbackCapability()
|
|
2244
3431
|
];
|
|
2245
3432
|
}
|
|
2246
3433
|
function icon(status) {
|
|
@@ -2262,26 +3449,28 @@ async function runDoctor() {
|
|
|
2262
3449
|
}
|
|
2263
3450
|
}
|
|
2264
3451
|
function registerDoctorCommands(program2) {
|
|
2265
|
-
program2.command("doctor").description(
|
|
3452
|
+
program2.command("doctor").description(
|
|
3453
|
+
"Check capture and feedback health end to end (auth, supervisor, delivery, worktree, server)"
|
|
3454
|
+
).action(async () => {
|
|
2266
3455
|
await runDoctor();
|
|
2267
3456
|
});
|
|
2268
3457
|
}
|
|
2269
3458
|
|
|
2270
3459
|
// src/commands/hermes-install.ts
|
|
2271
3460
|
import {
|
|
2272
|
-
chmodSync,
|
|
3461
|
+
chmodSync as chmodSync2,
|
|
2273
3462
|
closeSync as closeSync3,
|
|
2274
|
-
existsSync as
|
|
3463
|
+
existsSync as existsSync6,
|
|
2275
3464
|
fsyncSync as fsyncSync2,
|
|
2276
|
-
mkdirSync as
|
|
3465
|
+
mkdirSync as mkdirSync5,
|
|
2277
3466
|
openSync as openSync3,
|
|
2278
|
-
readFileSync as
|
|
2279
|
-
renameSync as
|
|
2280
|
-
rmSync as
|
|
2281
|
-
writeFileSync as
|
|
3467
|
+
readFileSync as readFileSync5,
|
|
3468
|
+
renameSync as renameSync3,
|
|
3469
|
+
rmSync as rmSync3,
|
|
3470
|
+
writeFileSync as writeFileSync4
|
|
2282
3471
|
} from "fs";
|
|
2283
|
-
import { homedir as
|
|
2284
|
-
import { dirname as
|
|
3472
|
+
import { homedir as homedir5 } from "os";
|
|
3473
|
+
import { dirname as dirname4, join as join5 } from "path";
|
|
2285
3474
|
import { Document, parseDocument, stringify } from "yaml";
|
|
2286
3475
|
var CAPTURE_BIN3 = "prim-hook";
|
|
2287
3476
|
var GATE_BIN3 = "prim-pre-tool-use";
|
|
@@ -2300,13 +3489,13 @@ var PRIM_BINS3 = [
|
|
|
2300
3489
|
SESSION_END_BIN2
|
|
2301
3490
|
];
|
|
2302
3491
|
function hermesHome() {
|
|
2303
|
-
return process.env.HERMES_HOME ??
|
|
3492
|
+
return process.env.HERMES_HOME ?? join5(homedir5(), ".hermes");
|
|
2304
3493
|
}
|
|
2305
3494
|
function configPath() {
|
|
2306
|
-
return
|
|
3495
|
+
return join5(hermesHome(), "config.yaml");
|
|
2307
3496
|
}
|
|
2308
3497
|
function shimPath() {
|
|
2309
|
-
return
|
|
3498
|
+
return join5(hermesHome(), "agent-hooks", "prim-shim.sh");
|
|
2310
3499
|
}
|
|
2311
3500
|
var SHIM_SCRIPT = `#!/bin/sh
|
|
2312
3501
|
# prim Hermes hook shim \u2014 managed by \`prim hermes install\`. Hermes runs hooks
|
|
@@ -2395,10 +3584,10 @@ function isCaptureInstalled(hooks) {
|
|
|
2395
3584
|
);
|
|
2396
3585
|
}
|
|
2397
3586
|
function readDoc(path) {
|
|
2398
|
-
if (!
|
|
3587
|
+
if (!existsSync6(path)) {
|
|
2399
3588
|
return new Document();
|
|
2400
3589
|
}
|
|
2401
|
-
const raw =
|
|
3590
|
+
const raw = readFileSync5(path, "utf-8");
|
|
2402
3591
|
return raw.trim().length === 0 ? new Document() : parseDocument(raw);
|
|
2403
3592
|
}
|
|
2404
3593
|
function readHooks(doc) {
|
|
@@ -2470,19 +3659,19 @@ function setAutoAccept(raw) {
|
|
|
2470
3659
|
`;
|
|
2471
3660
|
}
|
|
2472
3661
|
function atomicWriteFile(path, content) {
|
|
2473
|
-
const dir =
|
|
2474
|
-
if (!
|
|
2475
|
-
|
|
3662
|
+
const dir = dirname4(path);
|
|
3663
|
+
if (!existsSync6(dir)) {
|
|
3664
|
+
mkdirSync5(dir, { recursive: true });
|
|
2476
3665
|
}
|
|
2477
3666
|
const tmp = `${path}.tmp.${String(process.pid)}`;
|
|
2478
|
-
|
|
3667
|
+
writeFileSync4(tmp, content, "utf-8");
|
|
2479
3668
|
const fd = openSync3(tmp, "r+");
|
|
2480
3669
|
try {
|
|
2481
3670
|
fsyncSync2(fd);
|
|
2482
3671
|
} finally {
|
|
2483
3672
|
closeSync3(fd);
|
|
2484
3673
|
}
|
|
2485
|
-
|
|
3674
|
+
renameSync3(tmp, path);
|
|
2486
3675
|
}
|
|
2487
3676
|
function mergeKeepsYamlValid(before, after) {
|
|
2488
3677
|
return parseDocument(after).errors.length <= parseDocument(before).errors.length;
|
|
@@ -2495,15 +3684,15 @@ function assertMergeValid(before, after) {
|
|
|
2495
3684
|
}
|
|
2496
3685
|
function writeShim() {
|
|
2497
3686
|
const path = shimPath();
|
|
2498
|
-
const dir =
|
|
2499
|
-
if (!
|
|
2500
|
-
|
|
3687
|
+
const dir = dirname4(path);
|
|
3688
|
+
if (!existsSync6(dir)) {
|
|
3689
|
+
mkdirSync5(dir, { recursive: true });
|
|
2501
3690
|
}
|
|
2502
|
-
|
|
2503
|
-
|
|
3691
|
+
writeFileSync4(path, SHIM_SCRIPT, "utf-8");
|
|
3692
|
+
chmodSync2(path, SHIM_MODE);
|
|
2504
3693
|
}
|
|
2505
3694
|
function removeShim() {
|
|
2506
|
-
|
|
3695
|
+
rmSync3(shimPath(), { force: true });
|
|
2507
3696
|
}
|
|
2508
3697
|
function autoAcceptOf(raw) {
|
|
2509
3698
|
if (raw.trim().length === 0) {
|
|
@@ -2516,7 +3705,7 @@ function readHooksFromRaw(raw) {
|
|
|
2516
3705
|
}
|
|
2517
3706
|
function performInstall3(opts) {
|
|
2518
3707
|
const path = configPath();
|
|
2519
|
-
const raw =
|
|
3708
|
+
const raw = existsSync6(path) ? readFileSync5(path, "utf-8") : "";
|
|
2520
3709
|
const existing = readHooksFromRaw(raw);
|
|
2521
3710
|
const desired = applyInstall3(existing, opts.force);
|
|
2522
3711
|
let next = raw;
|
|
@@ -2542,11 +3731,11 @@ function performInstall3(opts) {
|
|
|
2542
3731
|
}
|
|
2543
3732
|
function performUninstall3() {
|
|
2544
3733
|
const path = configPath();
|
|
2545
|
-
if (!
|
|
3734
|
+
if (!existsSync6(path)) {
|
|
2546
3735
|
removeShim();
|
|
2547
3736
|
return { path, gate: false, capture: false, autoAccept: false, changed: false };
|
|
2548
3737
|
}
|
|
2549
|
-
const raw =
|
|
3738
|
+
const raw = readFileSync5(path, "utf-8");
|
|
2550
3739
|
const existing = readHooksFromRaw(raw);
|
|
2551
3740
|
const remaining = applyUninstall3(existing);
|
|
2552
3741
|
const next = JSON.stringify(existing) === JSON.stringify(remaining) ? raw : spliceHooks(raw, remaining);
|
|
@@ -2566,7 +3755,7 @@ function performUninstall3() {
|
|
|
2566
3755
|
}
|
|
2567
3756
|
function performStatus3() {
|
|
2568
3757
|
const path = configPath();
|
|
2569
|
-
const hooks =
|
|
3758
|
+
const hooks = existsSync6(path) ? readHooks(readDoc(path)) : {};
|
|
2570
3759
|
return { path, gate: isGateInstalled3(hooks), capture: isCaptureInstalled(hooks) };
|
|
2571
3760
|
}
|
|
2572
3761
|
var TRUST_NOTICE2 = "[prim] Hermes requires hook consent: it prompts once per (event, command) on a TTY and records approval in ~/.hermes/shell-hooks-allowlist.json. To pre-approve non-interactively, re-run with --auto-accept (sets hooks_auto_accept: true), export HERMES_ACCEPT_HOOKS=1, or start Hermes with `hermes --accept-hooks chat`. Until approved, the hooks will not fire.";
|
|
@@ -2611,9 +3800,9 @@ function registerHermesCommands(program2) {
|
|
|
2611
3800
|
|
|
2612
3801
|
// src/commands/hooks.ts
|
|
2613
3802
|
import { execFileSync } from "child_process";
|
|
2614
|
-
import { existsSync as
|
|
2615
|
-
import { homedir as
|
|
2616
|
-
import { dirname as
|
|
3803
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
3804
|
+
import { homedir as homedir6 } from "os";
|
|
3805
|
+
import { dirname as dirname5, join as join6, resolve as resolve2 } from "path";
|
|
2617
3806
|
import { Option } from "commander";
|
|
2618
3807
|
var PRE_COMMIT = { hookName: "pre-commit", binName: "prim-pre-commit" };
|
|
2619
3808
|
var POST_COMMIT = { hookName: "post-commit", binName: "prim-post-commit" };
|
|
@@ -2662,16 +3851,16 @@ ${gatedShim(spec.binName)}
|
|
|
2662
3851
|
${end}`;
|
|
2663
3852
|
}
|
|
2664
3853
|
function mergePrimBlock(hookPath, block, binName) {
|
|
2665
|
-
if (
|
|
2666
|
-
const existing =
|
|
3854
|
+
if (existsSync7(hookPath)) {
|
|
3855
|
+
const existing = readFileSync6(hookPath, "utf-8");
|
|
2667
3856
|
if (containsPrimHook(existing, binName)) return false;
|
|
2668
3857
|
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
2669
|
-
|
|
3858
|
+
writeFileSync5(hookPath, `${existing}${separator}${block}
|
|
2670
3859
|
`, { mode: 493 });
|
|
2671
3860
|
return true;
|
|
2672
3861
|
}
|
|
2673
|
-
|
|
2674
|
-
|
|
3862
|
+
mkdirSync6(dirname5(hookPath), { recursive: true });
|
|
3863
|
+
writeFileSync5(hookPath, `#!/bin/sh
|
|
2675
3864
|
# ${PRIM_CREATED_MARK}
|
|
2676
3865
|
|
|
2677
3866
|
${block}
|
|
@@ -2686,14 +3875,14 @@ function getGitRoot() {
|
|
|
2686
3875
|
return root;
|
|
2687
3876
|
}
|
|
2688
3877
|
function detectHusky(gitRoot) {
|
|
2689
|
-
const huskyDir =
|
|
2690
|
-
if (!
|
|
2691
|
-
if (
|
|
2692
|
-
if (
|
|
2693
|
-
const pkgPath =
|
|
2694
|
-
if (
|
|
3878
|
+
const huskyDir = resolve2(gitRoot, ".husky");
|
|
3879
|
+
if (!existsSync7(huskyDir)) return false;
|
|
3880
|
+
if (existsSync7(resolve2(huskyDir, "_"))) return true;
|
|
3881
|
+
if (existsSync7(resolve2(huskyDir, "pre-commit"))) return true;
|
|
3882
|
+
const pkgPath = resolve2(gitRoot, "package.json");
|
|
3883
|
+
if (existsSync7(pkgPath)) {
|
|
2695
3884
|
try {
|
|
2696
|
-
const pkg2 = JSON.parse(
|
|
3885
|
+
const pkg2 = JSON.parse(readFileSync6(pkgPath, "utf-8"));
|
|
2697
3886
|
const scripts = pkg2.scripts ?? {};
|
|
2698
3887
|
if (/husky/i.test(scripts.prepare ?? "") || /husky/i.test(scripts.postinstall ?? "")) {
|
|
2699
3888
|
return true;
|
|
@@ -2708,8 +3897,8 @@ function containsPrimHook(content, binName = PRE_COMMIT.binName) {
|
|
|
2708
3897
|
}
|
|
2709
3898
|
async function askConfirmation(question) {
|
|
2710
3899
|
if (!process.stdin.isTTY) return false;
|
|
2711
|
-
const { createInterface } = await import("readline/promises");
|
|
2712
|
-
const rl =
|
|
3900
|
+
const { createInterface: createInterface2 } = await import("readline/promises");
|
|
3901
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
2713
3902
|
try {
|
|
2714
3903
|
const answer = await rl.question(`${question} [y/N] `);
|
|
2715
3904
|
const normalized = answer.trim().toLowerCase();
|
|
@@ -2719,8 +3908,8 @@ async function askConfirmation(question) {
|
|
|
2719
3908
|
}
|
|
2720
3909
|
}
|
|
2721
3910
|
function installToHusky(gitRoot, spec = PRE_COMMIT) {
|
|
2722
|
-
const hookPath =
|
|
2723
|
-
const existed =
|
|
3911
|
+
const hookPath = resolve2(gitRoot, ".husky", spec.hookName);
|
|
3912
|
+
const existed = existsSync7(hookPath);
|
|
2724
3913
|
const wrote = mergePrimBlock(hookPath, huskyBlock(spec), spec.binName);
|
|
2725
3914
|
if (!wrote) {
|
|
2726
3915
|
console.log(`Prim ${spec.hookName} hook is already installed in .husky/${spec.hookName}.`);
|
|
@@ -2731,13 +3920,13 @@ function installToHusky(gitRoot, spec = PRE_COMMIT) {
|
|
|
2731
3920
|
}
|
|
2732
3921
|
}
|
|
2733
3922
|
function installToDotGit(gitRoot, spec = PRE_COMMIT) {
|
|
2734
|
-
const hooksDir =
|
|
2735
|
-
const hookPath =
|
|
2736
|
-
if (!
|
|
2737
|
-
|
|
3923
|
+
const hooksDir = resolve2(gitRoot, ".git", "hooks");
|
|
3924
|
+
const hookPath = resolve2(hooksDir, spec.hookName);
|
|
3925
|
+
if (!existsSync7(hooksDir)) {
|
|
3926
|
+
mkdirSync6(hooksDir, { recursive: true });
|
|
2738
3927
|
}
|
|
2739
|
-
if (
|
|
2740
|
-
const existing =
|
|
3928
|
+
if (existsSync7(hookPath)) {
|
|
3929
|
+
const existing = readFileSync6(hookPath, "utf-8");
|
|
2741
3930
|
if (containsPrimHook(existing, spec.binName)) {
|
|
2742
3931
|
console.log(`Prim ${spec.hookName} hook is already installed at ${hookPath}.`);
|
|
2743
3932
|
return;
|
|
@@ -2746,12 +3935,12 @@ function installToDotGit(gitRoot, spec = PRE_COMMIT) {
|
|
|
2746
3935
|
console.log("To replace it, run: prim hooks uninstall && prim hooks install");
|
|
2747
3936
|
return;
|
|
2748
3937
|
}
|
|
2749
|
-
|
|
3938
|
+
writeFileSync5(hookPath, dotGitScript(spec), { mode: 493 });
|
|
2750
3939
|
console.log(`Installed ${spec.hookName} hook at ${hookPath}`);
|
|
2751
3940
|
}
|
|
2752
|
-
var PRIM_GIT_HOOKS_DIR =
|
|
3941
|
+
var PRIM_GIT_HOOKS_DIR = join6(homedir6(), ".config", "prim", "git-hooks");
|
|
2753
3942
|
function expandTilde(p) {
|
|
2754
|
-
return p.startsWith("~/") ?
|
|
3943
|
+
return p.startsWith("~/") ? join6(homedir6(), p.slice(2)) : p;
|
|
2755
3944
|
}
|
|
2756
3945
|
function isOurHooksDir(value) {
|
|
2757
3946
|
return value !== "" && expandTilde(value) === PRIM_GIT_HOOKS_DIR;
|
|
@@ -2816,24 +4005,24 @@ function ownedHookNames() {
|
|
|
2816
4005
|
return [...HOOKS.map((s) => s.hookName), ...PASSTHROUGH_HOOKS];
|
|
2817
4006
|
}
|
|
2818
4007
|
function writeOwnHooks() {
|
|
2819
|
-
if (!
|
|
2820
|
-
|
|
4008
|
+
if (!existsSync7(PRIM_GIT_HOOKS_DIR)) {
|
|
4009
|
+
mkdirSync6(PRIM_GIT_HOOKS_DIR, { recursive: true });
|
|
2821
4010
|
}
|
|
2822
4011
|
for (const spec of HOOKS) {
|
|
2823
|
-
|
|
4012
|
+
writeFileSync5(resolve2(PRIM_GIT_HOOKS_DIR, spec.hookName), globalHookScript(spec), {
|
|
2824
4013
|
mode: 493
|
|
2825
4014
|
});
|
|
2826
4015
|
}
|
|
2827
4016
|
for (const name of PASSTHROUGH_HOOKS) {
|
|
2828
|
-
|
|
4017
|
+
writeFileSync5(resolve2(PRIM_GIT_HOOKS_DIR, name), passThroughScript(name), { mode: 493 });
|
|
2829
4018
|
}
|
|
2830
4019
|
}
|
|
2831
4020
|
function appendPrimBlock(hookPath, spec) {
|
|
2832
4021
|
mergePrimBlock(hookPath, gatedBlock(spec), spec.binName);
|
|
2833
4022
|
}
|
|
2834
4023
|
function stripPrimBlock(hookPath, spec) {
|
|
2835
|
-
if (!
|
|
2836
|
-
const existing =
|
|
4024
|
+
if (!existsSync7(hookPath)) return;
|
|
4025
|
+
const existing = readFileSync6(hookPath, "utf-8");
|
|
2837
4026
|
const primCreated = existing.includes(PRIM_CREATED_MARK);
|
|
2838
4027
|
const { start, end } = blockMarkers(spec);
|
|
2839
4028
|
const s = existing.indexOf(start);
|
|
@@ -2845,7 +4034,7 @@ function stripPrimBlock(hookPath, spec) {
|
|
|
2845
4034
|
unlinkSync2(hookPath);
|
|
2846
4035
|
return;
|
|
2847
4036
|
}
|
|
2848
|
-
|
|
4037
|
+
writeFileSync5(hookPath, out, { mode: 493 });
|
|
2849
4038
|
}
|
|
2850
4039
|
function installGlobalHooks(opts = {}) {
|
|
2851
4040
|
const global = gitConfigGet("--global");
|
|
@@ -2876,7 +4065,7 @@ function installGlobalHooks(opts = {}) {
|
|
|
2876
4065
|
}
|
|
2877
4066
|
const dir = expandTilde(global);
|
|
2878
4067
|
for (const spec of HOOKS) {
|
|
2879
|
-
appendPrimBlock(
|
|
4068
|
+
appendPrimBlock(resolve2(dir, spec.hookName), spec);
|
|
2880
4069
|
}
|
|
2881
4070
|
console.log(
|
|
2882
4071
|
`Appended prim hooks into existing core.hooksPath dir ${global} (pointer unchanged).`
|
|
@@ -2887,8 +4076,8 @@ function uninstallGlobalHooks() {
|
|
|
2887
4076
|
const global = gitConfigGet("--global");
|
|
2888
4077
|
if (isOurHooksDir(global)) {
|
|
2889
4078
|
for (const name of ownedHookNames()) {
|
|
2890
|
-
const p =
|
|
2891
|
-
if (
|
|
4079
|
+
const p = resolve2(PRIM_GIT_HOOKS_DIR, name);
|
|
4080
|
+
if (existsSync7(p)) unlinkSync2(p);
|
|
2892
4081
|
}
|
|
2893
4082
|
execFileSync("git", ["config", "--global", "--unset", "core.hooksPath"]);
|
|
2894
4083
|
console.log("Removed prim global git hooks and unset core.hooksPath.");
|
|
@@ -2897,7 +4086,7 @@ function uninstallGlobalHooks() {
|
|
|
2897
4086
|
if (global !== "") {
|
|
2898
4087
|
const dir = expandTilde(global);
|
|
2899
4088
|
for (const spec of HOOKS) {
|
|
2900
|
-
stripPrimBlock(
|
|
4089
|
+
stripPrimBlock(resolve2(dir, spec.hookName), spec);
|
|
2901
4090
|
}
|
|
2902
4091
|
console.log(`Removed the prim block from ${global} (left the dir and core.hooksPath intact).`);
|
|
2903
4092
|
return;
|
|
@@ -2977,12 +4166,12 @@ function registerHooksCommands(program2) {
|
|
|
2977
4166
|
}
|
|
2978
4167
|
const gitRoot = getGitRoot();
|
|
2979
4168
|
for (const spec of HOOKS) {
|
|
2980
|
-
const hookPath =
|
|
2981
|
-
if (!
|
|
4169
|
+
const hookPath = resolve2(gitRoot, ".git", "hooks", spec.hookName);
|
|
4170
|
+
if (!existsSync7(hookPath)) {
|
|
2982
4171
|
console.log(`No ${spec.hookName} hook found.`);
|
|
2983
4172
|
continue;
|
|
2984
4173
|
}
|
|
2985
|
-
if (containsPrimHook(
|
|
4174
|
+
if (containsPrimHook(readFileSync6(hookPath, "utf-8"), spec.binName)) {
|
|
2986
4175
|
unlinkSync2(hookPath);
|
|
2987
4176
|
console.log(`Removed ${spec.hookName} hook at ${hookPath}`);
|
|
2988
4177
|
} else {
|
|
@@ -2993,43 +4182,60 @@ function registerHooksCommands(program2) {
|
|
|
2993
4182
|
}
|
|
2994
4183
|
|
|
2995
4184
|
// src/commands/moves.ts
|
|
2996
|
-
import { existsSync as
|
|
2997
|
-
import { join as
|
|
4185
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync7, unlinkSync as unlinkSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
4186
|
+
import { join as join7 } from "path";
|
|
2998
4187
|
|
|
2999
4188
|
// src/flusher.ts
|
|
3000
|
-
import { renameSync as
|
|
4189
|
+
import { createReadStream, renameSync as renameSync4, unlinkSync as unlinkSync3 } from "fs";
|
|
4190
|
+
import { createInterface } from "readline";
|
|
3001
4191
|
var BATCH_SIZE = 500;
|
|
3002
4192
|
var HTTP_TIMEOUT_MS = 1e4;
|
|
3003
4193
|
var OPPORTUNISTIC_FLUSH_AFTER_MS = 6e4;
|
|
3004
4194
|
var ORPHAN_QUARANTINE_MS = 6e4;
|
|
3005
|
-
function
|
|
3006
|
-
const
|
|
3007
|
-
|
|
3008
|
-
batches.push(moves.slice(i, i + size));
|
|
3009
|
-
}
|
|
3010
|
-
return batches;
|
|
3011
|
-
}
|
|
3012
|
-
async function drainFlushingPath(flushingPath) {
|
|
3013
|
-
const moves = readMovesFromPath(flushingPath);
|
|
3014
|
-
if (moves.length === 0) {
|
|
3015
|
-
unlinkSync3(flushingPath);
|
|
3016
|
-
return 0;
|
|
3017
|
-
}
|
|
3018
|
-
const client = getClient();
|
|
3019
|
-
for (const batch of batchMoves(moves)) {
|
|
3020
|
-
await client.post(
|
|
4195
|
+
async function drainFlushingPath(flushingPath, client = getClient()) {
|
|
4196
|
+
const postBatch = async (batch2) => {
|
|
4197
|
+
const response = await client.post(
|
|
3021
4198
|
"/api/cli/moves/ingest",
|
|
3022
|
-
{ batch },
|
|
4199
|
+
{ batch: batch2 },
|
|
3023
4200
|
{ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS) }
|
|
3024
4201
|
);
|
|
4202
|
+
requireDurableIngestAcknowledgement(response, batch2.length);
|
|
4203
|
+
};
|
|
4204
|
+
const input = createReadStream(flushingPath, { encoding: "utf-8" });
|
|
4205
|
+
const lines = createInterface({ input, crlfDelay: Number.POSITIVE_INFINITY });
|
|
4206
|
+
let batch = [];
|
|
4207
|
+
let total = 0;
|
|
4208
|
+
try {
|
|
4209
|
+
for await (const line of lines) {
|
|
4210
|
+
if (line.length === 0) {
|
|
4211
|
+
continue;
|
|
4212
|
+
}
|
|
4213
|
+
try {
|
|
4214
|
+
batch.push(JSON.parse(line));
|
|
4215
|
+
} catch {
|
|
4216
|
+
continue;
|
|
4217
|
+
}
|
|
4218
|
+
if (batch.length === BATCH_SIZE) {
|
|
4219
|
+
await postBatch(batch);
|
|
4220
|
+
total += batch.length;
|
|
4221
|
+
batch = [];
|
|
4222
|
+
}
|
|
4223
|
+
}
|
|
4224
|
+
if (batch.length > 0) {
|
|
4225
|
+
await postBatch(batch);
|
|
4226
|
+
total += batch.length;
|
|
4227
|
+
}
|
|
4228
|
+
} finally {
|
|
4229
|
+
lines.close();
|
|
4230
|
+
input.destroy();
|
|
3025
4231
|
}
|
|
3026
4232
|
unlinkSync3(flushingPath);
|
|
3027
|
-
return
|
|
4233
|
+
return total;
|
|
3028
4234
|
}
|
|
3029
4235
|
async function drainPath(path) {
|
|
3030
4236
|
const tmpPath = `${path}.flushing.${String(Date.now())}.${String(process.pid)}`;
|
|
3031
4237
|
try {
|
|
3032
|
-
|
|
4238
|
+
renameSync4(path, tmpPath);
|
|
3033
4239
|
} catch (err) {
|
|
3034
4240
|
if (err.code === "ENOENT") {
|
|
3035
4241
|
return 0;
|
|
@@ -3038,7 +4244,7 @@ async function drainPath(path) {
|
|
|
3038
4244
|
}
|
|
3039
4245
|
return drainFlushingPath(tmpPath);
|
|
3040
4246
|
}
|
|
3041
|
-
function
|
|
4247
|
+
function processIsAlive3(pid) {
|
|
3042
4248
|
try {
|
|
3043
4249
|
process.kill(pid, 0);
|
|
3044
4250
|
return true;
|
|
@@ -3048,36 +4254,84 @@ function processIsAlive2(pid) {
|
|
|
3048
4254
|
}
|
|
3049
4255
|
function selectRecoverable(files, now, opts = {}) {
|
|
3050
4256
|
const quarantineMs = opts.quarantineMs ?? ORPHAN_QUARANTINE_MS;
|
|
3051
|
-
const isAlive = opts.isAlive ??
|
|
3052
|
-
return files.filter(
|
|
3053
|
-
(f
|
|
3054
|
-
|
|
4257
|
+
const isAlive = opts.isAlive ?? processIsAlive3;
|
|
4258
|
+
return files.filter((f) => {
|
|
4259
|
+
if (f.pid === void 0) {
|
|
4260
|
+
return now - f.mtimeMs > quarantineMs;
|
|
4261
|
+
}
|
|
4262
|
+
return f.pid === opts.ownerPid || !isAlive(f.pid);
|
|
4263
|
+
});
|
|
3055
4264
|
}
|
|
3056
|
-
async function recoverOrphans() {
|
|
3057
|
-
|
|
3058
|
-
|
|
4265
|
+
async function recoverOrphans(candidates = listFlushing({ sampleBytes: 0 }), options = {}) {
|
|
4266
|
+
const summary = { flushed: 0, errors: [], failedBuckets: /* @__PURE__ */ new Set() };
|
|
4267
|
+
const recoverable = selectRecoverable(candidates, options.now ?? Date.now(), {
|
|
4268
|
+
ownerPid: options.ownerPid ?? process.pid,
|
|
4269
|
+
isAlive: options.isAlive
|
|
4270
|
+
}).sort((a, b) => a.mtimeMs - b.mtimeMs || a.path.localeCompare(b.path));
|
|
4271
|
+
const drain = options.drain ?? drainFlushingPath;
|
|
4272
|
+
for (const file of recoverable) {
|
|
4273
|
+
if (summary.failedBuckets.has(file.bucket)) {
|
|
4274
|
+
continue;
|
|
4275
|
+
}
|
|
3059
4276
|
try {
|
|
3060
|
-
|
|
3061
|
-
} catch {
|
|
4277
|
+
summary.flushed += await drain(file.path);
|
|
4278
|
+
} catch (err) {
|
|
4279
|
+
summary.errors.push(err);
|
|
4280
|
+
summary.failedBuckets.add(file.bucket);
|
|
3062
4281
|
}
|
|
3063
4282
|
}
|
|
3064
|
-
return
|
|
4283
|
+
return summary;
|
|
3065
4284
|
}
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
4285
|
+
var FlushError = class extends Error {
|
|
4286
|
+
flushed;
|
|
4287
|
+
constructor(cause, flushed) {
|
|
4288
|
+
super(cause instanceof Error ? cause.message : String(cause), { cause });
|
|
4289
|
+
this.name = "FlushError";
|
|
4290
|
+
this.flushed = flushed;
|
|
4291
|
+
}
|
|
4292
|
+
};
|
|
4293
|
+
async function flushOnce() {
|
|
4294
|
+
const recovered = await recoverOrphans();
|
|
4295
|
+
let total = recovered.flushed;
|
|
4296
|
+
const errors = recovered.errors;
|
|
4297
|
+
for (const { bucket, path } of listBuckets()) {
|
|
4298
|
+
if (recovered.failedBuckets.has(bucket)) {
|
|
4299
|
+
continue;
|
|
4300
|
+
}
|
|
4301
|
+
try {
|
|
4302
|
+
total += await drainPath(path);
|
|
4303
|
+
} catch (err) {
|
|
4304
|
+
errors.push(err);
|
|
4305
|
+
}
|
|
4306
|
+
}
|
|
4307
|
+
if (errors.length > 0) {
|
|
4308
|
+
throw new FlushError(errors[0], total);
|
|
3070
4309
|
}
|
|
3071
4310
|
return { flushed: total };
|
|
3072
4311
|
}
|
|
4312
|
+
var flushInFlight;
|
|
4313
|
+
function flush() {
|
|
4314
|
+
if (flushInFlight) {
|
|
4315
|
+
return flushInFlight;
|
|
4316
|
+
}
|
|
4317
|
+
const attempt = flushOnce().finally(() => {
|
|
4318
|
+
if (flushInFlight === attempt) {
|
|
4319
|
+
flushInFlight = void 0;
|
|
4320
|
+
}
|
|
4321
|
+
});
|
|
4322
|
+
flushInFlight = attempt;
|
|
4323
|
+
return attempt;
|
|
4324
|
+
}
|
|
4325
|
+
function shouldFlushPending(stats, now, thresholdMs = OPPORTUNISTIC_FLUSH_AFTER_MS) {
|
|
4326
|
+
if (stats.sampled) {
|
|
4327
|
+
return true;
|
|
4328
|
+
}
|
|
4329
|
+
return stats.pendingCount > 0 && (stats.oldestPendingAt === void 0 || now - stats.oldestPendingAt > thresholdMs);
|
|
4330
|
+
}
|
|
3073
4331
|
async function flushIfNeeded() {
|
|
3074
4332
|
try {
|
|
3075
|
-
const stats =
|
|
3076
|
-
if (stats.
|
|
3077
|
-
return;
|
|
3078
|
-
}
|
|
3079
|
-
const oldest = stats.reduce((min, s) => s.mtimeMs < min ? s.mtimeMs : min, stats[0].mtimeMs);
|
|
3080
|
-
if (Date.now() - oldest > OPPORTUNISTIC_FLUSH_AFTER_MS) {
|
|
4333
|
+
const stats = pendingJournalStats();
|
|
4334
|
+
if (shouldFlushPending(stats, Date.now())) {
|
|
3081
4335
|
await flush();
|
|
3082
4336
|
}
|
|
3083
4337
|
} catch {
|
|
@@ -3111,21 +4365,25 @@ function registerMovesCommands(program2) {
|
|
|
3111
4365
|
console.log(`[prim] root: ${JOURNAL_DIR}`);
|
|
3112
4366
|
for (const s of stats) {
|
|
3113
4367
|
const ageS = Math.round((Date.now() - s.mtimeMs) / MS_PER_SECOND2);
|
|
4368
|
+
const count = `${String(s.lineCount)}${s.sampled ? "+" : ""}`;
|
|
3114
4369
|
console.log(
|
|
3115
|
-
` ${s.bucket.padEnd(BUCKET_COL_WIDTH)} ${
|
|
4370
|
+
` ${s.bucket.padEnd(BUCKET_COL_WIDTH)} ${count.padStart(5)} pending, ${String(s.sizeBytes).padStart(8)} bytes, last write ${String(ageS)}s ago`
|
|
3116
4371
|
);
|
|
3117
4372
|
}
|
|
3118
4373
|
if (stranded.length > 0) {
|
|
3119
4374
|
const moveCount = stranded.reduce((n, f) => n + f.lineCount, 0);
|
|
3120
4375
|
const byteCount = stranded.reduce((n, f) => n + f.sizeBytes, 0);
|
|
4376
|
+
const sampled = stranded.some((file) => file.sampled);
|
|
4377
|
+
const qualifier = sampled ? "at least " : "";
|
|
3121
4378
|
console.log(
|
|
3122
|
-
`[prim] \u26A0 ${String(stranded.length)} stranded flush file(s): ${String(moveCount)} move(s), ${String(byteCount)} bytes \u2014 recover with \`prim moves flush\``
|
|
4379
|
+
`[prim] \u26A0 ${String(stranded.length)} stranded flush file(s): ${qualifier}${String(moveCount)} move(s), ${String(byteCount)} bytes \u2014 recover with \`prim moves flush\``
|
|
3123
4380
|
);
|
|
3124
4381
|
for (const f of stranded) {
|
|
3125
4382
|
const ageS = Math.round((Date.now() - f.mtimeMs) / MS_PER_SECOND2);
|
|
3126
4383
|
const owner = f.pid === void 0 ? "no pid" : `pid ${String(f.pid)}`;
|
|
4384
|
+
const count = `${String(f.lineCount)}${f.sampled ? "+" : ""}`;
|
|
3127
4385
|
console.log(
|
|
3128
|
-
` ${f.bucket.padEnd(BUCKET_COL_WIDTH)} ${
|
|
4386
|
+
` ${f.bucket.padEnd(BUCKET_COL_WIDTH)} ${count.padStart(5)} stranded, ${String(f.sizeBytes).padStart(8)} bytes, ${String(ageS)}s ago (${owner})`
|
|
3129
4387
|
);
|
|
3130
4388
|
}
|
|
3131
4389
|
}
|
|
@@ -3153,19 +4411,19 @@ function registerMovesCommands(program2) {
|
|
|
3153
4411
|
}
|
|
3154
4412
|
});
|
|
3155
4413
|
moves.command("bind").description("Pin the current directory to an org via .prim/workspace.json").requiredOption("--orgId <orgId>", "Convex organization id").action((opts) => {
|
|
3156
|
-
const dir =
|
|
3157
|
-
if (!
|
|
3158
|
-
|
|
4414
|
+
const dir = join7(process.cwd(), ".prim");
|
|
4415
|
+
if (!existsSync8(dir)) {
|
|
4416
|
+
mkdirSync7(dir, { recursive: true, mode: DIR_MODE });
|
|
3159
4417
|
}
|
|
3160
|
-
const file =
|
|
3161
|
-
|
|
4418
|
+
const file = join7(process.cwd(), WORKSPACE_FILE);
|
|
4419
|
+
writeFileSync6(file, JSON.stringify({ orgId: opts.orgId, boundAt: Date.now() }, null, 2), {
|
|
3162
4420
|
mode: FILE_MODE2
|
|
3163
4421
|
});
|
|
3164
4422
|
console.log(`[prim] bound ${process.cwd()} to org ${opts.orgId}`);
|
|
3165
4423
|
});
|
|
3166
4424
|
moves.command("drop").description("Remove the .prim/workspace.json binding from the cwd").action(() => {
|
|
3167
|
-
const file =
|
|
3168
|
-
if (!
|
|
4425
|
+
const file = join7(process.cwd(), WORKSPACE_FILE);
|
|
4426
|
+
if (!existsSync8(file)) {
|
|
3169
4427
|
console.log("[prim] no workspace binding in cwd");
|
|
3170
4428
|
return;
|
|
3171
4429
|
}
|
|
@@ -3258,23 +4516,23 @@ function registerReconcileCommands(program2) {
|
|
|
3258
4516
|
|
|
3259
4517
|
// src/commands/session.ts
|
|
3260
4518
|
import {
|
|
3261
|
-
existsSync as
|
|
3262
|
-
mkdirSync as
|
|
3263
|
-
readFileSync as
|
|
4519
|
+
existsSync as existsSync9,
|
|
4520
|
+
mkdirSync as mkdirSync8,
|
|
4521
|
+
readFileSync as readFileSync7,
|
|
3264
4522
|
readdirSync,
|
|
3265
4523
|
unlinkSync as unlinkSync5,
|
|
3266
|
-
writeFileSync as
|
|
4524
|
+
writeFileSync as writeFileSync7
|
|
3267
4525
|
} from "fs";
|
|
3268
|
-
import { join as
|
|
4526
|
+
import { join as join8 } from "path";
|
|
3269
4527
|
var DIR_MODE2 = 448;
|
|
3270
4528
|
var FILE_MODE3 = 384;
|
|
3271
4529
|
function ensureDir() {
|
|
3272
|
-
if (!
|
|
3273
|
-
|
|
4530
|
+
if (!existsSync9(SESSIONS_DIR)) {
|
|
4531
|
+
mkdirSync8(SESSIONS_DIR, { recursive: true, mode: DIR_MODE2 });
|
|
3274
4532
|
}
|
|
3275
4533
|
}
|
|
3276
4534
|
function markerPath(sessionId) {
|
|
3277
|
-
return
|
|
4535
|
+
return join8(SESSIONS_DIR, `${sessionId}.json`);
|
|
3278
4536
|
}
|
|
3279
4537
|
function registerSessionCommands(program2) {
|
|
3280
4538
|
const session = program2.command("session").description("Decision Event Pipeline \u2014 session binding markers");
|
|
@@ -3284,13 +4542,13 @@ function registerSessionCommands(program2) {
|
|
|
3284
4542
|
orgId: opts.orgId,
|
|
3285
4543
|
startedAt: Date.now()
|
|
3286
4544
|
};
|
|
3287
|
-
|
|
4545
|
+
writeFileSync7(markerPath(sessionId), JSON.stringify(marker, null, 2), {
|
|
3288
4546
|
mode: FILE_MODE3
|
|
3289
4547
|
});
|
|
3290
4548
|
console.log(`[prim] session ${sessionId} bound to org ${opts.orgId}`);
|
|
3291
4549
|
});
|
|
3292
4550
|
session.command("list").description("List active session markers").action(() => {
|
|
3293
|
-
if (!
|
|
4551
|
+
if (!existsSync9(SESSIONS_DIR)) {
|
|
3294
4552
|
console.log("[prim] no session markers");
|
|
3295
4553
|
return;
|
|
3296
4554
|
}
|
|
@@ -3302,7 +4560,7 @@ function registerSessionCommands(program2) {
|
|
|
3302
4560
|
for (const f of files) {
|
|
3303
4561
|
const sessionId = f.replace(/\.json$/, "");
|
|
3304
4562
|
try {
|
|
3305
|
-
const m = JSON.parse(
|
|
4563
|
+
const m = JSON.parse(readFileSync7(join8(SESSIONS_DIR, f), "utf-8"));
|
|
3306
4564
|
console.log(`${sessionId} org=${m.orgId}`);
|
|
3307
4565
|
} catch {
|
|
3308
4566
|
}
|
|
@@ -3310,7 +4568,7 @@ function registerSessionCommands(program2) {
|
|
|
3310
4568
|
});
|
|
3311
4569
|
session.command("drop <sessionId>").description("Remove a session marker").action((sessionId) => {
|
|
3312
4570
|
const p = markerPath(sessionId);
|
|
3313
|
-
if (!
|
|
4571
|
+
if (!existsSync9(p)) {
|
|
3314
4572
|
console.log(`[prim] no marker for session ${sessionId}`);
|
|
3315
4573
|
return;
|
|
3316
4574
|
}
|
|
@@ -3320,9 +4578,9 @@ function registerSessionCommands(program2) {
|
|
|
3320
4578
|
}
|
|
3321
4579
|
|
|
3322
4580
|
// src/commands/setup.ts
|
|
3323
|
-
import { spawnSync } from "child_process";
|
|
3324
|
-
import { existsSync as
|
|
3325
|
-
import { join as
|
|
4581
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
4582
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
|
|
4583
|
+
import { join as join9 } from "path";
|
|
3326
4584
|
var EXIT_INCOMPLETE = 1;
|
|
3327
4585
|
var EXIT_USAGE3 = 2;
|
|
3328
4586
|
var SESSION_LABELS = {
|
|
@@ -3346,7 +4604,20 @@ function planSetupSteps(opts) {
|
|
|
3346
4604
|
key: "daemon",
|
|
3347
4605
|
label: "Companion daemon",
|
|
3348
4606
|
args: ["daemon", "start"],
|
|
3349
|
-
required:
|
|
4607
|
+
required: true
|
|
4608
|
+
});
|
|
4609
|
+
steps.push({
|
|
4610
|
+
key: "health",
|
|
4611
|
+
label: "Capture health",
|
|
4612
|
+
args: ["doctor"],
|
|
4613
|
+
required: true
|
|
4614
|
+
});
|
|
4615
|
+
} else {
|
|
4616
|
+
steps.push({
|
|
4617
|
+
key: "daemon-opt-out",
|
|
4618
|
+
label: "Daemon opt-out",
|
|
4619
|
+
args: ["daemon", "stop"],
|
|
4620
|
+
required: true
|
|
3350
4621
|
});
|
|
3351
4622
|
}
|
|
3352
4623
|
steps.push({
|
|
@@ -3385,8 +4656,8 @@ function detectProjectConflicts(agent, run) {
|
|
|
3385
4656
|
}
|
|
3386
4657
|
try {
|
|
3387
4658
|
const root = gitToplevel();
|
|
3388
|
-
const preCommit = root &&
|
|
3389
|
-
if (preCommit &&
|
|
4659
|
+
const preCommit = root && join9(root, ".git", "hooks", "pre-commit");
|
|
4660
|
+
if (preCommit && existsSync10(preCommit) && readFileSync8(preCommit, "utf-8").includes("prim-pre-commit")) {
|
|
3390
4661
|
conflicts.push(CONFLICT_HOOKS);
|
|
3391
4662
|
}
|
|
3392
4663
|
} catch {
|
|
@@ -3422,7 +4693,7 @@ function registerSetupCommand(program2) {
|
|
|
3422
4693
|
).option(
|
|
3423
4694
|
"--migrate",
|
|
3424
4695
|
"with the default user scope, remove any project-scoped prim config in this repo (else just warn)"
|
|
3425
|
-
).option("--no-daemon", "
|
|
4696
|
+
).option("--no-daemon", "stop and disable the companion daemon").action((opts) => {
|
|
3426
4697
|
const { agent: agentInput, detected } = resolveAgent(opts.agent, process.env);
|
|
3427
4698
|
if (agentInput !== "claude" && agentInput !== "codex" && agentInput !== "hermes") {
|
|
3428
4699
|
process.stderr.write(
|
|
@@ -3440,7 +4711,7 @@ function registerSetupCommand(program2) {
|
|
|
3440
4711
|
const scope = opts.scope;
|
|
3441
4712
|
const self = process.argv[1];
|
|
3442
4713
|
const run = (args, capture = false) => {
|
|
3443
|
-
const r =
|
|
4714
|
+
const r = spawnSync2(process.execPath, [self, ...args], {
|
|
3444
4715
|
stdio: capture ? ["inherit", "pipe", "ignore"] : "inherit",
|
|
3445
4716
|
encoding: "utf-8"
|
|
3446
4717
|
});
|
|
@@ -3504,39 +4775,39 @@ function registerSetupCommand(program2) {
|
|
|
3504
4775
|
// src/commands/skill.ts
|
|
3505
4776
|
import {
|
|
3506
4777
|
closeSync as closeSync4,
|
|
3507
|
-
existsSync as
|
|
4778
|
+
existsSync as existsSync12,
|
|
3508
4779
|
fsyncSync as fsyncSync3,
|
|
3509
4780
|
openSync as openSync4,
|
|
3510
|
-
readFileSync as
|
|
3511
|
-
renameSync as
|
|
3512
|
-
writeFileSync as
|
|
4781
|
+
readFileSync as readFileSync10,
|
|
4782
|
+
renameSync as renameSync5,
|
|
4783
|
+
writeFileSync as writeFileSync8
|
|
3513
4784
|
} from "fs";
|
|
3514
|
-
import { homedir as
|
|
3515
|
-
import { dirname as
|
|
4785
|
+
import { homedir as homedir8 } from "os";
|
|
4786
|
+
import { dirname as dirname7, join as join11, resolve as resolve4 } from "path";
|
|
3516
4787
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3517
4788
|
import { createPatch } from "diff";
|
|
3518
4789
|
|
|
3519
4790
|
// src/commands/claude-plugin.ts
|
|
3520
|
-
import { existsSync as
|
|
3521
|
-
import { homedir as
|
|
3522
|
-
import { dirname as
|
|
4791
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync9, readFileSync as readFileSync9, readdirSync as readdirSync2, rmSync as rmSync4, rmdirSync } from "fs";
|
|
4792
|
+
import { homedir as homedir7 } from "os";
|
|
4793
|
+
import { dirname as dirname6, join as join10, resolve as resolve3 } from "path";
|
|
3523
4794
|
import { fileURLToPath } from "url";
|
|
3524
|
-
var __dirname =
|
|
4795
|
+
var __dirname = dirname6(fileURLToPath(import.meta.url));
|
|
3525
4796
|
var PLUGIN_DESCRIPTION = "Primitive decision-graph guidance for the prim CLI \u2014 a model-invoked skill installed by prim skill install.";
|
|
3526
4797
|
function resolvePluginDir(cwd, scope) {
|
|
3527
4798
|
if (scope && scope !== "user" && scope !== "project") {
|
|
3528
4799
|
console.error(`Unknown --scope "${scope}" (expected user or project)`);
|
|
3529
4800
|
return null;
|
|
3530
4801
|
}
|
|
3531
|
-
const base = scope === "user" ?
|
|
3532
|
-
return
|
|
4802
|
+
const base = scope === "user" ? join10(homedir7(), ".claude") : join10(gitToplevel(cwd) ?? cwd, ".claude");
|
|
4803
|
+
return join10(base, "skills", "prim");
|
|
3533
4804
|
}
|
|
3534
4805
|
function packageVersion() {
|
|
3535
4806
|
let dir = __dirname;
|
|
3536
|
-
while (dir !==
|
|
3537
|
-
const p =
|
|
3538
|
-
if (
|
|
3539
|
-
dir =
|
|
4807
|
+
while (dir !== dirname6(dir)) {
|
|
4808
|
+
const p = resolve3(dir, "package.json");
|
|
4809
|
+
if (existsSync11(p)) return JSON.parse(readFileSync9(p, "utf-8")).version;
|
|
4810
|
+
dir = dirname6(dir);
|
|
3540
4811
|
}
|
|
3541
4812
|
return "0.0.0";
|
|
3542
4813
|
}
|
|
@@ -3546,12 +4817,12 @@ function pluginManifest() {
|
|
|
3546
4817
|
`;
|
|
3547
4818
|
}
|
|
3548
4819
|
function removeDirIfEmpty(dir) {
|
|
3549
|
-
if (
|
|
4820
|
+
if (existsSync11(dir) && readdirSync2(dir).length === 0) rmdirSync(dir);
|
|
3550
4821
|
}
|
|
3551
4822
|
function pluginPaths(dir) {
|
|
3552
4823
|
return {
|
|
3553
|
-
manifestPath:
|
|
3554
|
-
skillPath:
|
|
4824
|
+
manifestPath: join10(dir, ".claude-plugin", "plugin.json"),
|
|
4825
|
+
skillPath: join10(dir, "SKILL.md")
|
|
3555
4826
|
};
|
|
3556
4827
|
}
|
|
3557
4828
|
function installClaudePlugin(cwd, opts) {
|
|
@@ -3560,8 +4831,8 @@ function installClaudePlugin(cwd, opts) {
|
|
|
3560
4831
|
const { manifestPath, skillPath } = pluginPaths(dir);
|
|
3561
4832
|
const manifest = pluginManifest();
|
|
3562
4833
|
const skill = loadSkill();
|
|
3563
|
-
const manifestCurrent =
|
|
3564
|
-
const skillCurrent =
|
|
4834
|
+
const manifestCurrent = existsSync11(manifestPath) ? readFileSync9(manifestPath, "utf-8") : null;
|
|
4835
|
+
const skillCurrent = existsSync11(skillPath) ? readFileSync9(skillPath, "utf-8") : null;
|
|
3565
4836
|
if (manifestCurrent === manifest && skillCurrent === skill) {
|
|
3566
4837
|
console.log("No changes \u2014 prim skill plugin already up to date.");
|
|
3567
4838
|
return 0;
|
|
@@ -3570,9 +4841,9 @@ function installClaudePlugin(cwd, opts) {
|
|
|
3570
4841
|
console.log(`Would write plugin to ${dir} (.claude-plugin/plugin.json + SKILL.md)`);
|
|
3571
4842
|
return 0;
|
|
3572
4843
|
}
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
4844
|
+
mkdirSync9(join10(dir, ".claude-plugin"), { recursive: true });
|
|
4845
|
+
atomicWrite3(manifestPath, manifest);
|
|
4846
|
+
atomicWrite3(skillPath, skill);
|
|
3576
4847
|
console.log(`Installed prim skill plugin at ${dir}`);
|
|
3577
4848
|
console.log("Restart Claude Code or run /reload-plugins to load it.");
|
|
3578
4849
|
if (opts.scope !== "user") {
|
|
@@ -3584,13 +4855,13 @@ function uninstallClaudePlugin(cwd, opts) {
|
|
|
3584
4855
|
const dir = resolvePluginDir(cwd, opts.scope);
|
|
3585
4856
|
if (dir === null) return 1;
|
|
3586
4857
|
const { manifestPath, skillPath } = pluginPaths(dir);
|
|
3587
|
-
if (!
|
|
4858
|
+
if (!existsSync11(manifestPath) && !existsSync11(skillPath)) {
|
|
3588
4859
|
console.log(`prim skill plugin not present at ${dir}`);
|
|
3589
4860
|
return 0;
|
|
3590
4861
|
}
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
removeDirIfEmpty(
|
|
4862
|
+
rmSync4(manifestPath, { force: true });
|
|
4863
|
+
rmSync4(skillPath, { force: true });
|
|
4864
|
+
removeDirIfEmpty(join10(dir, ".claude-plugin"));
|
|
3594
4865
|
removeDirIfEmpty(dir);
|
|
3595
4866
|
console.log(`Removed prim skill plugin from ${dir}`);
|
|
3596
4867
|
return 0;
|
|
@@ -3599,7 +4870,7 @@ function statusClaudePlugin(cwd, opts) {
|
|
|
3599
4870
|
const dir = resolvePluginDir(cwd, opts.scope);
|
|
3600
4871
|
if (dir === null) return 1;
|
|
3601
4872
|
const { skillPath } = pluginPaths(dir);
|
|
3602
|
-
const installed =
|
|
4873
|
+
const installed = existsSync11(skillPath);
|
|
3603
4874
|
if (opts.json) {
|
|
3604
4875
|
printJson({ installed, target: dir });
|
|
3605
4876
|
return installed ? 0 : 1;
|
|
@@ -3613,7 +4884,7 @@ function statusClaudePlugin(cwd, opts) {
|
|
|
3613
4884
|
}
|
|
3614
4885
|
|
|
3615
4886
|
// src/commands/skill.ts
|
|
3616
|
-
var __dirname2 =
|
|
4887
|
+
var __dirname2 = dirname7(fileURLToPath2(import.meta.url));
|
|
3617
4888
|
var SKILL_BEGIN = "<!-- BEGIN PRIM SKILL v1 -->";
|
|
3618
4889
|
var SKILL_END = "<!-- END PRIM SKILL v1 -->";
|
|
3619
4890
|
var TARGET_CANDIDATES = [
|
|
@@ -3631,24 +4902,24 @@ var AGENT_TARGET = {
|
|
|
3631
4902
|
hermes: ".hermes.md"
|
|
3632
4903
|
};
|
|
3633
4904
|
function userTargetFor(agent) {
|
|
3634
|
-
if (agent === "claude") return
|
|
3635
|
-
if (agent === "codex") return
|
|
4905
|
+
if (agent === "claude") return join11(homedir8(), ".claude", "CLAUDE.md");
|
|
4906
|
+
if (agent === "codex") return join11(homedir8(), ".codex", "AGENTS.md");
|
|
3636
4907
|
if (agent === "hermes") {
|
|
3637
|
-
return
|
|
4908
|
+
return join11(process.env.HERMES_HOME ?? join11(homedir8(), ".hermes"), ".hermes.md");
|
|
3638
4909
|
}
|
|
3639
4910
|
return null;
|
|
3640
4911
|
}
|
|
3641
4912
|
function loadSkill() {
|
|
3642
4913
|
let dir = __dirname2;
|
|
3643
|
-
while (dir !==
|
|
3644
|
-
const p =
|
|
3645
|
-
if (
|
|
3646
|
-
dir =
|
|
4914
|
+
while (dir !== dirname7(dir)) {
|
|
4915
|
+
const p = resolve4(dir, "SKILL.md");
|
|
4916
|
+
if (existsSync12(p)) return readFileSync10(p, "utf-8");
|
|
4917
|
+
dir = dirname7(dir);
|
|
3647
4918
|
}
|
|
3648
4919
|
throw new Error("SKILL.md not found in package");
|
|
3649
4920
|
}
|
|
3650
4921
|
function detectTargets(cwd) {
|
|
3651
|
-
return TARGET_CANDIDATES.filter((p) =>
|
|
4922
|
+
return TARGET_CANDIDATES.filter((p) => existsSync12(resolve4(cwd, p)));
|
|
3652
4923
|
}
|
|
3653
4924
|
function detectNewline(content) {
|
|
3654
4925
|
return content.includes("\r\n") ? "\r\n" : "\n";
|
|
@@ -3674,23 +4945,23 @@ function removeBlock(existing) {
|
|
|
3674
4945
|
const out = existing.slice(0, b) + existing.slice(e + SKILL_END.length);
|
|
3675
4946
|
return out.replace(/(\r?\n){2,}$/, "$1");
|
|
3676
4947
|
}
|
|
3677
|
-
function
|
|
4948
|
+
function atomicWrite3(target, content) {
|
|
3678
4949
|
const tmp = `${target}.tmp`;
|
|
3679
|
-
|
|
4950
|
+
writeFileSync8(tmp, content);
|
|
3680
4951
|
const fd = openSync4(tmp, "r+");
|
|
3681
4952
|
try {
|
|
3682
4953
|
fsyncSync3(fd);
|
|
3683
4954
|
} finally {
|
|
3684
4955
|
closeSync4(fd);
|
|
3685
4956
|
}
|
|
3686
|
-
|
|
4957
|
+
renameSync5(tmp, target);
|
|
3687
4958
|
}
|
|
3688
4959
|
function resolveTarget(cwd, opts) {
|
|
3689
4960
|
if (opts.scope && opts.scope !== "user" && opts.scope !== "project") {
|
|
3690
4961
|
console.error(`Unknown --scope "${opts.scope}" (expected user or project)`);
|
|
3691
4962
|
return null;
|
|
3692
4963
|
}
|
|
3693
|
-
if (opts.target) return
|
|
4964
|
+
if (opts.target) return resolve4(cwd, opts.target);
|
|
3694
4965
|
if (opts.scope === "user") {
|
|
3695
4966
|
if (!opts.agent) {
|
|
3696
4967
|
console.error("--scope user requires --agent (claude, codex, or hermes)");
|
|
@@ -3703,13 +4974,13 @@ function resolveTarget(cwd, opts) {
|
|
|
3703
4974
|
}
|
|
3704
4975
|
if (opts.agent) {
|
|
3705
4976
|
const mapped = AGENT_TARGET[opts.agent];
|
|
3706
|
-
if (typeof mapped === "string") return
|
|
4977
|
+
if (typeof mapped === "string") return resolve4(cwd, mapped);
|
|
3707
4978
|
console.error(`Unknown --agent "${opts.agent}" (expected claude, codex, or hermes)`);
|
|
3708
4979
|
return null;
|
|
3709
4980
|
}
|
|
3710
4981
|
const matches = detectTargets(cwd);
|
|
3711
|
-
if (matches.length === 0) return
|
|
3712
|
-
if (matches.length === 1) return
|
|
4982
|
+
if (matches.length === 0) return resolve4(cwd, DEFAULT_TARGET);
|
|
4983
|
+
if (matches.length === 1) return resolve4(cwd, matches[0]);
|
|
3713
4984
|
console.error("Multiple rules files detected. Use --target to disambiguate:");
|
|
3714
4985
|
for (const m of matches) console.error(` ${m}`);
|
|
3715
4986
|
return null;
|
|
@@ -3718,7 +4989,7 @@ function runInstall(cwd, opts) {
|
|
|
3718
4989
|
if (opts.agent === "claude" && !opts.target) return installClaudePlugin(cwd, opts);
|
|
3719
4990
|
const target = resolveTarget(cwd, opts);
|
|
3720
4991
|
if (target === null) return 1;
|
|
3721
|
-
const existing =
|
|
4992
|
+
const existing = existsSync12(target) ? readFileSync10(target, "utf-8") : "";
|
|
3722
4993
|
const eol = existing ? detectNewline(existing) : "\n";
|
|
3723
4994
|
const block = composeBlock(loadSkill(), eol);
|
|
3724
4995
|
const next = applyBlock(existing, block, eol);
|
|
@@ -3730,7 +5001,7 @@ function runInstall(cwd, opts) {
|
|
|
3730
5001
|
process.stdout.write(createPatch(target, existing, next, "current", "proposed"));
|
|
3731
5002
|
return 0;
|
|
3732
5003
|
}
|
|
3733
|
-
|
|
5004
|
+
atomicWrite3(target, next);
|
|
3734
5005
|
console.log(`Wrote ${Buffer.byteLength(next)} bytes to ${target}`);
|
|
3735
5006
|
return 0;
|
|
3736
5007
|
}
|
|
@@ -3738,17 +5009,17 @@ function runUninstall(cwd, opts) {
|
|
|
3738
5009
|
if (opts.agent === "claude" && !opts.target) return uninstallClaudePlugin(cwd, opts);
|
|
3739
5010
|
const target = resolveTarget(cwd, opts);
|
|
3740
5011
|
if (target === null) return 1;
|
|
3741
|
-
if (!
|
|
5012
|
+
if (!existsSync12(target)) {
|
|
3742
5013
|
console.log(`Skill block not present at ${target}`);
|
|
3743
5014
|
return 0;
|
|
3744
5015
|
}
|
|
3745
|
-
const existing =
|
|
5016
|
+
const existing = readFileSync10(target, "utf-8");
|
|
3746
5017
|
const next = removeBlock(existing);
|
|
3747
5018
|
if (next === null) {
|
|
3748
5019
|
console.log(`Skill block not present at ${target}`);
|
|
3749
5020
|
return 0;
|
|
3750
5021
|
}
|
|
3751
|
-
|
|
5022
|
+
atomicWrite3(target, next);
|
|
3752
5023
|
console.log(`Removed skill block from ${target}`);
|
|
3753
5024
|
return 0;
|
|
3754
5025
|
}
|
|
@@ -3756,10 +5027,10 @@ function runStatus(cwd, opts) {
|
|
|
3756
5027
|
if (opts.agent === "claude" && !opts.target) return statusClaudePlugin(cwd, opts);
|
|
3757
5028
|
const target = resolveTarget(cwd, opts);
|
|
3758
5029
|
if (target === null) return 1;
|
|
3759
|
-
const fileExists =
|
|
5030
|
+
const fileExists = existsSync12(target);
|
|
3760
5031
|
let installed = false;
|
|
3761
5032
|
if (fileExists) {
|
|
3762
|
-
const content =
|
|
5033
|
+
const content = readFileSync10(target, "utf-8");
|
|
3763
5034
|
installed = content.includes(SKILL_BEGIN) && content.includes(SKILL_END);
|
|
3764
5035
|
}
|
|
3765
5036
|
if (opts.json) {
|
|
@@ -3796,18 +5067,24 @@ function registerSkillCommands(program2) {
|
|
|
3796
5067
|
}
|
|
3797
5068
|
|
|
3798
5069
|
// src/commands/statusline.ts
|
|
3799
|
-
import { readFileSync as
|
|
3800
|
-
import { dirname as
|
|
5070
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
5071
|
+
import { dirname as dirname8, resolve as resolve5 } from "path";
|
|
3801
5072
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
3802
5073
|
var STATUSLINE_TIMEOUT_MS = 200;
|
|
3803
5074
|
var STATUSLINE_NAME_CAP = 3;
|
|
3804
5075
|
function readPackageVersion() {
|
|
3805
5076
|
try {
|
|
3806
|
-
const here =
|
|
3807
|
-
const candidates = [
|
|
5077
|
+
const here = dirname8(fileURLToPath3(import.meta.url));
|
|
5078
|
+
const candidates = [
|
|
5079
|
+
// The supervised runtime stages a tiny manifest beside the standalone
|
|
5080
|
+
// statusline bundle, so it remains versioned without loading the package.
|
|
5081
|
+
resolve5(here, "manifest.json"),
|
|
5082
|
+
resolve5(here, "../../package.json"),
|
|
5083
|
+
resolve5(here, "../package.json")
|
|
5084
|
+
];
|
|
3808
5085
|
for (const path of candidates) {
|
|
3809
5086
|
try {
|
|
3810
|
-
const pkg2 = JSON.parse(
|
|
5087
|
+
const pkg2 = JSON.parse(readFileSync11(path, "utf-8"));
|
|
3811
5088
|
if (pkg2.version) {
|
|
3812
5089
|
return pkg2.version;
|
|
3813
5090
|
}
|
|
@@ -3837,6 +5114,17 @@ async function renderStatusline() {
|
|
|
3837
5114
|
debug("daemon snapshot missing");
|
|
3838
5115
|
return `primitive ${version} (daemon: down)`;
|
|
3839
5116
|
}
|
|
5117
|
+
if (snapshot.healthy === false) {
|
|
5118
|
+
if (snapshot.ingestion?.healthy === false) {
|
|
5119
|
+
const pending = snapshot.ingestion.pendingCount;
|
|
5120
|
+
const qualifier = snapshot.ingestion.pendingSampled ? "at least " : "";
|
|
5121
|
+
return `primitive ${version} (daemon: degraded \xB7 delivery: stalled${typeof pending === "number" ? ` \xB7 ${qualifier}${String(pending)} pending` : ""})`;
|
|
5122
|
+
}
|
|
5123
|
+
if (snapshot.heartbeat?.healthy === false) {
|
|
5124
|
+
return `primitive ${version} (daemon: degraded \xB7 presence: unavailable)`;
|
|
5125
|
+
}
|
|
5126
|
+
return `primitive ${version} (daemon: starting)`;
|
|
5127
|
+
}
|
|
3840
5128
|
if (snapshot.envMismatch) {
|
|
3841
5129
|
return `primitive ${version} (daemon: live \xB7 presence: other env)`;
|
|
3842
5130
|
}
|
|
@@ -3862,6 +5150,7 @@ function registerStatuslineCommands(program2) {
|
|
|
3862
5150
|
const line = await renderStatusline();
|
|
3863
5151
|
process.stdout.write(line);
|
|
3864
5152
|
} catch {
|
|
5153
|
+
process.stdout.write(`primitive ${readPackageVersion()} (daemon: unavailable)`);
|
|
3865
5154
|
}
|
|
3866
5155
|
});
|
|
3867
5156
|
}
|
|
@@ -3978,8 +5267,8 @@ function registerWelcomeCommand(program2, deps = { getClient }) {
|
|
|
3978
5267
|
}
|
|
3979
5268
|
|
|
3980
5269
|
// src/index.ts
|
|
3981
|
-
var __dirname3 =
|
|
3982
|
-
var pkg = JSON.parse(
|
|
5270
|
+
var __dirname3 = dirname9(fileURLToPath4(import.meta.url));
|
|
5271
|
+
var pkg = JSON.parse(readFileSync12(resolve6(__dirname3, "../package.json"), "utf-8"));
|
|
3983
5272
|
updateNotifier({ pkg }).notify();
|
|
3984
5273
|
var program = new Command();
|
|
3985
5274
|
program.name("prim").description("CLI for Primitive's decision graph").version(pkg.version).option("-y, --yes", "auto-confirm prompts").option(
|