@primitive.ai/prim 0.1.0-alpha.40 → 0.1.0-alpha.42
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/SKILL.md +1 -1
- 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-NDIK4FBF.js +184 -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-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 +1610 -320
- package/dist/statusline-main.js +248 -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-NDIK4FBF.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,9 +1657,9 @@ ${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
|
|
1059
1665
|
function formatLabeled(labels, cap) {
|
|
@@ -1083,33 +1689,35 @@ function formatTeammatesWithArea(teammates, cap) {
|
|
|
1083
1689
|
|
|
1084
1690
|
// src/commands/daemon.ts
|
|
1085
1691
|
var DAEMON_BIN = "prim-daemon-server";
|
|
1086
|
-
var CONFIG_DIR =
|
|
1087
|
-
var PID_PATH =
|
|
1088
|
-
var SOCK_PATH =
|
|
1089
|
-
var LOG_PATH =
|
|
1692
|
+
var CONFIG_DIR = join4(homedir4(), ".config", "prim");
|
|
1693
|
+
var PID_PATH = join4(CONFIG_DIR, "daemon.pid");
|
|
1694
|
+
var SOCK_PATH = join4(CONFIG_DIR, "sock");
|
|
1695
|
+
var LOG_PATH = join4(CONFIG_DIR, "daemon.log");
|
|
1090
1696
|
var CONFIG_DIR_MODE = 448;
|
|
1091
1697
|
var LOG_FILE_MODE = 384;
|
|
1092
1698
|
var STOP_TIMEOUT_MS = 5e3;
|
|
1093
1699
|
var STOP_POLL_MS = 100;
|
|
1094
1700
|
var STATUS_PROBE_TIMEOUT_MS = 500;
|
|
1095
|
-
var
|
|
1096
|
-
var
|
|
1097
|
-
var
|
|
1701
|
+
var READY_TIMEOUT_MS2 = 5e3;
|
|
1702
|
+
var READY_POLL_MS2 = 100;
|
|
1703
|
+
var READY_PROBE_TIMEOUT_MS2 = 250;
|
|
1704
|
+
var HEALTHY_TIMEOUT_MS = 3e4;
|
|
1705
|
+
var HEALTHY_POLL_MS = 250;
|
|
1098
1706
|
var EXIT_OK2 = 0;
|
|
1099
1707
|
var EXIT_NOT_RUNNING = 2;
|
|
1100
1708
|
var EXIT_BOOTING = 3;
|
|
1101
1709
|
function readPidfile() {
|
|
1102
|
-
if (!
|
|
1710
|
+
if (!existsSync4(PID_PATH)) {
|
|
1103
1711
|
return null;
|
|
1104
1712
|
}
|
|
1105
|
-
const raw =
|
|
1713
|
+
const raw = readFileSync4(PID_PATH, "utf-8").trim();
|
|
1106
1714
|
const pid = Number(raw);
|
|
1107
1715
|
if (!Number.isInteger(pid) || pid <= 0) {
|
|
1108
1716
|
return null;
|
|
1109
1717
|
}
|
|
1110
|
-
return { pid, alive:
|
|
1718
|
+
return { pid, alive: processIsAlive2(pid) };
|
|
1111
1719
|
}
|
|
1112
|
-
function
|
|
1720
|
+
function processIsAlive2(pid) {
|
|
1113
1721
|
try {
|
|
1114
1722
|
process.kill(pid, 0);
|
|
1115
1723
|
return true;
|
|
@@ -1128,40 +1736,89 @@ function clearStaleArtifacts() {
|
|
|
1128
1736
|
}
|
|
1129
1737
|
}
|
|
1130
1738
|
function sleep(ms) {
|
|
1131
|
-
return new Promise((
|
|
1132
|
-
const timer = setTimeout(resolve6, ms);
|
|
1133
|
-
timer.unref();
|
|
1134
|
-
});
|
|
1739
|
+
return new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
1135
1740
|
}
|
|
1136
1741
|
function spawnDaemon(options) {
|
|
1137
1742
|
const file = binFile(DAEMON_BIN);
|
|
1138
1743
|
return file ? spawn(process.execPath, [file], options) : spawn(DAEMON_BIN, [], options);
|
|
1139
1744
|
}
|
|
1140
1745
|
function openDaemonLog(configDir = CONFIG_DIR) {
|
|
1141
|
-
if (!
|
|
1142
|
-
|
|
1746
|
+
if (!existsSync4(configDir)) {
|
|
1747
|
+
mkdirSync4(configDir, { recursive: true, mode: CONFIG_DIR_MODE });
|
|
1143
1748
|
}
|
|
1144
|
-
return openSync2(
|
|
1749
|
+
return openSync2(join4(configDir, "daemon.log"), "a", LOG_FILE_MODE);
|
|
1145
1750
|
}
|
|
1146
|
-
async function
|
|
1147
|
-
const deadline = Date.now() +
|
|
1751
|
+
async function waitForReady2() {
|
|
1752
|
+
const deadline = Date.now() + READY_TIMEOUT_MS2;
|
|
1148
1753
|
while (Date.now() < deadline) {
|
|
1149
|
-
if (await daemonIsLive(
|
|
1754
|
+
if (await daemonIsLive(READY_PROBE_TIMEOUT_MS2)) {
|
|
1150
1755
|
return true;
|
|
1151
1756
|
}
|
|
1152
|
-
await sleep(
|
|
1757
|
+
await sleep(READY_POLL_MS2);
|
|
1153
1758
|
}
|
|
1154
|
-
return daemonIsLive(
|
|
1759
|
+
return daemonIsLive(READY_PROBE_TIMEOUT_MS2);
|
|
1155
1760
|
}
|
|
1156
|
-
async function
|
|
1761
|
+
async function waitForHealthySnapshot(expectedVersion) {
|
|
1762
|
+
const deadline = Date.now() + HEALTHY_TIMEOUT_MS;
|
|
1763
|
+
let snapshot = null;
|
|
1764
|
+
while (Date.now() < deadline) {
|
|
1765
|
+
snapshot = await daemonRequest(
|
|
1766
|
+
"status_snapshot",
|
|
1767
|
+
{},
|
|
1768
|
+
{ timeoutMs: STATUS_PROBE_TIMEOUT_MS }
|
|
1769
|
+
);
|
|
1770
|
+
if (snapshot?.healthy === true && snapshot.version === expectedVersion) return snapshot;
|
|
1771
|
+
await sleep(HEALTHY_POLL_MS);
|
|
1772
|
+
}
|
|
1773
|
+
return snapshot;
|
|
1774
|
+
}
|
|
1775
|
+
async function verifiedPid(existing) {
|
|
1776
|
+
if (!existing.alive) return null;
|
|
1777
|
+
const snapshot = await daemonRequest(
|
|
1778
|
+
"status_snapshot",
|
|
1779
|
+
{},
|
|
1780
|
+
{ timeoutMs: STATUS_PROBE_TIMEOUT_MS }
|
|
1781
|
+
);
|
|
1782
|
+
return snapshot?.pid === existing.pid ? snapshot : null;
|
|
1783
|
+
}
|
|
1784
|
+
function daemonStartIsHealthy(serviceReady, snapshot, expectedVersion) {
|
|
1785
|
+
return serviceReady && snapshot?.healthy === true && expectedVersion !== void 0 && snapshot.version === expectedVersion;
|
|
1786
|
+
}
|
|
1787
|
+
async function detachedDaemonStart(opts) {
|
|
1157
1788
|
const existing = readPidfile();
|
|
1158
1789
|
if (existing?.alive) {
|
|
1159
|
-
|
|
1790
|
+
const snapshot = await verifiedPid(existing);
|
|
1791
|
+
if (snapshot) {
|
|
1792
|
+
process.stderr.write(`[prim] daemon already running (pid=${existing.pid})
|
|
1160
1793
|
`);
|
|
1161
|
-
|
|
1794
|
+
console.log(JSON.stringify({ started: false, pid: existing.pid }, null, 2));
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
process.stderr.write(
|
|
1798
|
+
`[prim] refusing to replace live pid=${existing.pid}: daemon ownership could not be verified over ${SOCK_PATH}
|
|
1799
|
+
`
|
|
1800
|
+
);
|
|
1801
|
+
console.log(JSON.stringify({ started: false, pid: existing.pid, verified: false }, null, 2));
|
|
1802
|
+
if (!process.exitCode) process.exitCode = EXIT_BOOTING;
|
|
1162
1803
|
return;
|
|
1163
1804
|
}
|
|
1164
1805
|
if (existing && !existing.alive) {
|
|
1806
|
+
const socketOwner = await daemonRequest(
|
|
1807
|
+
"status_snapshot",
|
|
1808
|
+
{},
|
|
1809
|
+
{ timeoutMs: STATUS_PROBE_TIMEOUT_MS }
|
|
1810
|
+
);
|
|
1811
|
+
if (socketOwner) {
|
|
1812
|
+
process.stderr.write(
|
|
1813
|
+
`[prim] refusing to clear stale pidfile: socket is owned by pid=${socketOwner.pid}
|
|
1814
|
+
`
|
|
1815
|
+
);
|
|
1816
|
+
console.log(
|
|
1817
|
+
JSON.stringify({ started: false, pid: socketOwner.pid, verified: false }, null, 2)
|
|
1818
|
+
);
|
|
1819
|
+
if (!process.exitCode) process.exitCode = EXIT_BOOTING;
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1165
1822
|
clearStaleArtifacts();
|
|
1166
1823
|
}
|
|
1167
1824
|
if (opts.foreground) {
|
|
@@ -1185,7 +1842,7 @@ async function daemonStart(opts) {
|
|
|
1185
1842
|
if (logFd !== void 0) {
|
|
1186
1843
|
closeSync2(logFd);
|
|
1187
1844
|
}
|
|
1188
|
-
const live = await
|
|
1845
|
+
const live = await waitForReady2();
|
|
1189
1846
|
if (live) {
|
|
1190
1847
|
const after = readPidfile();
|
|
1191
1848
|
process.stderr.write(
|
|
@@ -1196,7 +1853,7 @@ async function daemonStart(opts) {
|
|
|
1196
1853
|
return;
|
|
1197
1854
|
}
|
|
1198
1855
|
process.stderr.write(
|
|
1199
|
-
`[prim] \u2717 daemon start: spawned but the socket did not respond within ${
|
|
1856
|
+
`[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
1857
|
`
|
|
1201
1858
|
);
|
|
1202
1859
|
console.log(JSON.stringify({ started: false }, null, 2));
|
|
@@ -1204,7 +1861,7 @@ async function daemonStart(opts) {
|
|
|
1204
1861
|
process.exitCode = EXIT_NOT_RUNNING;
|
|
1205
1862
|
}
|
|
1206
1863
|
}
|
|
1207
|
-
async function
|
|
1864
|
+
async function detachedDaemonStop() {
|
|
1208
1865
|
const existing = readPidfile();
|
|
1209
1866
|
if (!existing) {
|
|
1210
1867
|
process.stderr.write("[prim] daemon not running (no pidfile)\n");
|
|
@@ -1212,11 +1869,36 @@ async function daemonStop() {
|
|
|
1212
1869
|
return;
|
|
1213
1870
|
}
|
|
1214
1871
|
if (!existing.alive) {
|
|
1872
|
+
const socketOwner = await daemonRequest(
|
|
1873
|
+
"status_snapshot",
|
|
1874
|
+
{},
|
|
1875
|
+
{ timeoutMs: STATUS_PROBE_TIMEOUT_MS }
|
|
1876
|
+
);
|
|
1877
|
+
if (socketOwner) {
|
|
1878
|
+
process.stderr.write(
|
|
1879
|
+
`[prim] refusing to clear stale pidfile: socket is owned by pid=${socketOwner.pid}
|
|
1880
|
+
`
|
|
1881
|
+
);
|
|
1882
|
+
console.log(
|
|
1883
|
+
JSON.stringify({ stopped: false, pid: socketOwner.pid, verified: false }, null, 2)
|
|
1884
|
+
);
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1215
1887
|
clearStaleArtifacts();
|
|
1216
1888
|
process.stderr.write("[prim] daemon not running (cleared stale pidfile)\n");
|
|
1217
1889
|
console.log(JSON.stringify({ stopped: false, wasRunning: false }, null, 2));
|
|
1218
1890
|
return;
|
|
1219
1891
|
}
|
|
1892
|
+
const snapshot = await verifiedPid(existing);
|
|
1893
|
+
if (!snapshot) {
|
|
1894
|
+
process.stderr.write(
|
|
1895
|
+
`[prim] refusing to signal live pid=${existing.pid}: daemon ownership could not be verified over ${SOCK_PATH}
|
|
1896
|
+
`
|
|
1897
|
+
);
|
|
1898
|
+
console.log(JSON.stringify({ stopped: false, pid: existing.pid, verified: false }, null, 2));
|
|
1899
|
+
if (!process.exitCode) process.exitCode = EXIT_BOOTING;
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1220
1902
|
try {
|
|
1221
1903
|
process.kill(existing.pid, "SIGTERM");
|
|
1222
1904
|
} catch (err) {
|
|
@@ -1229,7 +1911,7 @@ async function daemonStop() {
|
|
|
1229
1911
|
}
|
|
1230
1912
|
const deadline = Date.now() + STOP_TIMEOUT_MS;
|
|
1231
1913
|
while (Date.now() < deadline) {
|
|
1232
|
-
if (!
|
|
1914
|
+
if (!processIsAlive2(existing.pid)) {
|
|
1233
1915
|
clearStaleArtifacts();
|
|
1234
1916
|
process.stderr.write(`[prim] daemon stopped (pid=${existing.pid})
|
|
1235
1917
|
`);
|
|
@@ -1243,23 +1925,212 @@ async function daemonStop() {
|
|
|
1243
1925
|
`
|
|
1244
1926
|
);
|
|
1245
1927
|
console.log(JSON.stringify({ stopped: false, pid: existing.pid }, null, 2));
|
|
1928
|
+
if (!process.exitCode) process.exitCode = EXIT_BOOTING;
|
|
1929
|
+
}
|
|
1930
|
+
async function macDaemonStart(forceRestart = false) {
|
|
1931
|
+
const result = await ensureMacDaemon({ explicitlyStarted: true, forceRestart });
|
|
1932
|
+
const serviceReady = result.state === "running";
|
|
1933
|
+
const expectedVersion = result.runtime?.manifest.version;
|
|
1934
|
+
const snapshot = serviceReady && expectedVersion ? await waitForHealthySnapshot(expectedVersion) : null;
|
|
1935
|
+
const healthy = daemonStartIsHealthy(serviceReady, snapshot, expectedVersion);
|
|
1936
|
+
if (healthy) {
|
|
1937
|
+
const verb = result.action === "none" ? "already running" : "started";
|
|
1938
|
+
process.stderr.write(
|
|
1939
|
+
`[prim] \u2713 daemon ${verb} under launchd (pid=${snapshot?.pid ?? result.service.pid ?? "?"})
|
|
1940
|
+
`
|
|
1941
|
+
);
|
|
1942
|
+
} else {
|
|
1943
|
+
process.stderr.write(
|
|
1944
|
+
`[prim] \u2717 launchd daemon did not reach healthy heartbeat + ingestion state (see ${LOG_PATH})
|
|
1945
|
+
`
|
|
1946
|
+
);
|
|
1947
|
+
if (!process.exitCode) process.exitCode = EXIT_NOT_RUNNING;
|
|
1948
|
+
}
|
|
1949
|
+
console.log(
|
|
1950
|
+
JSON.stringify(
|
|
1951
|
+
{
|
|
1952
|
+
started: healthy,
|
|
1953
|
+
supervised: true,
|
|
1954
|
+
action: result.action,
|
|
1955
|
+
pid: snapshot?.pid ?? result.service.pid,
|
|
1956
|
+
loaded: result.service.loaded,
|
|
1957
|
+
responding: result.responding,
|
|
1958
|
+
healthy,
|
|
1959
|
+
version: snapshot?.version,
|
|
1960
|
+
expectedVersion
|
|
1961
|
+
},
|
|
1962
|
+
null,
|
|
1963
|
+
2
|
|
1964
|
+
)
|
|
1965
|
+
);
|
|
1966
|
+
}
|
|
1967
|
+
async function daemonStart(opts) {
|
|
1968
|
+
if (process.platform === "darwin") {
|
|
1969
|
+
if (!opts.foreground) {
|
|
1970
|
+
await macDaemonStart(false);
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
await withDaemonLifecycleLock(async () => {
|
|
1975
|
+
setDaemonExplicitlyDisabled(false);
|
|
1976
|
+
await detachedDaemonStart(opts);
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
async function daemonStop() {
|
|
1980
|
+
if (process.platform !== "darwin") {
|
|
1981
|
+
await withDaemonLifecycleLock(async () => {
|
|
1982
|
+
setDaemonExplicitlyDisabled(true);
|
|
1983
|
+
await detachedDaemonStop();
|
|
1984
|
+
});
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
const result = await bootoutMacDaemon();
|
|
1988
|
+
process.stderr.write(
|
|
1989
|
+
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"
|
|
1990
|
+
);
|
|
1991
|
+
console.log(
|
|
1992
|
+
JSON.stringify(
|
|
1993
|
+
{
|
|
1994
|
+
stopped: result.wasLoaded || result.legacyStopped,
|
|
1995
|
+
wasRunning: result.wasLoaded || result.legacyStopped,
|
|
1996
|
+
supervised: true,
|
|
1997
|
+
disabled: true
|
|
1998
|
+
},
|
|
1999
|
+
null,
|
|
2000
|
+
2
|
|
2001
|
+
)
|
|
2002
|
+
);
|
|
1246
2003
|
}
|
|
1247
2004
|
function classifyStatus(pidAlive, responding, snapshot, pid) {
|
|
1248
2005
|
if (!pidAlive) {
|
|
1249
2006
|
return { json: { running: false }, exitCode: EXIT_NOT_RUNNING };
|
|
1250
2007
|
}
|
|
1251
|
-
if (!responding) {
|
|
2008
|
+
if (!responding) {
|
|
2009
|
+
return {
|
|
2010
|
+
json: { running: true, responding: false, state: "starting", pid },
|
|
2011
|
+
exitCode: EXIT_BOOTING
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
if (!snapshot) {
|
|
2015
|
+
return { json: { running: true, responding: true }, exitCode: EXIT_OK2 };
|
|
2016
|
+
}
|
|
2017
|
+
if (snapshot.healthy === false) {
|
|
2018
|
+
return {
|
|
2019
|
+
json: { running: true, responding: true, state: "degraded", ...snapshot },
|
|
2020
|
+
exitCode: EXIT_BOOTING
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
2023
|
+
return { json: { running: true, responding: true, ...snapshot }, exitCode: EXIT_OK2 };
|
|
2024
|
+
}
|
|
2025
|
+
function classifyLaunchdStatus(service, responding, snapshot, disabled) {
|
|
2026
|
+
if (!service.loaded) {
|
|
2027
|
+
if (responding) {
|
|
2028
|
+
return {
|
|
2029
|
+
json: {
|
|
2030
|
+
running: true,
|
|
2031
|
+
responding: true,
|
|
2032
|
+
supervised: false,
|
|
2033
|
+
state: "unsupervised",
|
|
2034
|
+
disabled,
|
|
2035
|
+
...snapshot
|
|
2036
|
+
},
|
|
2037
|
+
exitCode: EXIT_BOOTING
|
|
2038
|
+
};
|
|
2039
|
+
}
|
|
2040
|
+
return {
|
|
2041
|
+
json: { running: false, supervised: true, loaded: false, disabled },
|
|
2042
|
+
exitCode: EXIT_NOT_RUNNING
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
2045
|
+
if (!responding) {
|
|
2046
|
+
return {
|
|
2047
|
+
json: {
|
|
2048
|
+
running: true,
|
|
2049
|
+
responding: false,
|
|
2050
|
+
supervised: true,
|
|
2051
|
+
state: service.state ?? "starting",
|
|
2052
|
+
pid: service.pid,
|
|
2053
|
+
disabled
|
|
2054
|
+
},
|
|
2055
|
+
exitCode: EXIT_BOOTING
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2058
|
+
if (!snapshot || service.pid === void 0) {
|
|
2059
|
+
return {
|
|
2060
|
+
json: {
|
|
2061
|
+
running: true,
|
|
2062
|
+
responding: true,
|
|
2063
|
+
supervised: true,
|
|
2064
|
+
state: "ownership_unverified",
|
|
2065
|
+
pid: service.pid,
|
|
2066
|
+
socketPid: snapshot?.pid,
|
|
2067
|
+
disabled
|
|
2068
|
+
},
|
|
2069
|
+
exitCode: EXIT_BOOTING
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
if (service.pid !== snapshot.pid) {
|
|
1252
2073
|
return {
|
|
1253
|
-
json: {
|
|
2074
|
+
json: {
|
|
2075
|
+
running: true,
|
|
2076
|
+
responding: true,
|
|
2077
|
+
supervised: true,
|
|
2078
|
+
state: "pid_mismatch",
|
|
2079
|
+
pid: service.pid,
|
|
2080
|
+
socketPid: snapshot.pid,
|
|
2081
|
+
disabled
|
|
2082
|
+
},
|
|
2083
|
+
exitCode: EXIT_BOOTING
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
2086
|
+
if (snapshot?.healthy === false) {
|
|
2087
|
+
return {
|
|
2088
|
+
json: {
|
|
2089
|
+
running: true,
|
|
2090
|
+
responding: true,
|
|
2091
|
+
supervised: true,
|
|
2092
|
+
state: "degraded",
|
|
2093
|
+
disabled,
|
|
2094
|
+
...snapshot
|
|
2095
|
+
},
|
|
1254
2096
|
exitCode: EXIT_BOOTING
|
|
1255
2097
|
};
|
|
1256
2098
|
}
|
|
2099
|
+
return {
|
|
2100
|
+
json: {
|
|
2101
|
+
running: true,
|
|
2102
|
+
responding: true,
|
|
2103
|
+
supervised: true,
|
|
2104
|
+
state: service.state ?? "running",
|
|
2105
|
+
disabled,
|
|
2106
|
+
...snapshot
|
|
2107
|
+
},
|
|
2108
|
+
exitCode: EXIT_OK2
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
function writeLiveSnapshot(snapshot, supervised = false) {
|
|
1257
2112
|
if (!snapshot) {
|
|
1258
|
-
|
|
2113
|
+
process.stderr.write(
|
|
2114
|
+
supervised ? "[prim] \u2713 daemon live under launchd (no snapshot)\n" : "[prim] \u2713 daemon live\n"
|
|
2115
|
+
);
|
|
2116
|
+
return;
|
|
1259
2117
|
}
|
|
1260
|
-
|
|
2118
|
+
const team = snapshot.onlineNames !== void 0 ? ` \xB7 team: ${formatTeammates(snapshot.onlineNames, Number.POSITIVE_INFINITY)}` : "";
|
|
2119
|
+
if (snapshot.healthy === false) {
|
|
2120
|
+
process.stderr.write(
|
|
2121
|
+
`[prim] \u2717 daemon unhealthy${supervised ? " under launchd" : ""} \xB7 pid=${snapshot.pid}${team}
|
|
2122
|
+
`
|
|
2123
|
+
);
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
2126
|
+
process.stderr.write(
|
|
2127
|
+
`[prim] \u2713 daemon live${supervised ? " under launchd" : ""} \xB7 pid=${snapshot.pid} \xB7 uptime=${Math.round(
|
|
2128
|
+
snapshot.uptimeMs / 1e3
|
|
2129
|
+
)}s \xB7 session=${snapshot.sessionId}${team}
|
|
2130
|
+
`
|
|
2131
|
+
);
|
|
1261
2132
|
}
|
|
1262
|
-
async function
|
|
2133
|
+
async function detachedDaemonStatus() {
|
|
1263
2134
|
const pid = readPidfile();
|
|
1264
2135
|
const pidAlive = pid?.alive ?? false;
|
|
1265
2136
|
const responding = pidAlive ? await daemonIsLive(STATUS_PROBE_TIMEOUT_MS) : false;
|
|
@@ -1274,38 +2145,127 @@ async function daemonStatus() {
|
|
|
1274
2145
|
} else if (!responding) {
|
|
1275
2146
|
process.stderr.write(`[prim] \u25CC daemon pid=${pid?.pid} starting (socket not responding yet)
|
|
1276
2147
|
`);
|
|
1277
|
-
} else if (!snapshot) {
|
|
1278
|
-
process.stderr.write("[prim] \u2713 daemon live (no snapshot)\n");
|
|
1279
2148
|
} else {
|
|
1280
|
-
|
|
2149
|
+
writeLiveSnapshot(snapshot);
|
|
2150
|
+
}
|
|
2151
|
+
console.log(JSON.stringify(json, null, 2));
|
|
2152
|
+
if (exitCode !== EXIT_OK2 && !process.exitCode) {
|
|
2153
|
+
process.exitCode = exitCode;
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
async function macDaemonStatus() {
|
|
2157
|
+
const service = getLaunchdService();
|
|
2158
|
+
const responding = await daemonIsLive(STATUS_PROBE_TIMEOUT_MS);
|
|
2159
|
+
const snapshot = responding ? await daemonRequest(
|
|
2160
|
+
"status_snapshot",
|
|
2161
|
+
{},
|
|
2162
|
+
{ timeoutMs: STATUS_PROBE_TIMEOUT_MS }
|
|
2163
|
+
) : null;
|
|
2164
|
+
const disabled = daemonExplicitlyDisabled();
|
|
2165
|
+
const { json, exitCode } = classifyLaunchdStatus(service, responding, snapshot, disabled);
|
|
2166
|
+
if (!service.loaded && responding) {
|
|
2167
|
+
process.stderr.write(
|
|
2168
|
+
`[prim] \u25CC daemon pid=${snapshot?.pid ?? "?"} is live but not supervised by launchd
|
|
2169
|
+
`
|
|
2170
|
+
);
|
|
2171
|
+
} else if (!service.loaded) {
|
|
2172
|
+
process.stderr.write(`[prim] \u2717 daemon down${disabled ? " (explicitly disabled)" : ""}
|
|
2173
|
+
`);
|
|
2174
|
+
} else if (!responding) {
|
|
2175
|
+
process.stderr.write(
|
|
2176
|
+
`[prim] \u25CC launchd service ${service.state ?? "loaded"}; socket is not responding yet
|
|
2177
|
+
`
|
|
2178
|
+
);
|
|
2179
|
+
} else if (!snapshot || service.pid === void 0) {
|
|
2180
|
+
process.stderr.write("[prim] \u2717 launchd daemon socket ownership could not be verified\n");
|
|
2181
|
+
} else if (service.pid !== snapshot.pid) {
|
|
1281
2182
|
process.stderr.write(
|
|
1282
|
-
`[prim] \
|
|
2183
|
+
`[prim] \u2717 launchd pid=${service.pid} does not own the daemon socket (pid=${snapshot.pid})
|
|
1283
2184
|
`
|
|
1284
2185
|
);
|
|
2186
|
+
} else {
|
|
2187
|
+
writeLiveSnapshot(snapshot, true);
|
|
1285
2188
|
}
|
|
1286
2189
|
console.log(JSON.stringify(json, null, 2));
|
|
1287
|
-
if (exitCode !== EXIT_OK2 && !process.exitCode)
|
|
1288
|
-
|
|
2190
|
+
if (exitCode !== EXIT_OK2 && !process.exitCode) process.exitCode = exitCode;
|
|
2191
|
+
}
|
|
2192
|
+
async function daemonStatus() {
|
|
2193
|
+
if (process.platform === "darwin") {
|
|
2194
|
+
await macDaemonStatus();
|
|
2195
|
+
return;
|
|
1289
2196
|
}
|
|
2197
|
+
await detachedDaemonStatus();
|
|
1290
2198
|
}
|
|
1291
2199
|
async function daemonRestart(opts) {
|
|
2200
|
+
if (process.platform === "darwin" && !opts.foreground) {
|
|
2201
|
+
await macDaemonStart(true);
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
if (process.platform !== "darwin") {
|
|
2205
|
+
await withDaemonLifecycleLock(async () => {
|
|
2206
|
+
setDaemonExplicitlyDisabled(false);
|
|
2207
|
+
await detachedDaemonStop();
|
|
2208
|
+
await detachedDaemonStart(opts);
|
|
2209
|
+
});
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
1292
2212
|
await daemonStop();
|
|
1293
2213
|
await daemonStart(opts);
|
|
1294
2214
|
}
|
|
2215
|
+
async function daemonEnsure() {
|
|
2216
|
+
if (process.platform !== "darwin") {
|
|
2217
|
+
const disabled = await withDaemonLifecycleLock(async () => {
|
|
2218
|
+
if (daemonExplicitlyDisabled()) return true;
|
|
2219
|
+
await detachedDaemonStart({});
|
|
2220
|
+
return false;
|
|
2221
|
+
});
|
|
2222
|
+
if (disabled) {
|
|
2223
|
+
process.stderr.write("[prim] daemon remains explicitly disabled\n");
|
|
2224
|
+
console.log(JSON.stringify({ ensured: false, disabled: true, supervised: false }, null, 2));
|
|
2225
|
+
}
|
|
2226
|
+
return;
|
|
2227
|
+
}
|
|
2228
|
+
const result = await ensureMacDaemon();
|
|
2229
|
+
if (result.state === "disabled") {
|
|
2230
|
+
process.stderr.write("[prim] daemon remains explicitly disabled\n");
|
|
2231
|
+
} else if (result.state === "running") {
|
|
2232
|
+
process.stderr.write(`[prim] \u2713 daemon ensured under launchd (${result.action})
|
|
2233
|
+
`);
|
|
2234
|
+
} else {
|
|
2235
|
+
process.stderr.write(`[prim] \u2717 daemon ensure failed; see ${LOG_PATH}
|
|
2236
|
+
`);
|
|
2237
|
+
if (!process.exitCode) process.exitCode = EXIT_NOT_RUNNING;
|
|
2238
|
+
}
|
|
2239
|
+
console.log(
|
|
2240
|
+
JSON.stringify(
|
|
2241
|
+
{
|
|
2242
|
+
ensured: result.state === "running",
|
|
2243
|
+
disabled: result.state === "disabled",
|
|
2244
|
+
supervised: true,
|
|
2245
|
+
action: result.action
|
|
2246
|
+
},
|
|
2247
|
+
null,
|
|
2248
|
+
2
|
|
2249
|
+
)
|
|
2250
|
+
);
|
|
2251
|
+
}
|
|
1295
2252
|
function registerDaemonCommands(program2) {
|
|
1296
2253
|
const daemon = program2.command("daemon").description("Manage the prim companion daemon (latency unlock + presence + broadcast)");
|
|
1297
|
-
daemon.command("start").description("
|
|
2254
|
+
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
2255
|
await daemonStart(opts);
|
|
1299
2256
|
});
|
|
1300
|
-
daemon.command("stop").description("
|
|
2257
|
+
daemon.command("stop").description("Stop and explicitly disable the daemon").action(async () => {
|
|
1301
2258
|
await daemonStop();
|
|
1302
2259
|
});
|
|
1303
2260
|
daemon.command("status").description("Report daemon liveness + a snapshot if responding").action(async () => {
|
|
1304
2261
|
await daemonStatus();
|
|
1305
2262
|
});
|
|
1306
|
-
daemon.command("restart").description("
|
|
2263
|
+
daemon.command("restart").description("Restart the daemon and clear any explicit disable marker").option("--foreground", "Restart in the foreground").action(async (opts) => {
|
|
1307
2264
|
await daemonRestart(opts);
|
|
1308
2265
|
});
|
|
2266
|
+
daemon.command("ensure").description("Idempotently install, upgrade, and heal the daemon unless explicitly disabled").action(async () => {
|
|
2267
|
+
await daemonEnsure();
|
|
2268
|
+
});
|
|
1309
2269
|
}
|
|
1310
2270
|
|
|
1311
2271
|
// src/decisions/cascade-renderer.ts
|
|
@@ -1576,6 +2536,12 @@ async function fetchRecent(args, deps = defaultDeps2) {
|
|
|
1576
2536
|
if (res.authorHasDecisions !== void 0) {
|
|
1577
2537
|
result.authorHasDecisions = res.authorHasDecisions;
|
|
1578
2538
|
}
|
|
2539
|
+
if (res.windowTotal !== void 0) {
|
|
2540
|
+
result.windowTotal = res.windowTotal;
|
|
2541
|
+
}
|
|
2542
|
+
if (res.windowTotalCapped !== void 0) {
|
|
2543
|
+
result.windowTotalCapped = res.windowTotalCapped;
|
|
2544
|
+
}
|
|
1579
2545
|
if (res.unavailable !== void 0) {
|
|
1580
2546
|
result.unavailable = res.unavailable;
|
|
1581
2547
|
}
|
|
@@ -1628,6 +2594,19 @@ function formatRecentRow(row) {
|
|
|
1628
2594
|
const areaCol = row.area ? areaPlain.replace("\u2022", color("\u2022", colorForArea(row.area))) : areaPlain;
|
|
1629
2595
|
return ` ${clock} ${author}${areaCol}${row.intent}`;
|
|
1630
2596
|
}
|
|
2597
|
+
var RECENT_LIMIT_CEILING = 100;
|
|
2598
|
+
function remainingHint(result) {
|
|
2599
|
+
const { windowTotal } = result;
|
|
2600
|
+
if (windowTotal === void 0 || windowTotal <= result.decisions.length) {
|
|
2601
|
+
return;
|
|
2602
|
+
}
|
|
2603
|
+
const remaining = windowTotal - result.decisions.length;
|
|
2604
|
+
const remainingText = result.windowTotalCapped === true ? `${remaining}+` : String(remaining);
|
|
2605
|
+
if (windowTotal <= RECENT_LIMIT_CEILING) {
|
|
2606
|
+
return `${remainingText} more not shown \u2014 re-run with --limit ${windowTotal} to see all`;
|
|
2607
|
+
}
|
|
2608
|
+
return `${remainingText} more not shown \u2014 re-run with --limit ${RECENT_LIMIT_CEILING} for the newest ${RECENT_LIMIT_CEILING} (the full set exceeds the feed cap)`;
|
|
2609
|
+
}
|
|
1631
2610
|
function formatRecentHuman(result) {
|
|
1632
2611
|
if (result.unavailable !== void 0) {
|
|
1633
2612
|
return `[prim] recent \xB7 feed not verified \u2014 ${result.unavailable}`;
|
|
@@ -1645,7 +2624,9 @@ function formatRecentHuman(result) {
|
|
|
1645
2624
|
if (result.decisions.length === 0) {
|
|
1646
2625
|
return "[prim] recent \xB7 0 decisions";
|
|
1647
2626
|
}
|
|
1648
|
-
const
|
|
2627
|
+
const header = `[prim] ${label} \xB7 ${String(result.decisions.length)} decision(s)`;
|
|
2628
|
+
const hint = remainingHint(result);
|
|
2629
|
+
const lines = [hint === void 0 ? header : `${header} \xB7 ${hint}`];
|
|
1649
2630
|
for (const row of result.decisions) {
|
|
1650
2631
|
lines.push(formatRecentRow(row));
|
|
1651
2632
|
}
|
|
@@ -2132,11 +3113,11 @@ function registerDecisionsCommands(program2) {
|
|
|
2132
3113
|
}
|
|
2133
3114
|
|
|
2134
3115
|
// src/commands/doctor.ts
|
|
2135
|
-
import { existsSync as
|
|
3116
|
+
import { existsSync as existsSync5 } from "fs";
|
|
2136
3117
|
var DAEMON_PROBE_TIMEOUT_MS = 500;
|
|
2137
3118
|
var CONNECTIVITY_TIMEOUT_MS = 3e3;
|
|
2138
3119
|
var MS_PER_SECOND = 1e3;
|
|
2139
|
-
var STALE_PENDING_MS =
|
|
3120
|
+
var STALE_PENDING_MS = 3e4;
|
|
2140
3121
|
var EXIT_UNHEALTHY = 1;
|
|
2141
3122
|
function classifyDoctor(checks) {
|
|
2142
3123
|
const status = checks.some((c) => c.status === "fail") ? "fail" : checks.some((c) => c.status === "warn") ? "warn" : "ok";
|
|
@@ -2150,7 +3131,7 @@ function checkAuth() {
|
|
|
2150
3131
|
return { name: "auth", status: "fail", detail: "no token \u2014 run `prim auth login`" };
|
|
2151
3132
|
}
|
|
2152
3133
|
const expiresAt = getTokenExpiresAt();
|
|
2153
|
-
const hasRefresh =
|
|
3134
|
+
const hasRefresh = existsSync5(REFRESH_TOKEN_PATH);
|
|
2154
3135
|
if (expiresAt !== void 0 && Date.now() >= expiresAt) {
|
|
2155
3136
|
return hasRefresh ? { name: "auth", status: "warn", detail: "access token expired (refresh available)" } : {
|
|
2156
3137
|
name: "auth",
|
|
@@ -2164,62 +3145,269 @@ function checkAuth() {
|
|
|
2164
3145
|
const detail = expiresAt !== void 0 ? `valid (${String(Math.round((expiresAt - Date.now()) / MS_PER_SECOND))}s left)` : "valid";
|
|
2165
3146
|
return { name: "auth", status: "ok", detail };
|
|
2166
3147
|
}
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
3148
|
+
function classifyDaemonHealth(snapshot, options = {}) {
|
|
3149
|
+
if (options.disabled) {
|
|
3150
|
+
return {
|
|
3151
|
+
name: "daemon",
|
|
3152
|
+
status: "fail",
|
|
3153
|
+
detail: "explicitly stopped \u2014 run `prim daemon start`"
|
|
3154
|
+
};
|
|
3155
|
+
}
|
|
3156
|
+
if (options.service && !options.service.loaded) {
|
|
3157
|
+
return {
|
|
3158
|
+
name: "daemon",
|
|
3159
|
+
status: "fail",
|
|
3160
|
+
detail: "launchd service is not loaded \u2014 run `prim daemon start`"
|
|
3161
|
+
};
|
|
3162
|
+
}
|
|
3163
|
+
if (!snapshot) {
|
|
3164
|
+
return {
|
|
3165
|
+
name: "daemon",
|
|
3166
|
+
status: "fail",
|
|
3167
|
+
detail: "socket unavailable \u2014 run `prim daemon start`"
|
|
3168
|
+
};
|
|
3169
|
+
}
|
|
3170
|
+
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)) {
|
|
3171
|
+
return {
|
|
3172
|
+
name: "daemon",
|
|
3173
|
+
status: "fail",
|
|
3174
|
+
detail: `launchd does not own the daemon socket (launchd ${String(options.service.pid ?? "none")} \xB7 socket ${String(snapshot.pid ?? "none")})`
|
|
3175
|
+
};
|
|
3176
|
+
}
|
|
3177
|
+
if (!snapshot.heartbeat?.healthy) {
|
|
3178
|
+
return {
|
|
3179
|
+
name: "daemon",
|
|
3180
|
+
status: "fail",
|
|
3181
|
+
detail: `heartbeat unhealthy${snapshot.heartbeat?.lastError ? ` \u2014 ${snapshot.heartbeat.lastError}` : ""}`
|
|
3182
|
+
};
|
|
3183
|
+
}
|
|
3184
|
+
if (!snapshot.ingestion?.healthy) {
|
|
3185
|
+
const pending = snapshot.ingestion?.pendingCount ?? 0;
|
|
3186
|
+
const pendingLabel = snapshot.ingestion?.pendingSampled ? `at least ${String(pending)}` : String(pending);
|
|
3187
|
+
return {
|
|
3188
|
+
name: "daemon",
|
|
3189
|
+
status: "fail",
|
|
3190
|
+
detail: `ingestion unhealthy \xB7 ${pendingLabel} pending${snapshot.ingestion?.lastError ? ` \u2014 ${snapshot.ingestion.lastError}` : ""}`
|
|
3191
|
+
};
|
|
3192
|
+
}
|
|
3193
|
+
if (snapshot.healthy !== true) {
|
|
3194
|
+
return { name: "daemon", status: "fail", detail: "health state is not ready" };
|
|
3195
|
+
}
|
|
3196
|
+
return {
|
|
2170
3197
|
name: "daemon",
|
|
2171
|
-
status: "
|
|
2172
|
-
detail:
|
|
3198
|
+
status: "ok",
|
|
3199
|
+
detail: `supervised and healthy${snapshot.version ? ` \xB7 v${snapshot.version}` : ""}`
|
|
2173
3200
|
};
|
|
2174
3201
|
}
|
|
3202
|
+
async function checkDaemon() {
|
|
3203
|
+
let service;
|
|
3204
|
+
if (process.platform === "darwin") {
|
|
3205
|
+
try {
|
|
3206
|
+
service = getLaunchdService();
|
|
3207
|
+
} catch {
|
|
3208
|
+
service = { loaded: false };
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
const snapshot = await daemonRequest(
|
|
3212
|
+
"status_snapshot",
|
|
3213
|
+
{},
|
|
3214
|
+
{ timeoutMs: DAEMON_PROBE_TIMEOUT_MS }
|
|
3215
|
+
);
|
|
3216
|
+
return classifyDaemonHealth(snapshot, {
|
|
3217
|
+
disabled: daemonExplicitlyDisabled(),
|
|
3218
|
+
service
|
|
3219
|
+
});
|
|
3220
|
+
}
|
|
2175
3221
|
function checkJournal() {
|
|
2176
|
-
const stats =
|
|
2177
|
-
const pending = stats.
|
|
3222
|
+
const stats = pendingJournalStats();
|
|
3223
|
+
const pending = stats.pendingCount;
|
|
3224
|
+
const pendingLabel = stats.sampled ? `at least ${String(pending)}` : String(pending);
|
|
2178
3225
|
if (pending === 0) {
|
|
3226
|
+
if (stats.sampled) {
|
|
3227
|
+
return {
|
|
3228
|
+
name: "journal",
|
|
3229
|
+
status: "fail",
|
|
3230
|
+
detail: "bounded journal sample could not prove the queue is empty"
|
|
3231
|
+
};
|
|
3232
|
+
}
|
|
2179
3233
|
return { name: "journal", status: "ok", detail: "no pending moves" };
|
|
2180
3234
|
}
|
|
2181
|
-
|
|
3235
|
+
if (stats.oldestPendingAt === void 0) {
|
|
3236
|
+
return {
|
|
3237
|
+
name: "journal",
|
|
3238
|
+
status: "fail",
|
|
3239
|
+
detail: `${pendingLabel} pending with no readable capture timestamp`
|
|
3240
|
+
};
|
|
3241
|
+
}
|
|
3242
|
+
const oldestMs = Date.now() - stats.oldestPendingAt;
|
|
2182
3243
|
const oldestS = Math.round(oldestMs / MS_PER_SECOND);
|
|
2183
3244
|
if (oldestMs > STALE_PENDING_MS) {
|
|
2184
3245
|
return {
|
|
2185
3246
|
name: "journal",
|
|
2186
|
-
status: "
|
|
2187
|
-
detail: `${
|
|
3247
|
+
status: "fail",
|
|
3248
|
+
detail: `${pendingLabel} pending, oldest observed ${String(oldestS)}s \u2014 30s delivery SLA missed`
|
|
3249
|
+
};
|
|
3250
|
+
}
|
|
3251
|
+
if (stats.sampled) {
|
|
3252
|
+
return {
|
|
3253
|
+
name: "journal",
|
|
3254
|
+
status: "fail",
|
|
3255
|
+
detail: `${pendingLabel} pending in bounded sample; 30s delivery SLA cannot be proven`
|
|
2188
3256
|
};
|
|
2189
3257
|
}
|
|
2190
3258
|
return { name: "journal", status: "ok", detail: `${String(pending)} pending, draining` };
|
|
2191
3259
|
}
|
|
2192
3260
|
function checkStranded() {
|
|
2193
|
-
const
|
|
2194
|
-
if (
|
|
3261
|
+
const stats = pendingJournalStats();
|
|
3262
|
+
if (stats.strandedFileCount === 0 && !stats.strandedSampled) {
|
|
2195
3263
|
return { name: "stranded", status: "ok", detail: "none" };
|
|
2196
3264
|
}
|
|
2197
|
-
const
|
|
3265
|
+
const qualifier = stats.strandedSampled ? "at least " : "";
|
|
2198
3266
|
return {
|
|
2199
3267
|
name: "stranded",
|
|
2200
3268
|
status: "warn",
|
|
2201
|
-
detail: `${String(
|
|
3269
|
+
detail: `${qualifier}${String(stats.strandedCount)} move(s) in ${qualifier}${String(stats.strandedFileCount)} file(s) \u2014 run \`prim moves flush\``
|
|
3270
|
+
};
|
|
3271
|
+
}
|
|
3272
|
+
function checkWorkspaceIdentity() {
|
|
3273
|
+
const identity = inspectWorkspaceId();
|
|
3274
|
+
switch (identity.status) {
|
|
3275
|
+
case "ready":
|
|
3276
|
+
return { name: "feedback-id", status: "ok", detail: "stable worktree identity ready" };
|
|
3277
|
+
case "missing":
|
|
3278
|
+
return {
|
|
3279
|
+
name: "feedback-id",
|
|
3280
|
+
status: "warn",
|
|
3281
|
+
detail: "not initialized \u2014 the next active hook will create it"
|
|
3282
|
+
};
|
|
3283
|
+
case "not_git":
|
|
3284
|
+
return {
|
|
3285
|
+
name: "feedback-id",
|
|
3286
|
+
status: "warn",
|
|
3287
|
+
detail: "not in a Git worktree \u2014 capture falls back to legacy V1"
|
|
3288
|
+
};
|
|
3289
|
+
case "corrupt":
|
|
3290
|
+
return {
|
|
3291
|
+
name: "feedback-id",
|
|
3292
|
+
status: "warn",
|
|
3293
|
+
detail: "identity is corrupt \u2014 not rotated; capture falls back to legacy V1"
|
|
3294
|
+
};
|
|
3295
|
+
case "unavailable":
|
|
3296
|
+
return {
|
|
3297
|
+
name: "feedback-id",
|
|
3298
|
+
status: "warn",
|
|
3299
|
+
detail: `identity unavailable during ${identity.operation} \u2014 capture falls back to legacy V1`
|
|
3300
|
+
};
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
function checkFeedbackHooks() {
|
|
3304
|
+
try {
|
|
3305
|
+
const status = performStatus();
|
|
3306
|
+
if (status.project.feedback || status.user.feedback) {
|
|
3307
|
+
return {
|
|
3308
|
+
name: "feedback-hooks",
|
|
3309
|
+
status: "ok",
|
|
3310
|
+
detail: status.project.feedback ? "project handlers ready" : "user handlers ready"
|
|
3311
|
+
};
|
|
3312
|
+
}
|
|
3313
|
+
return {
|
|
3314
|
+
name: "feedback-hooks",
|
|
3315
|
+
status: "warn",
|
|
3316
|
+
detail: "Stop + SessionStart handlers missing \u2014 run `prim claude install`"
|
|
3317
|
+
};
|
|
3318
|
+
} catch (error) {
|
|
3319
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3320
|
+
return { name: "feedback-hooks", status: "warn", detail: message.slice(0, 80) };
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
function parseMovesStatus(value) {
|
|
3324
|
+
if (!value || typeof value !== "object") {
|
|
3325
|
+
throw new Error("moves status returned a non-object response");
|
|
3326
|
+
}
|
|
3327
|
+
const status = value;
|
|
3328
|
+
const nullableNumber = (candidate) => candidate === null || typeof candidate === "number" && Number.isFinite(candidate);
|
|
3329
|
+
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") {
|
|
3330
|
+
throw new Error("moves status returned an invalid response");
|
|
3331
|
+
}
|
|
3332
|
+
return status;
|
|
3333
|
+
}
|
|
3334
|
+
function classifyMovesStatus(status) {
|
|
3335
|
+
const capture = status.captureState === "enabled" ? { name: "capture", status: "ok", detail: "enabled; ingest endpoint durable" } : {
|
|
3336
|
+
name: "capture",
|
|
3337
|
+
status: "fail",
|
|
3338
|
+
detail: "disabled for the current organization; local Moves are retained"
|
|
2202
3339
|
};
|
|
3340
|
+
let classification;
|
|
3341
|
+
if (status.pendingSessionCount > 0) {
|
|
3342
|
+
classification = {
|
|
3343
|
+
name: "classification",
|
|
3344
|
+
status: "warn",
|
|
3345
|
+
detail: `${String(status.pendingSessionCount)} session(s) pending${status.sampled ? " in a bounded sample" : ""}`
|
|
3346
|
+
};
|
|
3347
|
+
} else if (status.sampled) {
|
|
3348
|
+
classification = {
|
|
3349
|
+
name: "classification",
|
|
3350
|
+
status: "warn",
|
|
3351
|
+
detail: "caught up in the bounded sample; older sessions were not inspected"
|
|
3352
|
+
};
|
|
3353
|
+
} else {
|
|
3354
|
+
classification = {
|
|
3355
|
+
name: "classification",
|
|
3356
|
+
status: "ok",
|
|
3357
|
+
detail: status.latestClassificationAt === null ? "no sessions awaiting classification" : `caught up \xB7 last ${new Date(status.latestClassificationAt).toISOString()}`
|
|
3358
|
+
};
|
|
3359
|
+
}
|
|
3360
|
+
return [capture, classification];
|
|
2203
3361
|
}
|
|
2204
|
-
async function
|
|
3362
|
+
async function checkBackend() {
|
|
2205
3363
|
try {
|
|
2206
|
-
await getClient().get("/api/cli/
|
|
3364
|
+
const response = await getClient().get("/api/cli/moves/status", {
|
|
2207
3365
|
signal: AbortSignal.timeout(CONNECTIVITY_TIMEOUT_MS)
|
|
2208
3366
|
});
|
|
2209
|
-
|
|
3367
|
+
const status = parseMovesStatus(response);
|
|
3368
|
+
return [
|
|
3369
|
+
{ name: "connectivity", status: "ok", detail: "server reachable and authenticated" },
|
|
3370
|
+
...classifyMovesStatus(status)
|
|
3371
|
+
];
|
|
2210
3372
|
} catch (err) {
|
|
2211
3373
|
const message = err instanceof Error ? err.message : String(err);
|
|
2212
|
-
|
|
2213
|
-
|
|
3374
|
+
return [{ name: "connectivity", status: "fail", detail: message.slice(0, 120) }];
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
async function checkFeedbackCapability() {
|
|
3378
|
+
try {
|
|
3379
|
+
const capability = await fetchFeedbackCapability(AbortSignal.timeout(CONNECTIVITY_TIMEOUT_MS));
|
|
3380
|
+
return capability.status === "available" ? { name: "feedback-api", status: "ok", detail: "server supports decision feedback" } : {
|
|
3381
|
+
name: "feedback-api",
|
|
3382
|
+
status: "warn",
|
|
3383
|
+
detail: "available after binding this CLI to an organization"
|
|
3384
|
+
};
|
|
3385
|
+
} catch (error) {
|
|
3386
|
+
if (error instanceof HttpError && error.status === 404) {
|
|
3387
|
+
return {
|
|
3388
|
+
name: "feedback-api",
|
|
3389
|
+
status: "warn",
|
|
3390
|
+
detail: "server does not support decision feedback yet"
|
|
3391
|
+
};
|
|
3392
|
+
}
|
|
3393
|
+
if (error instanceof HttpError && error.status === 401) {
|
|
3394
|
+
return { name: "feedback-api", status: "fail", detail: error.message.slice(0, 80) };
|
|
3395
|
+
}
|
|
3396
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3397
|
+
return { name: "feedback-api", status: "warn", detail: message.slice(0, 80) };
|
|
2214
3398
|
}
|
|
2215
3399
|
}
|
|
2216
3400
|
async function collectChecks() {
|
|
3401
|
+
const backend = await checkBackend();
|
|
2217
3402
|
return [
|
|
2218
3403
|
checkAuth(),
|
|
2219
3404
|
await checkDaemon(),
|
|
2220
3405
|
checkJournal(),
|
|
2221
3406
|
checkStranded(),
|
|
2222
|
-
|
|
3407
|
+
checkFeedbackHooks(),
|
|
3408
|
+
checkWorkspaceIdentity(),
|
|
3409
|
+
...backend,
|
|
3410
|
+
await checkFeedbackCapability()
|
|
2223
3411
|
];
|
|
2224
3412
|
}
|
|
2225
3413
|
function icon(status) {
|
|
@@ -2241,26 +3429,28 @@ async function runDoctor() {
|
|
|
2241
3429
|
}
|
|
2242
3430
|
}
|
|
2243
3431
|
function registerDoctorCommands(program2) {
|
|
2244
|
-
program2.command("doctor").description(
|
|
3432
|
+
program2.command("doctor").description(
|
|
3433
|
+
"Check capture and feedback health end to end (auth, supervisor, delivery, worktree, server)"
|
|
3434
|
+
).action(async () => {
|
|
2245
3435
|
await runDoctor();
|
|
2246
3436
|
});
|
|
2247
3437
|
}
|
|
2248
3438
|
|
|
2249
3439
|
// src/commands/hermes-install.ts
|
|
2250
3440
|
import {
|
|
2251
|
-
chmodSync,
|
|
3441
|
+
chmodSync as chmodSync2,
|
|
2252
3442
|
closeSync as closeSync3,
|
|
2253
|
-
existsSync as
|
|
3443
|
+
existsSync as existsSync6,
|
|
2254
3444
|
fsyncSync as fsyncSync2,
|
|
2255
|
-
mkdirSync as
|
|
3445
|
+
mkdirSync as mkdirSync5,
|
|
2256
3446
|
openSync as openSync3,
|
|
2257
|
-
readFileSync as
|
|
2258
|
-
renameSync as
|
|
2259
|
-
rmSync as
|
|
2260
|
-
writeFileSync as
|
|
3447
|
+
readFileSync as readFileSync5,
|
|
3448
|
+
renameSync as renameSync3,
|
|
3449
|
+
rmSync as rmSync3,
|
|
3450
|
+
writeFileSync as writeFileSync4
|
|
2261
3451
|
} from "fs";
|
|
2262
|
-
import { homedir as
|
|
2263
|
-
import { dirname as
|
|
3452
|
+
import { homedir as homedir5 } from "os";
|
|
3453
|
+
import { dirname as dirname4, join as join5 } from "path";
|
|
2264
3454
|
import { Document, parseDocument, stringify } from "yaml";
|
|
2265
3455
|
var CAPTURE_BIN3 = "prim-hook";
|
|
2266
3456
|
var GATE_BIN3 = "prim-pre-tool-use";
|
|
@@ -2279,13 +3469,13 @@ var PRIM_BINS3 = [
|
|
|
2279
3469
|
SESSION_END_BIN2
|
|
2280
3470
|
];
|
|
2281
3471
|
function hermesHome() {
|
|
2282
|
-
return process.env.HERMES_HOME ??
|
|
3472
|
+
return process.env.HERMES_HOME ?? join5(homedir5(), ".hermes");
|
|
2283
3473
|
}
|
|
2284
3474
|
function configPath() {
|
|
2285
|
-
return
|
|
3475
|
+
return join5(hermesHome(), "config.yaml");
|
|
2286
3476
|
}
|
|
2287
3477
|
function shimPath() {
|
|
2288
|
-
return
|
|
3478
|
+
return join5(hermesHome(), "agent-hooks", "prim-shim.sh");
|
|
2289
3479
|
}
|
|
2290
3480
|
var SHIM_SCRIPT = `#!/bin/sh
|
|
2291
3481
|
# prim Hermes hook shim \u2014 managed by \`prim hermes install\`. Hermes runs hooks
|
|
@@ -2374,10 +3564,10 @@ function isCaptureInstalled(hooks) {
|
|
|
2374
3564
|
);
|
|
2375
3565
|
}
|
|
2376
3566
|
function readDoc(path) {
|
|
2377
|
-
if (!
|
|
3567
|
+
if (!existsSync6(path)) {
|
|
2378
3568
|
return new Document();
|
|
2379
3569
|
}
|
|
2380
|
-
const raw =
|
|
3570
|
+
const raw = readFileSync5(path, "utf-8");
|
|
2381
3571
|
return raw.trim().length === 0 ? new Document() : parseDocument(raw);
|
|
2382
3572
|
}
|
|
2383
3573
|
function readHooks(doc) {
|
|
@@ -2449,19 +3639,19 @@ function setAutoAccept(raw) {
|
|
|
2449
3639
|
`;
|
|
2450
3640
|
}
|
|
2451
3641
|
function atomicWriteFile(path, content) {
|
|
2452
|
-
const dir =
|
|
2453
|
-
if (!
|
|
2454
|
-
|
|
3642
|
+
const dir = dirname4(path);
|
|
3643
|
+
if (!existsSync6(dir)) {
|
|
3644
|
+
mkdirSync5(dir, { recursive: true });
|
|
2455
3645
|
}
|
|
2456
3646
|
const tmp = `${path}.tmp.${String(process.pid)}`;
|
|
2457
|
-
|
|
3647
|
+
writeFileSync4(tmp, content, "utf-8");
|
|
2458
3648
|
const fd = openSync3(tmp, "r+");
|
|
2459
3649
|
try {
|
|
2460
3650
|
fsyncSync2(fd);
|
|
2461
3651
|
} finally {
|
|
2462
3652
|
closeSync3(fd);
|
|
2463
3653
|
}
|
|
2464
|
-
|
|
3654
|
+
renameSync3(tmp, path);
|
|
2465
3655
|
}
|
|
2466
3656
|
function mergeKeepsYamlValid(before, after) {
|
|
2467
3657
|
return parseDocument(after).errors.length <= parseDocument(before).errors.length;
|
|
@@ -2474,15 +3664,15 @@ function assertMergeValid(before, after) {
|
|
|
2474
3664
|
}
|
|
2475
3665
|
function writeShim() {
|
|
2476
3666
|
const path = shimPath();
|
|
2477
|
-
const dir =
|
|
2478
|
-
if (!
|
|
2479
|
-
|
|
3667
|
+
const dir = dirname4(path);
|
|
3668
|
+
if (!existsSync6(dir)) {
|
|
3669
|
+
mkdirSync5(dir, { recursive: true });
|
|
2480
3670
|
}
|
|
2481
|
-
|
|
2482
|
-
|
|
3671
|
+
writeFileSync4(path, SHIM_SCRIPT, "utf-8");
|
|
3672
|
+
chmodSync2(path, SHIM_MODE);
|
|
2483
3673
|
}
|
|
2484
3674
|
function removeShim() {
|
|
2485
|
-
|
|
3675
|
+
rmSync3(shimPath(), { force: true });
|
|
2486
3676
|
}
|
|
2487
3677
|
function autoAcceptOf(raw) {
|
|
2488
3678
|
if (raw.trim().length === 0) {
|
|
@@ -2495,7 +3685,7 @@ function readHooksFromRaw(raw) {
|
|
|
2495
3685
|
}
|
|
2496
3686
|
function performInstall3(opts) {
|
|
2497
3687
|
const path = configPath();
|
|
2498
|
-
const raw =
|
|
3688
|
+
const raw = existsSync6(path) ? readFileSync5(path, "utf-8") : "";
|
|
2499
3689
|
const existing = readHooksFromRaw(raw);
|
|
2500
3690
|
const desired = applyInstall3(existing, opts.force);
|
|
2501
3691
|
let next = raw;
|
|
@@ -2521,11 +3711,11 @@ function performInstall3(opts) {
|
|
|
2521
3711
|
}
|
|
2522
3712
|
function performUninstall3() {
|
|
2523
3713
|
const path = configPath();
|
|
2524
|
-
if (!
|
|
3714
|
+
if (!existsSync6(path)) {
|
|
2525
3715
|
removeShim();
|
|
2526
3716
|
return { path, gate: false, capture: false, autoAccept: false, changed: false };
|
|
2527
3717
|
}
|
|
2528
|
-
const raw =
|
|
3718
|
+
const raw = readFileSync5(path, "utf-8");
|
|
2529
3719
|
const existing = readHooksFromRaw(raw);
|
|
2530
3720
|
const remaining = applyUninstall3(existing);
|
|
2531
3721
|
const next = JSON.stringify(existing) === JSON.stringify(remaining) ? raw : spliceHooks(raw, remaining);
|
|
@@ -2545,7 +3735,7 @@ function performUninstall3() {
|
|
|
2545
3735
|
}
|
|
2546
3736
|
function performStatus3() {
|
|
2547
3737
|
const path = configPath();
|
|
2548
|
-
const hooks =
|
|
3738
|
+
const hooks = existsSync6(path) ? readHooks(readDoc(path)) : {};
|
|
2549
3739
|
return { path, gate: isGateInstalled3(hooks), capture: isCaptureInstalled(hooks) };
|
|
2550
3740
|
}
|
|
2551
3741
|
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.";
|
|
@@ -2590,9 +3780,9 @@ function registerHermesCommands(program2) {
|
|
|
2590
3780
|
|
|
2591
3781
|
// src/commands/hooks.ts
|
|
2592
3782
|
import { execFileSync } from "child_process";
|
|
2593
|
-
import { existsSync as
|
|
2594
|
-
import { homedir as
|
|
2595
|
-
import { dirname as
|
|
3783
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
3784
|
+
import { homedir as homedir6 } from "os";
|
|
3785
|
+
import { dirname as dirname5, join as join6, resolve as resolve2 } from "path";
|
|
2596
3786
|
import { Option } from "commander";
|
|
2597
3787
|
var PRE_COMMIT = { hookName: "pre-commit", binName: "prim-pre-commit" };
|
|
2598
3788
|
var POST_COMMIT = { hookName: "post-commit", binName: "prim-post-commit" };
|
|
@@ -2641,16 +3831,16 @@ ${gatedShim(spec.binName)}
|
|
|
2641
3831
|
${end}`;
|
|
2642
3832
|
}
|
|
2643
3833
|
function mergePrimBlock(hookPath, block, binName) {
|
|
2644
|
-
if (
|
|
2645
|
-
const existing =
|
|
3834
|
+
if (existsSync7(hookPath)) {
|
|
3835
|
+
const existing = readFileSync6(hookPath, "utf-8");
|
|
2646
3836
|
if (containsPrimHook(existing, binName)) return false;
|
|
2647
3837
|
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
2648
|
-
|
|
3838
|
+
writeFileSync5(hookPath, `${existing}${separator}${block}
|
|
2649
3839
|
`, { mode: 493 });
|
|
2650
3840
|
return true;
|
|
2651
3841
|
}
|
|
2652
|
-
|
|
2653
|
-
|
|
3842
|
+
mkdirSync6(dirname5(hookPath), { recursive: true });
|
|
3843
|
+
writeFileSync5(hookPath, `#!/bin/sh
|
|
2654
3844
|
# ${PRIM_CREATED_MARK}
|
|
2655
3845
|
|
|
2656
3846
|
${block}
|
|
@@ -2665,14 +3855,14 @@ function getGitRoot() {
|
|
|
2665
3855
|
return root;
|
|
2666
3856
|
}
|
|
2667
3857
|
function detectHusky(gitRoot) {
|
|
2668
|
-
const huskyDir =
|
|
2669
|
-
if (!
|
|
2670
|
-
if (
|
|
2671
|
-
if (
|
|
2672
|
-
const pkgPath =
|
|
2673
|
-
if (
|
|
3858
|
+
const huskyDir = resolve2(gitRoot, ".husky");
|
|
3859
|
+
if (!existsSync7(huskyDir)) return false;
|
|
3860
|
+
if (existsSync7(resolve2(huskyDir, "_"))) return true;
|
|
3861
|
+
if (existsSync7(resolve2(huskyDir, "pre-commit"))) return true;
|
|
3862
|
+
const pkgPath = resolve2(gitRoot, "package.json");
|
|
3863
|
+
if (existsSync7(pkgPath)) {
|
|
2674
3864
|
try {
|
|
2675
|
-
const pkg2 = JSON.parse(
|
|
3865
|
+
const pkg2 = JSON.parse(readFileSync6(pkgPath, "utf-8"));
|
|
2676
3866
|
const scripts = pkg2.scripts ?? {};
|
|
2677
3867
|
if (/husky/i.test(scripts.prepare ?? "") || /husky/i.test(scripts.postinstall ?? "")) {
|
|
2678
3868
|
return true;
|
|
@@ -2687,8 +3877,8 @@ function containsPrimHook(content, binName = PRE_COMMIT.binName) {
|
|
|
2687
3877
|
}
|
|
2688
3878
|
async function askConfirmation(question) {
|
|
2689
3879
|
if (!process.stdin.isTTY) return false;
|
|
2690
|
-
const { createInterface } = await import("readline/promises");
|
|
2691
|
-
const rl =
|
|
3880
|
+
const { createInterface: createInterface2 } = await import("readline/promises");
|
|
3881
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
2692
3882
|
try {
|
|
2693
3883
|
const answer = await rl.question(`${question} [y/N] `);
|
|
2694
3884
|
const normalized = answer.trim().toLowerCase();
|
|
@@ -2698,8 +3888,8 @@ async function askConfirmation(question) {
|
|
|
2698
3888
|
}
|
|
2699
3889
|
}
|
|
2700
3890
|
function installToHusky(gitRoot, spec = PRE_COMMIT) {
|
|
2701
|
-
const hookPath =
|
|
2702
|
-
const existed =
|
|
3891
|
+
const hookPath = resolve2(gitRoot, ".husky", spec.hookName);
|
|
3892
|
+
const existed = existsSync7(hookPath);
|
|
2703
3893
|
const wrote = mergePrimBlock(hookPath, huskyBlock(spec), spec.binName);
|
|
2704
3894
|
if (!wrote) {
|
|
2705
3895
|
console.log(`Prim ${spec.hookName} hook is already installed in .husky/${spec.hookName}.`);
|
|
@@ -2710,13 +3900,13 @@ function installToHusky(gitRoot, spec = PRE_COMMIT) {
|
|
|
2710
3900
|
}
|
|
2711
3901
|
}
|
|
2712
3902
|
function installToDotGit(gitRoot, spec = PRE_COMMIT) {
|
|
2713
|
-
const hooksDir =
|
|
2714
|
-
const hookPath =
|
|
2715
|
-
if (!
|
|
2716
|
-
|
|
3903
|
+
const hooksDir = resolve2(gitRoot, ".git", "hooks");
|
|
3904
|
+
const hookPath = resolve2(hooksDir, spec.hookName);
|
|
3905
|
+
if (!existsSync7(hooksDir)) {
|
|
3906
|
+
mkdirSync6(hooksDir, { recursive: true });
|
|
2717
3907
|
}
|
|
2718
|
-
if (
|
|
2719
|
-
const existing =
|
|
3908
|
+
if (existsSync7(hookPath)) {
|
|
3909
|
+
const existing = readFileSync6(hookPath, "utf-8");
|
|
2720
3910
|
if (containsPrimHook(existing, spec.binName)) {
|
|
2721
3911
|
console.log(`Prim ${spec.hookName} hook is already installed at ${hookPath}.`);
|
|
2722
3912
|
return;
|
|
@@ -2725,12 +3915,12 @@ function installToDotGit(gitRoot, spec = PRE_COMMIT) {
|
|
|
2725
3915
|
console.log("To replace it, run: prim hooks uninstall && prim hooks install");
|
|
2726
3916
|
return;
|
|
2727
3917
|
}
|
|
2728
|
-
|
|
3918
|
+
writeFileSync5(hookPath, dotGitScript(spec), { mode: 493 });
|
|
2729
3919
|
console.log(`Installed ${spec.hookName} hook at ${hookPath}`);
|
|
2730
3920
|
}
|
|
2731
|
-
var PRIM_GIT_HOOKS_DIR =
|
|
3921
|
+
var PRIM_GIT_HOOKS_DIR = join6(homedir6(), ".config", "prim", "git-hooks");
|
|
2732
3922
|
function expandTilde(p) {
|
|
2733
|
-
return p.startsWith("~/") ?
|
|
3923
|
+
return p.startsWith("~/") ? join6(homedir6(), p.slice(2)) : p;
|
|
2734
3924
|
}
|
|
2735
3925
|
function isOurHooksDir(value) {
|
|
2736
3926
|
return value !== "" && expandTilde(value) === PRIM_GIT_HOOKS_DIR;
|
|
@@ -2795,24 +3985,24 @@ function ownedHookNames() {
|
|
|
2795
3985
|
return [...HOOKS.map((s) => s.hookName), ...PASSTHROUGH_HOOKS];
|
|
2796
3986
|
}
|
|
2797
3987
|
function writeOwnHooks() {
|
|
2798
|
-
if (!
|
|
2799
|
-
|
|
3988
|
+
if (!existsSync7(PRIM_GIT_HOOKS_DIR)) {
|
|
3989
|
+
mkdirSync6(PRIM_GIT_HOOKS_DIR, { recursive: true });
|
|
2800
3990
|
}
|
|
2801
3991
|
for (const spec of HOOKS) {
|
|
2802
|
-
|
|
3992
|
+
writeFileSync5(resolve2(PRIM_GIT_HOOKS_DIR, spec.hookName), globalHookScript(spec), {
|
|
2803
3993
|
mode: 493
|
|
2804
3994
|
});
|
|
2805
3995
|
}
|
|
2806
3996
|
for (const name of PASSTHROUGH_HOOKS) {
|
|
2807
|
-
|
|
3997
|
+
writeFileSync5(resolve2(PRIM_GIT_HOOKS_DIR, name), passThroughScript(name), { mode: 493 });
|
|
2808
3998
|
}
|
|
2809
3999
|
}
|
|
2810
4000
|
function appendPrimBlock(hookPath, spec) {
|
|
2811
4001
|
mergePrimBlock(hookPath, gatedBlock(spec), spec.binName);
|
|
2812
4002
|
}
|
|
2813
4003
|
function stripPrimBlock(hookPath, spec) {
|
|
2814
|
-
if (!
|
|
2815
|
-
const existing =
|
|
4004
|
+
if (!existsSync7(hookPath)) return;
|
|
4005
|
+
const existing = readFileSync6(hookPath, "utf-8");
|
|
2816
4006
|
const primCreated = existing.includes(PRIM_CREATED_MARK);
|
|
2817
4007
|
const { start, end } = blockMarkers(spec);
|
|
2818
4008
|
const s = existing.indexOf(start);
|
|
@@ -2824,7 +4014,7 @@ function stripPrimBlock(hookPath, spec) {
|
|
|
2824
4014
|
unlinkSync2(hookPath);
|
|
2825
4015
|
return;
|
|
2826
4016
|
}
|
|
2827
|
-
|
|
4017
|
+
writeFileSync5(hookPath, out, { mode: 493 });
|
|
2828
4018
|
}
|
|
2829
4019
|
function installGlobalHooks(opts = {}) {
|
|
2830
4020
|
const global = gitConfigGet("--global");
|
|
@@ -2855,7 +4045,7 @@ function installGlobalHooks(opts = {}) {
|
|
|
2855
4045
|
}
|
|
2856
4046
|
const dir = expandTilde(global);
|
|
2857
4047
|
for (const spec of HOOKS) {
|
|
2858
|
-
appendPrimBlock(
|
|
4048
|
+
appendPrimBlock(resolve2(dir, spec.hookName), spec);
|
|
2859
4049
|
}
|
|
2860
4050
|
console.log(
|
|
2861
4051
|
`Appended prim hooks into existing core.hooksPath dir ${global} (pointer unchanged).`
|
|
@@ -2866,8 +4056,8 @@ function uninstallGlobalHooks() {
|
|
|
2866
4056
|
const global = gitConfigGet("--global");
|
|
2867
4057
|
if (isOurHooksDir(global)) {
|
|
2868
4058
|
for (const name of ownedHookNames()) {
|
|
2869
|
-
const p =
|
|
2870
|
-
if (
|
|
4059
|
+
const p = resolve2(PRIM_GIT_HOOKS_DIR, name);
|
|
4060
|
+
if (existsSync7(p)) unlinkSync2(p);
|
|
2871
4061
|
}
|
|
2872
4062
|
execFileSync("git", ["config", "--global", "--unset", "core.hooksPath"]);
|
|
2873
4063
|
console.log("Removed prim global git hooks and unset core.hooksPath.");
|
|
@@ -2876,7 +4066,7 @@ function uninstallGlobalHooks() {
|
|
|
2876
4066
|
if (global !== "") {
|
|
2877
4067
|
const dir = expandTilde(global);
|
|
2878
4068
|
for (const spec of HOOKS) {
|
|
2879
|
-
stripPrimBlock(
|
|
4069
|
+
stripPrimBlock(resolve2(dir, spec.hookName), spec);
|
|
2880
4070
|
}
|
|
2881
4071
|
console.log(`Removed the prim block from ${global} (left the dir and core.hooksPath intact).`);
|
|
2882
4072
|
return;
|
|
@@ -2956,12 +4146,12 @@ function registerHooksCommands(program2) {
|
|
|
2956
4146
|
}
|
|
2957
4147
|
const gitRoot = getGitRoot();
|
|
2958
4148
|
for (const spec of HOOKS) {
|
|
2959
|
-
const hookPath =
|
|
2960
|
-
if (!
|
|
4149
|
+
const hookPath = resolve2(gitRoot, ".git", "hooks", spec.hookName);
|
|
4150
|
+
if (!existsSync7(hookPath)) {
|
|
2961
4151
|
console.log(`No ${spec.hookName} hook found.`);
|
|
2962
4152
|
continue;
|
|
2963
4153
|
}
|
|
2964
|
-
if (containsPrimHook(
|
|
4154
|
+
if (containsPrimHook(readFileSync6(hookPath, "utf-8"), spec.binName)) {
|
|
2965
4155
|
unlinkSync2(hookPath);
|
|
2966
4156
|
console.log(`Removed ${spec.hookName} hook at ${hookPath}`);
|
|
2967
4157
|
} else {
|
|
@@ -2972,43 +4162,60 @@ function registerHooksCommands(program2) {
|
|
|
2972
4162
|
}
|
|
2973
4163
|
|
|
2974
4164
|
// src/commands/moves.ts
|
|
2975
|
-
import { existsSync as
|
|
2976
|
-
import { join as
|
|
4165
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync7, unlinkSync as unlinkSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
4166
|
+
import { join as join7 } from "path";
|
|
2977
4167
|
|
|
2978
4168
|
// src/flusher.ts
|
|
2979
|
-
import { renameSync as
|
|
4169
|
+
import { createReadStream, renameSync as renameSync4, unlinkSync as unlinkSync3 } from "fs";
|
|
4170
|
+
import { createInterface } from "readline";
|
|
2980
4171
|
var BATCH_SIZE = 500;
|
|
2981
4172
|
var HTTP_TIMEOUT_MS = 1e4;
|
|
2982
4173
|
var OPPORTUNISTIC_FLUSH_AFTER_MS = 6e4;
|
|
2983
4174
|
var ORPHAN_QUARANTINE_MS = 6e4;
|
|
2984
|
-
function
|
|
2985
|
-
const
|
|
2986
|
-
|
|
2987
|
-
batches.push(moves.slice(i, i + size));
|
|
2988
|
-
}
|
|
2989
|
-
return batches;
|
|
2990
|
-
}
|
|
2991
|
-
async function drainFlushingPath(flushingPath) {
|
|
2992
|
-
const moves = readMovesFromPath(flushingPath);
|
|
2993
|
-
if (moves.length === 0) {
|
|
2994
|
-
unlinkSync3(flushingPath);
|
|
2995
|
-
return 0;
|
|
2996
|
-
}
|
|
2997
|
-
const client = getClient();
|
|
2998
|
-
for (const batch of batchMoves(moves)) {
|
|
2999
|
-
await client.post(
|
|
4175
|
+
async function drainFlushingPath(flushingPath, client = getClient()) {
|
|
4176
|
+
const postBatch = async (batch2) => {
|
|
4177
|
+
const response = await client.post(
|
|
3000
4178
|
"/api/cli/moves/ingest",
|
|
3001
|
-
{ batch },
|
|
4179
|
+
{ batch: batch2 },
|
|
3002
4180
|
{ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS) }
|
|
3003
4181
|
);
|
|
4182
|
+
requireDurableIngestAcknowledgement(response, batch2.length);
|
|
4183
|
+
};
|
|
4184
|
+
const input = createReadStream(flushingPath, { encoding: "utf-8" });
|
|
4185
|
+
const lines = createInterface({ input, crlfDelay: Number.POSITIVE_INFINITY });
|
|
4186
|
+
let batch = [];
|
|
4187
|
+
let total = 0;
|
|
4188
|
+
try {
|
|
4189
|
+
for await (const line of lines) {
|
|
4190
|
+
if (line.length === 0) {
|
|
4191
|
+
continue;
|
|
4192
|
+
}
|
|
4193
|
+
try {
|
|
4194
|
+
batch.push(JSON.parse(line));
|
|
4195
|
+
} catch {
|
|
4196
|
+
continue;
|
|
4197
|
+
}
|
|
4198
|
+
if (batch.length === BATCH_SIZE) {
|
|
4199
|
+
await postBatch(batch);
|
|
4200
|
+
total += batch.length;
|
|
4201
|
+
batch = [];
|
|
4202
|
+
}
|
|
4203
|
+
}
|
|
4204
|
+
if (batch.length > 0) {
|
|
4205
|
+
await postBatch(batch);
|
|
4206
|
+
total += batch.length;
|
|
4207
|
+
}
|
|
4208
|
+
} finally {
|
|
4209
|
+
lines.close();
|
|
4210
|
+
input.destroy();
|
|
3004
4211
|
}
|
|
3005
4212
|
unlinkSync3(flushingPath);
|
|
3006
|
-
return
|
|
4213
|
+
return total;
|
|
3007
4214
|
}
|
|
3008
4215
|
async function drainPath(path) {
|
|
3009
4216
|
const tmpPath = `${path}.flushing.${String(Date.now())}.${String(process.pid)}`;
|
|
3010
4217
|
try {
|
|
3011
|
-
|
|
4218
|
+
renameSync4(path, tmpPath);
|
|
3012
4219
|
} catch (err) {
|
|
3013
4220
|
if (err.code === "ENOENT") {
|
|
3014
4221
|
return 0;
|
|
@@ -3017,7 +4224,7 @@ async function drainPath(path) {
|
|
|
3017
4224
|
}
|
|
3018
4225
|
return drainFlushingPath(tmpPath);
|
|
3019
4226
|
}
|
|
3020
|
-
function
|
|
4227
|
+
function processIsAlive3(pid) {
|
|
3021
4228
|
try {
|
|
3022
4229
|
process.kill(pid, 0);
|
|
3023
4230
|
return true;
|
|
@@ -3027,36 +4234,84 @@ function processIsAlive2(pid) {
|
|
|
3027
4234
|
}
|
|
3028
4235
|
function selectRecoverable(files, now, opts = {}) {
|
|
3029
4236
|
const quarantineMs = opts.quarantineMs ?? ORPHAN_QUARANTINE_MS;
|
|
3030
|
-
const isAlive = opts.isAlive ??
|
|
3031
|
-
return files.filter(
|
|
3032
|
-
(f
|
|
3033
|
-
|
|
4237
|
+
const isAlive = opts.isAlive ?? processIsAlive3;
|
|
4238
|
+
return files.filter((f) => {
|
|
4239
|
+
if (f.pid === void 0) {
|
|
4240
|
+
return now - f.mtimeMs > quarantineMs;
|
|
4241
|
+
}
|
|
4242
|
+
return f.pid === opts.ownerPid || !isAlive(f.pid);
|
|
4243
|
+
});
|
|
3034
4244
|
}
|
|
3035
|
-
async function recoverOrphans() {
|
|
3036
|
-
|
|
3037
|
-
|
|
4245
|
+
async function recoverOrphans(candidates = listFlushing({ sampleBytes: 0 }), options = {}) {
|
|
4246
|
+
const summary = { flushed: 0, errors: [], failedBuckets: /* @__PURE__ */ new Set() };
|
|
4247
|
+
const recoverable = selectRecoverable(candidates, options.now ?? Date.now(), {
|
|
4248
|
+
ownerPid: options.ownerPid ?? process.pid,
|
|
4249
|
+
isAlive: options.isAlive
|
|
4250
|
+
}).sort((a, b) => a.mtimeMs - b.mtimeMs || a.path.localeCompare(b.path));
|
|
4251
|
+
const drain = options.drain ?? drainFlushingPath;
|
|
4252
|
+
for (const file of recoverable) {
|
|
4253
|
+
if (summary.failedBuckets.has(file.bucket)) {
|
|
4254
|
+
continue;
|
|
4255
|
+
}
|
|
3038
4256
|
try {
|
|
3039
|
-
|
|
3040
|
-
} catch {
|
|
4257
|
+
summary.flushed += await drain(file.path);
|
|
4258
|
+
} catch (err) {
|
|
4259
|
+
summary.errors.push(err);
|
|
4260
|
+
summary.failedBuckets.add(file.bucket);
|
|
3041
4261
|
}
|
|
3042
4262
|
}
|
|
3043
|
-
return
|
|
4263
|
+
return summary;
|
|
3044
4264
|
}
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
4265
|
+
var FlushError = class extends Error {
|
|
4266
|
+
flushed;
|
|
4267
|
+
constructor(cause, flushed) {
|
|
4268
|
+
super(cause instanceof Error ? cause.message : String(cause), { cause });
|
|
4269
|
+
this.name = "FlushError";
|
|
4270
|
+
this.flushed = flushed;
|
|
4271
|
+
}
|
|
4272
|
+
};
|
|
4273
|
+
async function flushOnce() {
|
|
4274
|
+
const recovered = await recoverOrphans();
|
|
4275
|
+
let total = recovered.flushed;
|
|
4276
|
+
const errors = recovered.errors;
|
|
4277
|
+
for (const { bucket, path } of listBuckets()) {
|
|
4278
|
+
if (recovered.failedBuckets.has(bucket)) {
|
|
4279
|
+
continue;
|
|
4280
|
+
}
|
|
4281
|
+
try {
|
|
4282
|
+
total += await drainPath(path);
|
|
4283
|
+
} catch (err) {
|
|
4284
|
+
errors.push(err);
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
4287
|
+
if (errors.length > 0) {
|
|
4288
|
+
throw new FlushError(errors[0], total);
|
|
3049
4289
|
}
|
|
3050
4290
|
return { flushed: total };
|
|
3051
4291
|
}
|
|
4292
|
+
var flushInFlight;
|
|
4293
|
+
function flush() {
|
|
4294
|
+
if (flushInFlight) {
|
|
4295
|
+
return flushInFlight;
|
|
4296
|
+
}
|
|
4297
|
+
const attempt = flushOnce().finally(() => {
|
|
4298
|
+
if (flushInFlight === attempt) {
|
|
4299
|
+
flushInFlight = void 0;
|
|
4300
|
+
}
|
|
4301
|
+
});
|
|
4302
|
+
flushInFlight = attempt;
|
|
4303
|
+
return attempt;
|
|
4304
|
+
}
|
|
4305
|
+
function shouldFlushPending(stats, now, thresholdMs = OPPORTUNISTIC_FLUSH_AFTER_MS) {
|
|
4306
|
+
if (stats.sampled) {
|
|
4307
|
+
return true;
|
|
4308
|
+
}
|
|
4309
|
+
return stats.pendingCount > 0 && (stats.oldestPendingAt === void 0 || now - stats.oldestPendingAt > thresholdMs);
|
|
4310
|
+
}
|
|
3052
4311
|
async function flushIfNeeded() {
|
|
3053
4312
|
try {
|
|
3054
|
-
const stats =
|
|
3055
|
-
if (stats.
|
|
3056
|
-
return;
|
|
3057
|
-
}
|
|
3058
|
-
const oldest = stats.reduce((min, s) => s.mtimeMs < min ? s.mtimeMs : min, stats[0].mtimeMs);
|
|
3059
|
-
if (Date.now() - oldest > OPPORTUNISTIC_FLUSH_AFTER_MS) {
|
|
4313
|
+
const stats = pendingJournalStats();
|
|
4314
|
+
if (shouldFlushPending(stats, Date.now())) {
|
|
3060
4315
|
await flush();
|
|
3061
4316
|
}
|
|
3062
4317
|
} catch {
|
|
@@ -3090,21 +4345,25 @@ function registerMovesCommands(program2) {
|
|
|
3090
4345
|
console.log(`[prim] root: ${JOURNAL_DIR}`);
|
|
3091
4346
|
for (const s of stats) {
|
|
3092
4347
|
const ageS = Math.round((Date.now() - s.mtimeMs) / MS_PER_SECOND2);
|
|
4348
|
+
const count = `${String(s.lineCount)}${s.sampled ? "+" : ""}`;
|
|
3093
4349
|
console.log(
|
|
3094
|
-
` ${s.bucket.padEnd(BUCKET_COL_WIDTH)} ${
|
|
4350
|
+
` ${s.bucket.padEnd(BUCKET_COL_WIDTH)} ${count.padStart(5)} pending, ${String(s.sizeBytes).padStart(8)} bytes, last write ${String(ageS)}s ago`
|
|
3095
4351
|
);
|
|
3096
4352
|
}
|
|
3097
4353
|
if (stranded.length > 0) {
|
|
3098
4354
|
const moveCount = stranded.reduce((n, f) => n + f.lineCount, 0);
|
|
3099
4355
|
const byteCount = stranded.reduce((n, f) => n + f.sizeBytes, 0);
|
|
4356
|
+
const sampled = stranded.some((file) => file.sampled);
|
|
4357
|
+
const qualifier = sampled ? "at least " : "";
|
|
3100
4358
|
console.log(
|
|
3101
|
-
`[prim] \u26A0 ${String(stranded.length)} stranded flush file(s): ${String(moveCount)} move(s), ${String(byteCount)} bytes \u2014 recover with \`prim moves flush\``
|
|
4359
|
+
`[prim] \u26A0 ${String(stranded.length)} stranded flush file(s): ${qualifier}${String(moveCount)} move(s), ${String(byteCount)} bytes \u2014 recover with \`prim moves flush\``
|
|
3102
4360
|
);
|
|
3103
4361
|
for (const f of stranded) {
|
|
3104
4362
|
const ageS = Math.round((Date.now() - f.mtimeMs) / MS_PER_SECOND2);
|
|
3105
4363
|
const owner = f.pid === void 0 ? "no pid" : `pid ${String(f.pid)}`;
|
|
4364
|
+
const count = `${String(f.lineCount)}${f.sampled ? "+" : ""}`;
|
|
3106
4365
|
console.log(
|
|
3107
|
-
` ${f.bucket.padEnd(BUCKET_COL_WIDTH)} ${
|
|
4366
|
+
` ${f.bucket.padEnd(BUCKET_COL_WIDTH)} ${count.padStart(5)} stranded, ${String(f.sizeBytes).padStart(8)} bytes, ${String(ageS)}s ago (${owner})`
|
|
3108
4367
|
);
|
|
3109
4368
|
}
|
|
3110
4369
|
}
|
|
@@ -3132,19 +4391,19 @@ function registerMovesCommands(program2) {
|
|
|
3132
4391
|
}
|
|
3133
4392
|
});
|
|
3134
4393
|
moves.command("bind").description("Pin the current directory to an org via .prim/workspace.json").requiredOption("--orgId <orgId>", "Convex organization id").action((opts) => {
|
|
3135
|
-
const dir =
|
|
3136
|
-
if (!
|
|
3137
|
-
|
|
4394
|
+
const dir = join7(process.cwd(), ".prim");
|
|
4395
|
+
if (!existsSync8(dir)) {
|
|
4396
|
+
mkdirSync7(dir, { recursive: true, mode: DIR_MODE });
|
|
3138
4397
|
}
|
|
3139
|
-
const file =
|
|
3140
|
-
|
|
4398
|
+
const file = join7(process.cwd(), WORKSPACE_FILE);
|
|
4399
|
+
writeFileSync6(file, JSON.stringify({ orgId: opts.orgId, boundAt: Date.now() }, null, 2), {
|
|
3141
4400
|
mode: FILE_MODE2
|
|
3142
4401
|
});
|
|
3143
4402
|
console.log(`[prim] bound ${process.cwd()} to org ${opts.orgId}`);
|
|
3144
4403
|
});
|
|
3145
4404
|
moves.command("drop").description("Remove the .prim/workspace.json binding from the cwd").action(() => {
|
|
3146
|
-
const file =
|
|
3147
|
-
if (!
|
|
4405
|
+
const file = join7(process.cwd(), WORKSPACE_FILE);
|
|
4406
|
+
if (!existsSync8(file)) {
|
|
3148
4407
|
console.log("[prim] no workspace binding in cwd");
|
|
3149
4408
|
return;
|
|
3150
4409
|
}
|
|
@@ -3237,23 +4496,23 @@ function registerReconcileCommands(program2) {
|
|
|
3237
4496
|
|
|
3238
4497
|
// src/commands/session.ts
|
|
3239
4498
|
import {
|
|
3240
|
-
existsSync as
|
|
3241
|
-
mkdirSync as
|
|
3242
|
-
readFileSync as
|
|
4499
|
+
existsSync as existsSync9,
|
|
4500
|
+
mkdirSync as mkdirSync8,
|
|
4501
|
+
readFileSync as readFileSync7,
|
|
3243
4502
|
readdirSync,
|
|
3244
4503
|
unlinkSync as unlinkSync5,
|
|
3245
|
-
writeFileSync as
|
|
4504
|
+
writeFileSync as writeFileSync7
|
|
3246
4505
|
} from "fs";
|
|
3247
|
-
import { join as
|
|
4506
|
+
import { join as join8 } from "path";
|
|
3248
4507
|
var DIR_MODE2 = 448;
|
|
3249
4508
|
var FILE_MODE3 = 384;
|
|
3250
4509
|
function ensureDir() {
|
|
3251
|
-
if (!
|
|
3252
|
-
|
|
4510
|
+
if (!existsSync9(SESSIONS_DIR)) {
|
|
4511
|
+
mkdirSync8(SESSIONS_DIR, { recursive: true, mode: DIR_MODE2 });
|
|
3253
4512
|
}
|
|
3254
4513
|
}
|
|
3255
4514
|
function markerPath(sessionId) {
|
|
3256
|
-
return
|
|
4515
|
+
return join8(SESSIONS_DIR, `${sessionId}.json`);
|
|
3257
4516
|
}
|
|
3258
4517
|
function registerSessionCommands(program2) {
|
|
3259
4518
|
const session = program2.command("session").description("Decision Event Pipeline \u2014 session binding markers");
|
|
@@ -3263,13 +4522,13 @@ function registerSessionCommands(program2) {
|
|
|
3263
4522
|
orgId: opts.orgId,
|
|
3264
4523
|
startedAt: Date.now()
|
|
3265
4524
|
};
|
|
3266
|
-
|
|
4525
|
+
writeFileSync7(markerPath(sessionId), JSON.stringify(marker, null, 2), {
|
|
3267
4526
|
mode: FILE_MODE3
|
|
3268
4527
|
});
|
|
3269
4528
|
console.log(`[prim] session ${sessionId} bound to org ${opts.orgId}`);
|
|
3270
4529
|
});
|
|
3271
4530
|
session.command("list").description("List active session markers").action(() => {
|
|
3272
|
-
if (!
|
|
4531
|
+
if (!existsSync9(SESSIONS_DIR)) {
|
|
3273
4532
|
console.log("[prim] no session markers");
|
|
3274
4533
|
return;
|
|
3275
4534
|
}
|
|
@@ -3281,7 +4540,7 @@ function registerSessionCommands(program2) {
|
|
|
3281
4540
|
for (const f of files) {
|
|
3282
4541
|
const sessionId = f.replace(/\.json$/, "");
|
|
3283
4542
|
try {
|
|
3284
|
-
const m = JSON.parse(
|
|
4543
|
+
const m = JSON.parse(readFileSync7(join8(SESSIONS_DIR, f), "utf-8"));
|
|
3285
4544
|
console.log(`${sessionId} org=${m.orgId}`);
|
|
3286
4545
|
} catch {
|
|
3287
4546
|
}
|
|
@@ -3289,7 +4548,7 @@ function registerSessionCommands(program2) {
|
|
|
3289
4548
|
});
|
|
3290
4549
|
session.command("drop <sessionId>").description("Remove a session marker").action((sessionId) => {
|
|
3291
4550
|
const p = markerPath(sessionId);
|
|
3292
|
-
if (!
|
|
4551
|
+
if (!existsSync9(p)) {
|
|
3293
4552
|
console.log(`[prim] no marker for session ${sessionId}`);
|
|
3294
4553
|
return;
|
|
3295
4554
|
}
|
|
@@ -3299,9 +4558,9 @@ function registerSessionCommands(program2) {
|
|
|
3299
4558
|
}
|
|
3300
4559
|
|
|
3301
4560
|
// src/commands/setup.ts
|
|
3302
|
-
import { spawnSync } from "child_process";
|
|
3303
|
-
import { existsSync as
|
|
3304
|
-
import { join as
|
|
4561
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
4562
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
|
|
4563
|
+
import { join as join9 } from "path";
|
|
3305
4564
|
var EXIT_INCOMPLETE = 1;
|
|
3306
4565
|
var EXIT_USAGE3 = 2;
|
|
3307
4566
|
var SESSION_LABELS = {
|
|
@@ -3325,7 +4584,20 @@ function planSetupSteps(opts) {
|
|
|
3325
4584
|
key: "daemon",
|
|
3326
4585
|
label: "Companion daemon",
|
|
3327
4586
|
args: ["daemon", "start"],
|
|
3328
|
-
required:
|
|
4587
|
+
required: true
|
|
4588
|
+
});
|
|
4589
|
+
steps.push({
|
|
4590
|
+
key: "health",
|
|
4591
|
+
label: "Capture health",
|
|
4592
|
+
args: ["doctor"],
|
|
4593
|
+
required: true
|
|
4594
|
+
});
|
|
4595
|
+
} else {
|
|
4596
|
+
steps.push({
|
|
4597
|
+
key: "daemon-opt-out",
|
|
4598
|
+
label: "Daemon opt-out",
|
|
4599
|
+
args: ["daemon", "stop"],
|
|
4600
|
+
required: true
|
|
3329
4601
|
});
|
|
3330
4602
|
}
|
|
3331
4603
|
steps.push({
|
|
@@ -3364,8 +4636,8 @@ function detectProjectConflicts(agent, run) {
|
|
|
3364
4636
|
}
|
|
3365
4637
|
try {
|
|
3366
4638
|
const root = gitToplevel();
|
|
3367
|
-
const preCommit = root &&
|
|
3368
|
-
if (preCommit &&
|
|
4639
|
+
const preCommit = root && join9(root, ".git", "hooks", "pre-commit");
|
|
4640
|
+
if (preCommit && existsSync10(preCommit) && readFileSync8(preCommit, "utf-8").includes("prim-pre-commit")) {
|
|
3369
4641
|
conflicts.push(CONFLICT_HOOKS);
|
|
3370
4642
|
}
|
|
3371
4643
|
} catch {
|
|
@@ -3401,7 +4673,7 @@ function registerSetupCommand(program2) {
|
|
|
3401
4673
|
).option(
|
|
3402
4674
|
"--migrate",
|
|
3403
4675
|
"with the default user scope, remove any project-scoped prim config in this repo (else just warn)"
|
|
3404
|
-
).option("--no-daemon", "
|
|
4676
|
+
).option("--no-daemon", "stop and disable the companion daemon").action((opts) => {
|
|
3405
4677
|
const { agent: agentInput, detected } = resolveAgent(opts.agent, process.env);
|
|
3406
4678
|
if (agentInput !== "claude" && agentInput !== "codex" && agentInput !== "hermes") {
|
|
3407
4679
|
process.stderr.write(
|
|
@@ -3419,7 +4691,7 @@ function registerSetupCommand(program2) {
|
|
|
3419
4691
|
const scope = opts.scope;
|
|
3420
4692
|
const self = process.argv[1];
|
|
3421
4693
|
const run = (args, capture = false) => {
|
|
3422
|
-
const r =
|
|
4694
|
+
const r = spawnSync2(process.execPath, [self, ...args], {
|
|
3423
4695
|
stdio: capture ? ["inherit", "pipe", "ignore"] : "inherit",
|
|
3424
4696
|
encoding: "utf-8"
|
|
3425
4697
|
});
|
|
@@ -3483,39 +4755,39 @@ function registerSetupCommand(program2) {
|
|
|
3483
4755
|
// src/commands/skill.ts
|
|
3484
4756
|
import {
|
|
3485
4757
|
closeSync as closeSync4,
|
|
3486
|
-
existsSync as
|
|
4758
|
+
existsSync as existsSync12,
|
|
3487
4759
|
fsyncSync as fsyncSync3,
|
|
3488
4760
|
openSync as openSync4,
|
|
3489
|
-
readFileSync as
|
|
3490
|
-
renameSync as
|
|
3491
|
-
writeFileSync as
|
|
4761
|
+
readFileSync as readFileSync10,
|
|
4762
|
+
renameSync as renameSync5,
|
|
4763
|
+
writeFileSync as writeFileSync8
|
|
3492
4764
|
} from "fs";
|
|
3493
|
-
import { homedir as
|
|
3494
|
-
import { dirname as
|
|
4765
|
+
import { homedir as homedir8 } from "os";
|
|
4766
|
+
import { dirname as dirname7, join as join11, resolve as resolve4 } from "path";
|
|
3495
4767
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3496
4768
|
import { createPatch } from "diff";
|
|
3497
4769
|
|
|
3498
4770
|
// src/commands/claude-plugin.ts
|
|
3499
|
-
import { existsSync as
|
|
3500
|
-
import { homedir as
|
|
3501
|
-
import { dirname as
|
|
4771
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync9, readFileSync as readFileSync9, readdirSync as readdirSync2, rmSync as rmSync4, rmdirSync } from "fs";
|
|
4772
|
+
import { homedir as homedir7 } from "os";
|
|
4773
|
+
import { dirname as dirname6, join as join10, resolve as resolve3 } from "path";
|
|
3502
4774
|
import { fileURLToPath } from "url";
|
|
3503
|
-
var __dirname =
|
|
4775
|
+
var __dirname = dirname6(fileURLToPath(import.meta.url));
|
|
3504
4776
|
var PLUGIN_DESCRIPTION = "Primitive decision-graph guidance for the prim CLI \u2014 a model-invoked skill installed by prim skill install.";
|
|
3505
4777
|
function resolvePluginDir(cwd, scope) {
|
|
3506
4778
|
if (scope && scope !== "user" && scope !== "project") {
|
|
3507
4779
|
console.error(`Unknown --scope "${scope}" (expected user or project)`);
|
|
3508
4780
|
return null;
|
|
3509
4781
|
}
|
|
3510
|
-
const base = scope === "user" ?
|
|
3511
|
-
return
|
|
4782
|
+
const base = scope === "user" ? join10(homedir7(), ".claude") : join10(gitToplevel(cwd) ?? cwd, ".claude");
|
|
4783
|
+
return join10(base, "skills", "prim");
|
|
3512
4784
|
}
|
|
3513
4785
|
function packageVersion() {
|
|
3514
4786
|
let dir = __dirname;
|
|
3515
|
-
while (dir !==
|
|
3516
|
-
const p =
|
|
3517
|
-
if (
|
|
3518
|
-
dir =
|
|
4787
|
+
while (dir !== dirname6(dir)) {
|
|
4788
|
+
const p = resolve3(dir, "package.json");
|
|
4789
|
+
if (existsSync11(p)) return JSON.parse(readFileSync9(p, "utf-8")).version;
|
|
4790
|
+
dir = dirname6(dir);
|
|
3519
4791
|
}
|
|
3520
4792
|
return "0.0.0";
|
|
3521
4793
|
}
|
|
@@ -3525,12 +4797,12 @@ function pluginManifest() {
|
|
|
3525
4797
|
`;
|
|
3526
4798
|
}
|
|
3527
4799
|
function removeDirIfEmpty(dir) {
|
|
3528
|
-
if (
|
|
4800
|
+
if (existsSync11(dir) && readdirSync2(dir).length === 0) rmdirSync(dir);
|
|
3529
4801
|
}
|
|
3530
4802
|
function pluginPaths(dir) {
|
|
3531
4803
|
return {
|
|
3532
|
-
manifestPath:
|
|
3533
|
-
skillPath:
|
|
4804
|
+
manifestPath: join10(dir, ".claude-plugin", "plugin.json"),
|
|
4805
|
+
skillPath: join10(dir, "SKILL.md")
|
|
3534
4806
|
};
|
|
3535
4807
|
}
|
|
3536
4808
|
function installClaudePlugin(cwd, opts) {
|
|
@@ -3539,8 +4811,8 @@ function installClaudePlugin(cwd, opts) {
|
|
|
3539
4811
|
const { manifestPath, skillPath } = pluginPaths(dir);
|
|
3540
4812
|
const manifest = pluginManifest();
|
|
3541
4813
|
const skill = loadSkill();
|
|
3542
|
-
const manifestCurrent =
|
|
3543
|
-
const skillCurrent =
|
|
4814
|
+
const manifestCurrent = existsSync11(manifestPath) ? readFileSync9(manifestPath, "utf-8") : null;
|
|
4815
|
+
const skillCurrent = existsSync11(skillPath) ? readFileSync9(skillPath, "utf-8") : null;
|
|
3544
4816
|
if (manifestCurrent === manifest && skillCurrent === skill) {
|
|
3545
4817
|
console.log("No changes \u2014 prim skill plugin already up to date.");
|
|
3546
4818
|
return 0;
|
|
@@ -3549,9 +4821,9 @@ function installClaudePlugin(cwd, opts) {
|
|
|
3549
4821
|
console.log(`Would write plugin to ${dir} (.claude-plugin/plugin.json + SKILL.md)`);
|
|
3550
4822
|
return 0;
|
|
3551
4823
|
}
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
4824
|
+
mkdirSync9(join10(dir, ".claude-plugin"), { recursive: true });
|
|
4825
|
+
atomicWrite3(manifestPath, manifest);
|
|
4826
|
+
atomicWrite3(skillPath, skill);
|
|
3555
4827
|
console.log(`Installed prim skill plugin at ${dir}`);
|
|
3556
4828
|
console.log("Restart Claude Code or run /reload-plugins to load it.");
|
|
3557
4829
|
if (opts.scope !== "user") {
|
|
@@ -3563,13 +4835,13 @@ function uninstallClaudePlugin(cwd, opts) {
|
|
|
3563
4835
|
const dir = resolvePluginDir(cwd, opts.scope);
|
|
3564
4836
|
if (dir === null) return 1;
|
|
3565
4837
|
const { manifestPath, skillPath } = pluginPaths(dir);
|
|
3566
|
-
if (!
|
|
4838
|
+
if (!existsSync11(manifestPath) && !existsSync11(skillPath)) {
|
|
3567
4839
|
console.log(`prim skill plugin not present at ${dir}`);
|
|
3568
4840
|
return 0;
|
|
3569
4841
|
}
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
removeDirIfEmpty(
|
|
4842
|
+
rmSync4(manifestPath, { force: true });
|
|
4843
|
+
rmSync4(skillPath, { force: true });
|
|
4844
|
+
removeDirIfEmpty(join10(dir, ".claude-plugin"));
|
|
3573
4845
|
removeDirIfEmpty(dir);
|
|
3574
4846
|
console.log(`Removed prim skill plugin from ${dir}`);
|
|
3575
4847
|
return 0;
|
|
@@ -3578,7 +4850,7 @@ function statusClaudePlugin(cwd, opts) {
|
|
|
3578
4850
|
const dir = resolvePluginDir(cwd, opts.scope);
|
|
3579
4851
|
if (dir === null) return 1;
|
|
3580
4852
|
const { skillPath } = pluginPaths(dir);
|
|
3581
|
-
const installed =
|
|
4853
|
+
const installed = existsSync11(skillPath);
|
|
3582
4854
|
if (opts.json) {
|
|
3583
4855
|
printJson({ installed, target: dir });
|
|
3584
4856
|
return installed ? 0 : 1;
|
|
@@ -3592,7 +4864,7 @@ function statusClaudePlugin(cwd, opts) {
|
|
|
3592
4864
|
}
|
|
3593
4865
|
|
|
3594
4866
|
// src/commands/skill.ts
|
|
3595
|
-
var __dirname2 =
|
|
4867
|
+
var __dirname2 = dirname7(fileURLToPath2(import.meta.url));
|
|
3596
4868
|
var SKILL_BEGIN = "<!-- BEGIN PRIM SKILL v1 -->";
|
|
3597
4869
|
var SKILL_END = "<!-- END PRIM SKILL v1 -->";
|
|
3598
4870
|
var TARGET_CANDIDATES = [
|
|
@@ -3610,24 +4882,24 @@ var AGENT_TARGET = {
|
|
|
3610
4882
|
hermes: ".hermes.md"
|
|
3611
4883
|
};
|
|
3612
4884
|
function userTargetFor(agent) {
|
|
3613
|
-
if (agent === "claude") return
|
|
3614
|
-
if (agent === "codex") return
|
|
4885
|
+
if (agent === "claude") return join11(homedir8(), ".claude", "CLAUDE.md");
|
|
4886
|
+
if (agent === "codex") return join11(homedir8(), ".codex", "AGENTS.md");
|
|
3615
4887
|
if (agent === "hermes") {
|
|
3616
|
-
return
|
|
4888
|
+
return join11(process.env.HERMES_HOME ?? join11(homedir8(), ".hermes"), ".hermes.md");
|
|
3617
4889
|
}
|
|
3618
4890
|
return null;
|
|
3619
4891
|
}
|
|
3620
4892
|
function loadSkill() {
|
|
3621
4893
|
let dir = __dirname2;
|
|
3622
|
-
while (dir !==
|
|
3623
|
-
const p =
|
|
3624
|
-
if (
|
|
3625
|
-
dir =
|
|
4894
|
+
while (dir !== dirname7(dir)) {
|
|
4895
|
+
const p = resolve4(dir, "SKILL.md");
|
|
4896
|
+
if (existsSync12(p)) return readFileSync10(p, "utf-8");
|
|
4897
|
+
dir = dirname7(dir);
|
|
3626
4898
|
}
|
|
3627
4899
|
throw new Error("SKILL.md not found in package");
|
|
3628
4900
|
}
|
|
3629
4901
|
function detectTargets(cwd) {
|
|
3630
|
-
return TARGET_CANDIDATES.filter((p) =>
|
|
4902
|
+
return TARGET_CANDIDATES.filter((p) => existsSync12(resolve4(cwd, p)));
|
|
3631
4903
|
}
|
|
3632
4904
|
function detectNewline(content) {
|
|
3633
4905
|
return content.includes("\r\n") ? "\r\n" : "\n";
|
|
@@ -3653,23 +4925,23 @@ function removeBlock(existing) {
|
|
|
3653
4925
|
const out = existing.slice(0, b) + existing.slice(e + SKILL_END.length);
|
|
3654
4926
|
return out.replace(/(\r?\n){2,}$/, "$1");
|
|
3655
4927
|
}
|
|
3656
|
-
function
|
|
4928
|
+
function atomicWrite3(target, content) {
|
|
3657
4929
|
const tmp = `${target}.tmp`;
|
|
3658
|
-
|
|
4930
|
+
writeFileSync8(tmp, content);
|
|
3659
4931
|
const fd = openSync4(tmp, "r+");
|
|
3660
4932
|
try {
|
|
3661
4933
|
fsyncSync3(fd);
|
|
3662
4934
|
} finally {
|
|
3663
4935
|
closeSync4(fd);
|
|
3664
4936
|
}
|
|
3665
|
-
|
|
4937
|
+
renameSync5(tmp, target);
|
|
3666
4938
|
}
|
|
3667
4939
|
function resolveTarget(cwd, opts) {
|
|
3668
4940
|
if (opts.scope && opts.scope !== "user" && opts.scope !== "project") {
|
|
3669
4941
|
console.error(`Unknown --scope "${opts.scope}" (expected user or project)`);
|
|
3670
4942
|
return null;
|
|
3671
4943
|
}
|
|
3672
|
-
if (opts.target) return
|
|
4944
|
+
if (opts.target) return resolve4(cwd, opts.target);
|
|
3673
4945
|
if (opts.scope === "user") {
|
|
3674
4946
|
if (!opts.agent) {
|
|
3675
4947
|
console.error("--scope user requires --agent (claude, codex, or hermes)");
|
|
@@ -3682,13 +4954,13 @@ function resolveTarget(cwd, opts) {
|
|
|
3682
4954
|
}
|
|
3683
4955
|
if (opts.agent) {
|
|
3684
4956
|
const mapped = AGENT_TARGET[opts.agent];
|
|
3685
|
-
if (typeof mapped === "string") return
|
|
4957
|
+
if (typeof mapped === "string") return resolve4(cwd, mapped);
|
|
3686
4958
|
console.error(`Unknown --agent "${opts.agent}" (expected claude, codex, or hermes)`);
|
|
3687
4959
|
return null;
|
|
3688
4960
|
}
|
|
3689
4961
|
const matches = detectTargets(cwd);
|
|
3690
|
-
if (matches.length === 0) return
|
|
3691
|
-
if (matches.length === 1) return
|
|
4962
|
+
if (matches.length === 0) return resolve4(cwd, DEFAULT_TARGET);
|
|
4963
|
+
if (matches.length === 1) return resolve4(cwd, matches[0]);
|
|
3692
4964
|
console.error("Multiple rules files detected. Use --target to disambiguate:");
|
|
3693
4965
|
for (const m of matches) console.error(` ${m}`);
|
|
3694
4966
|
return null;
|
|
@@ -3697,7 +4969,7 @@ function runInstall(cwd, opts) {
|
|
|
3697
4969
|
if (opts.agent === "claude" && !opts.target) return installClaudePlugin(cwd, opts);
|
|
3698
4970
|
const target = resolveTarget(cwd, opts);
|
|
3699
4971
|
if (target === null) return 1;
|
|
3700
|
-
const existing =
|
|
4972
|
+
const existing = existsSync12(target) ? readFileSync10(target, "utf-8") : "";
|
|
3701
4973
|
const eol = existing ? detectNewline(existing) : "\n";
|
|
3702
4974
|
const block = composeBlock(loadSkill(), eol);
|
|
3703
4975
|
const next = applyBlock(existing, block, eol);
|
|
@@ -3709,7 +4981,7 @@ function runInstall(cwd, opts) {
|
|
|
3709
4981
|
process.stdout.write(createPatch(target, existing, next, "current", "proposed"));
|
|
3710
4982
|
return 0;
|
|
3711
4983
|
}
|
|
3712
|
-
|
|
4984
|
+
atomicWrite3(target, next);
|
|
3713
4985
|
console.log(`Wrote ${Buffer.byteLength(next)} bytes to ${target}`);
|
|
3714
4986
|
return 0;
|
|
3715
4987
|
}
|
|
@@ -3717,17 +4989,17 @@ function runUninstall(cwd, opts) {
|
|
|
3717
4989
|
if (opts.agent === "claude" && !opts.target) return uninstallClaudePlugin(cwd, opts);
|
|
3718
4990
|
const target = resolveTarget(cwd, opts);
|
|
3719
4991
|
if (target === null) return 1;
|
|
3720
|
-
if (!
|
|
4992
|
+
if (!existsSync12(target)) {
|
|
3721
4993
|
console.log(`Skill block not present at ${target}`);
|
|
3722
4994
|
return 0;
|
|
3723
4995
|
}
|
|
3724
|
-
const existing =
|
|
4996
|
+
const existing = readFileSync10(target, "utf-8");
|
|
3725
4997
|
const next = removeBlock(existing);
|
|
3726
4998
|
if (next === null) {
|
|
3727
4999
|
console.log(`Skill block not present at ${target}`);
|
|
3728
5000
|
return 0;
|
|
3729
5001
|
}
|
|
3730
|
-
|
|
5002
|
+
atomicWrite3(target, next);
|
|
3731
5003
|
console.log(`Removed skill block from ${target}`);
|
|
3732
5004
|
return 0;
|
|
3733
5005
|
}
|
|
@@ -3735,10 +5007,10 @@ function runStatus(cwd, opts) {
|
|
|
3735
5007
|
if (opts.agent === "claude" && !opts.target) return statusClaudePlugin(cwd, opts);
|
|
3736
5008
|
const target = resolveTarget(cwd, opts);
|
|
3737
5009
|
if (target === null) return 1;
|
|
3738
|
-
const fileExists =
|
|
5010
|
+
const fileExists = existsSync12(target);
|
|
3739
5011
|
let installed = false;
|
|
3740
5012
|
if (fileExists) {
|
|
3741
|
-
const content =
|
|
5013
|
+
const content = readFileSync10(target, "utf-8");
|
|
3742
5014
|
installed = content.includes(SKILL_BEGIN) && content.includes(SKILL_END);
|
|
3743
5015
|
}
|
|
3744
5016
|
if (opts.json) {
|
|
@@ -3775,18 +5047,24 @@ function registerSkillCommands(program2) {
|
|
|
3775
5047
|
}
|
|
3776
5048
|
|
|
3777
5049
|
// src/commands/statusline.ts
|
|
3778
|
-
import { readFileSync as
|
|
3779
|
-
import { dirname as
|
|
5050
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
5051
|
+
import { dirname as dirname8, resolve as resolve5 } from "path";
|
|
3780
5052
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
3781
5053
|
var STATUSLINE_TIMEOUT_MS = 200;
|
|
3782
5054
|
var STATUSLINE_NAME_CAP = 3;
|
|
3783
5055
|
function readPackageVersion() {
|
|
3784
5056
|
try {
|
|
3785
|
-
const here =
|
|
3786
|
-
const candidates = [
|
|
5057
|
+
const here = dirname8(fileURLToPath3(import.meta.url));
|
|
5058
|
+
const candidates = [
|
|
5059
|
+
// The supervised runtime stages a tiny manifest beside the standalone
|
|
5060
|
+
// statusline bundle, so it remains versioned without loading the package.
|
|
5061
|
+
resolve5(here, "manifest.json"),
|
|
5062
|
+
resolve5(here, "../../package.json"),
|
|
5063
|
+
resolve5(here, "../package.json")
|
|
5064
|
+
];
|
|
3787
5065
|
for (const path of candidates) {
|
|
3788
5066
|
try {
|
|
3789
|
-
const pkg2 = JSON.parse(
|
|
5067
|
+
const pkg2 = JSON.parse(readFileSync11(path, "utf-8"));
|
|
3790
5068
|
if (pkg2.version) {
|
|
3791
5069
|
return pkg2.version;
|
|
3792
5070
|
}
|
|
@@ -3816,6 +5094,17 @@ async function renderStatusline() {
|
|
|
3816
5094
|
debug("daemon snapshot missing");
|
|
3817
5095
|
return `primitive ${version} (daemon: down)`;
|
|
3818
5096
|
}
|
|
5097
|
+
if (snapshot.healthy === false) {
|
|
5098
|
+
if (snapshot.ingestion?.healthy === false) {
|
|
5099
|
+
const pending = snapshot.ingestion.pendingCount;
|
|
5100
|
+
const qualifier = snapshot.ingestion.pendingSampled ? "at least " : "";
|
|
5101
|
+
return `primitive ${version} (daemon: degraded \xB7 delivery: stalled${typeof pending === "number" ? ` \xB7 ${qualifier}${String(pending)} pending` : ""})`;
|
|
5102
|
+
}
|
|
5103
|
+
if (snapshot.heartbeat?.healthy === false) {
|
|
5104
|
+
return `primitive ${version} (daemon: degraded \xB7 presence: unavailable)`;
|
|
5105
|
+
}
|
|
5106
|
+
return `primitive ${version} (daemon: starting)`;
|
|
5107
|
+
}
|
|
3819
5108
|
if (snapshot.envMismatch) {
|
|
3820
5109
|
return `primitive ${version} (daemon: live \xB7 presence: other env)`;
|
|
3821
5110
|
}
|
|
@@ -3841,6 +5130,7 @@ function registerStatuslineCommands(program2) {
|
|
|
3841
5130
|
const line = await renderStatusline();
|
|
3842
5131
|
process.stdout.write(line);
|
|
3843
5132
|
} catch {
|
|
5133
|
+
process.stdout.write(`primitive ${readPackageVersion()} (daemon: unavailable)`);
|
|
3844
5134
|
}
|
|
3845
5135
|
});
|
|
3846
5136
|
}
|
|
@@ -3957,8 +5247,8 @@ function registerWelcomeCommand(program2, deps = { getClient }) {
|
|
|
3957
5247
|
}
|
|
3958
5248
|
|
|
3959
5249
|
// src/index.ts
|
|
3960
|
-
var __dirname3 =
|
|
3961
|
-
var pkg = JSON.parse(
|
|
5250
|
+
var __dirname3 = dirname9(fileURLToPath4(import.meta.url));
|
|
5251
|
+
var pkg = JSON.parse(readFileSync12(resolve6(__dirname3, "../package.json"), "utf-8"));
|
|
3962
5252
|
updateNotifier({ pkg }).notify();
|
|
3963
5253
|
var program = new Command();
|
|
3964
5254
|
program.name("prim").description("CLI for Primitive's decision graph").version(pkg.version).option("-y, --yes", "auto-confirm prompts").option(
|