@primitive.ai/prim 0.1.0-alpha.59 → 0.1.0-alpha.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +2 -2
- package/dist/{chunk-QY75BQMF.js → chunk-JQFN6LBO.js} +8 -0
- package/dist/{chunk-UBEBEBLM.js → chunk-N4R7EXZT.js} +63 -37
- package/dist/daemon/server.js +1 -0
- package/dist/hooks/post-tool-use.js +1 -1
- package/dist/hooks/session-start.js +35 -11
- package/dist/index.js +558 -165
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
bold,
|
|
4
|
+
boundedHealthError,
|
|
4
5
|
color,
|
|
5
6
|
colorForArea,
|
|
6
7
|
dim,
|
|
7
8
|
requireDurableIngestAcknowledgement,
|
|
8
9
|
stripAnsi,
|
|
9
10
|
stripControlChars
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-JQFN6LBO.js";
|
|
11
12
|
import {
|
|
12
13
|
checkAffectedDecisions,
|
|
13
14
|
daemonOrDirectGet,
|
|
@@ -23,9 +24,11 @@ import {
|
|
|
23
24
|
readMovesFromPath
|
|
24
25
|
} from "./chunk-S4LFRVGD.js";
|
|
25
26
|
import {
|
|
27
|
+
bindRepository,
|
|
28
|
+
compareSemver,
|
|
26
29
|
printJson,
|
|
27
30
|
registerSkillCommands
|
|
28
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-N4R7EXZT.js";
|
|
29
32
|
import {
|
|
30
33
|
fetchFeedbackCapability
|
|
31
34
|
} from "./chunk-R4CUX6FY.js";
|
|
@@ -45,11 +48,9 @@ import {
|
|
|
45
48
|
canonicalRepositoryPath,
|
|
46
49
|
decisionIngestionStatus,
|
|
47
50
|
gitToplevel,
|
|
48
|
-
githubRepositoryFullName,
|
|
49
51
|
isRepoActiveForCapture,
|
|
50
52
|
repoSyncId,
|
|
51
|
-
setRepoActive
|
|
52
|
-
setRepoSyncId
|
|
53
|
+
setRepoActive
|
|
53
54
|
} from "./chunk-LDQZASIF.js";
|
|
54
55
|
import {
|
|
55
56
|
HttpError,
|
|
@@ -91,18 +92,7 @@ async function applyActivation(active) {
|
|
|
91
92
|
try {
|
|
92
93
|
let bound;
|
|
93
94
|
if (active) {
|
|
94
|
-
|
|
95
|
-
if (!repositoryFullName) {
|
|
96
|
-
throw new Error("origin must be a GitHub HTTPS/SSH remote in owner/name form");
|
|
97
|
-
}
|
|
98
|
-
const response = await getClient().post("/api/cli/repositories/bind", {
|
|
99
|
-
repositoryFullName
|
|
100
|
-
});
|
|
101
|
-
if (typeof response.repoSyncId !== "string" || response.repoSyncId.length === 0) {
|
|
102
|
-
throw new Error("server returned no repository binding");
|
|
103
|
-
}
|
|
104
|
-
setRepoSyncId(root, response.repoSyncId);
|
|
105
|
-
bound = { repoSyncId: response.repoSyncId, repositoryFullName };
|
|
95
|
+
bound = await bindRepository(root);
|
|
106
96
|
}
|
|
107
97
|
setRepoActive(root, active);
|
|
108
98
|
process.stderr.write(`[prim] prim ${active ? "enabled" : "disabled"} in ${root}
|
|
@@ -648,28 +638,58 @@ import { dirname as dirname2, join as join2 } from "path";
|
|
|
648
638
|
import { spawnSync } from "child_process";
|
|
649
639
|
import { createHash as createHash2 } from "crypto";
|
|
650
640
|
import {
|
|
641
|
+
constants,
|
|
642
|
+
accessSync,
|
|
651
643
|
chmodSync,
|
|
652
644
|
copyFileSync,
|
|
653
645
|
existsSync as existsSync2,
|
|
654
646
|
mkdirSync,
|
|
655
647
|
mkdtempSync,
|
|
656
648
|
readFileSync as readFileSync2,
|
|
649
|
+
realpathSync,
|
|
657
650
|
renameSync,
|
|
658
651
|
rmSync,
|
|
659
652
|
symlinkSync,
|
|
660
653
|
writeFileSync
|
|
661
654
|
} from "fs";
|
|
662
655
|
import { homedir } from "os";
|
|
663
|
-
import { dirname, isAbsolute, join, resolve } from "path";
|
|
656
|
+
import { basename, dirname, isAbsolute, join, resolve } from "path";
|
|
657
|
+
|
|
658
|
+
// src/daemon/env-binding.ts
|
|
659
|
+
function normalizeApiUrl(url) {
|
|
660
|
+
return url.trim().replace(/\/+$/, "");
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// src/daemon/launchd.ts
|
|
664
664
|
var LAUNCHD_LABEL = "ai.getprimitive.prim-daemon";
|
|
665
|
-
var RUNTIME_SCHEMA_VERSION =
|
|
665
|
+
var RUNTIME_SCHEMA_VERSION = 2;
|
|
666
666
|
var RUNTIME_DIR_MODE = 448;
|
|
667
667
|
var RUNTIME_FILE_MODE = 384;
|
|
668
668
|
var RUNTIME_LAUNCHER_MODE = 448;
|
|
669
669
|
var PLIST_FILE_MODE = 384;
|
|
670
|
-
var
|
|
670
|
+
var LAUNCHER_SCHEMA_VERSION = 1;
|
|
671
|
+
var TRANSITION_TIMEOUT_MS = 15e3;
|
|
672
|
+
var COMMAND_TIMEOUT_MS = 5e3;
|
|
671
673
|
var READY_POLL_MS = 100;
|
|
672
674
|
var READY_PROBE_TIMEOUT_MS = 250;
|
|
675
|
+
var RETRY_MAX_MS = 500;
|
|
676
|
+
var SERVICE_NOT_FOUND = 113;
|
|
677
|
+
var TRANSITION_IN_PROGRESS = 5;
|
|
678
|
+
var OUTPUT_LIMIT = 4096;
|
|
679
|
+
var LaunchctlError = class extends Error {
|
|
680
|
+
status;
|
|
681
|
+
timedOut;
|
|
682
|
+
constructor(operation, result) {
|
|
683
|
+
const stdout = result.stdout.slice(0, OUTPUT_LIMIT);
|
|
684
|
+
const stderr = result.stderr.slice(0, OUTPUT_LIMIT);
|
|
685
|
+
const raw = stderr.trim() || stdout.trim() || `exit ${String(result.status)}`;
|
|
686
|
+
const detail = raw.replace(/\n?Try re-running the command as root for richer errors\.?/giu, "").trim() || `exit ${String(result.status)}`;
|
|
687
|
+
super(`launchctl ${operation} failed: ${detail}`);
|
|
688
|
+
this.name = "LaunchctlError";
|
|
689
|
+
this.status = result.status;
|
|
690
|
+
this.timedOut = result.timedOut === true;
|
|
691
|
+
}
|
|
692
|
+
};
|
|
673
693
|
function xdgDataHome(options) {
|
|
674
694
|
const home = options.homeDir ?? homedir();
|
|
675
695
|
const configured = (options.env ?? process.env).XDG_DATA_HOME;
|
|
@@ -684,14 +704,22 @@ function runtimePaths(options = {}) {
|
|
|
684
704
|
runtimeDir,
|
|
685
705
|
releasesDir: join(runtimeDir, "releases"),
|
|
686
706
|
currentLink,
|
|
687
|
-
daemonEntry: join(currentLink, "prim-daemon-server"),
|
|
688
|
-
statuslineEntry: join(currentLink, "prim-statusline"),
|
|
689
707
|
statuslineLauncher: join(runtimeDir, "prim-statusline"),
|
|
690
|
-
manifestPath: join(currentLink, "manifest.json"),
|
|
691
708
|
disabledMarker: join(dataDir, "daemon.disabled"),
|
|
692
709
|
lifecycleLockDir: join(dataDir, "daemon.lifecycle.lock")
|
|
693
710
|
};
|
|
694
711
|
}
|
|
712
|
+
function daemonControlPaths(options = {}) {
|
|
713
|
+
const configDir = join(options.homeDir ?? homedir(), ".config", "prim");
|
|
714
|
+
const legacy = runtimePaths(options);
|
|
715
|
+
return {
|
|
716
|
+
launcher: join(configDir, `prim-daemon-launcher-v${LAUNCHER_SCHEMA_VERSION}`),
|
|
717
|
+
disabledMarker: join(configDir, "daemon.disabled"),
|
|
718
|
+
lifecycleLockDir: join(configDir, "daemon.lifecycle.lock"),
|
|
719
|
+
legacyDisabledMarker: legacy.disabledMarker,
|
|
720
|
+
legacyLifecycleLockDir: legacy.lifecycleLockDir
|
|
721
|
+
};
|
|
722
|
+
}
|
|
695
723
|
function findPackageVersion(entryFile) {
|
|
696
724
|
let dir = dirname(resolve(entryFile));
|
|
697
725
|
for (let depth = 0; depth < 6; depth++) {
|
|
@@ -714,7 +742,7 @@ function findPackageVersion(entryFile) {
|
|
|
714
742
|
function readRuntimeManifest(path) {
|
|
715
743
|
try {
|
|
716
744
|
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
717
|
-
if (parsed.schemaVersion !== RUNTIME_SCHEMA_VERSION || typeof parsed.version !== "string" || typeof parsed.nodePath !== "string" || parsed.daemonFile !== "prim-daemon-server") {
|
|
745
|
+
if (parsed.schemaVersion !== RUNTIME_SCHEMA_VERSION || typeof parsed.version !== "string" || typeof parsed.nodePath !== "string" || parsed.daemonFile !== "prim-daemon-server" || typeof parsed.daemonSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(parsed.daemonSha256) || parsed.statuslineFile !== void 0 && parsed.statuslineFile !== "prim-statusline" || parsed.statuslineFile === void 0 !== (parsed.statuslineSha256 === void 0) || parsed.statuslineSha256 !== void 0 && !/^[a-f0-9]{64}$/u.test(parsed.statuslineSha256)) {
|
|
718
746
|
return null;
|
|
719
747
|
}
|
|
720
748
|
return parsed;
|
|
@@ -722,10 +750,18 @@ function readRuntimeManifest(path) {
|
|
|
722
750
|
return null;
|
|
723
751
|
}
|
|
724
752
|
}
|
|
725
|
-
function sameRuntime(current, desired,
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
753
|
+
function sameRuntime(current, desired, daemonPath, statuslinePath) {
|
|
754
|
+
if (!current || current.version !== desired.version || current.nodePath !== desired.nodePath || current.daemonSha256 !== desired.daemonSha256 || current.statuslineFile !== desired.statuslineFile || current.statuslineSha256 !== desired.statuslineSha256) {
|
|
755
|
+
return false;
|
|
756
|
+
}
|
|
757
|
+
try {
|
|
758
|
+
return sha256File(daemonPath) === desired.daemonSha256 && (desired.statuslineSha256 === void 0 || sha256File(statuslinePath) === desired.statuslineSha256);
|
|
759
|
+
} catch {
|
|
760
|
+
return false;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
function sha256File(path) {
|
|
764
|
+
return createHash2("sha256").update(readFileSync2(path)).digest("hex");
|
|
729
765
|
}
|
|
730
766
|
function atomicSymlink(target, linkPath) {
|
|
731
767
|
const tempLink = `${linkPath}.tmp-${process.pid}-${createHash2("sha256").update(`${target}\0${Date.now()}\0${Math.random()}`).digest("hex").slice(0, 12)}`;
|
|
@@ -752,35 +788,60 @@ function stageRuntime(options = {}) {
|
|
|
752
788
|
version,
|
|
753
789
|
nodePath,
|
|
754
790
|
daemonFile: "prim-daemon-server",
|
|
755
|
-
|
|
791
|
+
daemonSha256: sha256File(daemonSource),
|
|
792
|
+
...statuslineSource ? {
|
|
793
|
+
statuslineFile: "prim-statusline",
|
|
794
|
+
statuslineSha256: sha256File(statuslineSource)
|
|
795
|
+
} : {},
|
|
756
796
|
stagedAt: (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
|
|
757
797
|
};
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
798
|
+
let currentRelease = null;
|
|
799
|
+
try {
|
|
800
|
+
currentRelease = realpathSync(paths.currentLink);
|
|
801
|
+
} catch {
|
|
802
|
+
}
|
|
803
|
+
const current = currentRelease ? readRuntimeManifest(join(currentRelease, "manifest.json")) : null;
|
|
804
|
+
const currentDaemon = currentRelease ? join(currentRelease, desired.daemonFile) : "";
|
|
805
|
+
const currentStatusline = currentRelease ? join(currentRelease, "prim-statusline") : "";
|
|
806
|
+
if (currentRelease && sameRuntime(current, desired, currentDaemon, currentStatusline)) {
|
|
807
|
+
const manifest = current;
|
|
808
|
+
const statuslinePath = manifest.statuslineFile ? currentStatusline : void 0;
|
|
809
|
+
if (statuslinePath) {
|
|
761
810
|
atomicWrite(
|
|
762
811
|
paths.statuslineLauncher,
|
|
763
|
-
statuslineLauncherContent(
|
|
812
|
+
statuslineLauncherContent(statuslinePath, manifest.nodePath),
|
|
764
813
|
RUNTIME_LAUNCHER_MODE
|
|
765
814
|
);
|
|
766
815
|
} else {
|
|
767
816
|
rmSync(paths.statuslineLauncher, { force: true });
|
|
768
817
|
}
|
|
769
|
-
return {
|
|
818
|
+
return {
|
|
819
|
+
changed: false,
|
|
820
|
+
manifest,
|
|
821
|
+
paths,
|
|
822
|
+
daemonPath: currentDaemon
|
|
823
|
+
};
|
|
770
824
|
}
|
|
771
825
|
mkdirSync(paths.releasesDir, { recursive: true, mode: RUNTIME_DIR_MODE });
|
|
772
826
|
chmodSync(paths.runtimeDir, RUNTIME_DIR_MODE);
|
|
773
827
|
chmodSync(paths.releasesDir, RUNTIME_DIR_MODE);
|
|
774
828
|
const stagingDir = mkdtempSync(join(paths.releasesDir, ".stage-"));
|
|
775
829
|
chmodSync(stagingDir, RUNTIME_DIR_MODE);
|
|
830
|
+
let releaseDir;
|
|
776
831
|
try {
|
|
777
832
|
const daemonTarget = join(stagingDir, desired.daemonFile);
|
|
778
833
|
copyFileSync(daemonSource, daemonTarget);
|
|
779
834
|
chmodSync(daemonTarget, RUNTIME_FILE_MODE);
|
|
835
|
+
if (sha256File(daemonTarget) !== desired.daemonSha256) {
|
|
836
|
+
throw new Error("cannot stage runtime: daemon bundle changed while it was copied");
|
|
837
|
+
}
|
|
780
838
|
if (statuslineSource && desired.statuslineFile) {
|
|
781
839
|
const statuslineTarget = join(stagingDir, desired.statuslineFile);
|
|
782
840
|
copyFileSync(statuslineSource, statuslineTarget);
|
|
783
841
|
chmodSync(statuslineTarget, RUNTIME_FILE_MODE);
|
|
842
|
+
if (sha256File(statuslineTarget) !== desired.statuslineSha256) {
|
|
843
|
+
throw new Error("cannot stage runtime: statusline bundle changed while it was copied");
|
|
844
|
+
}
|
|
784
845
|
}
|
|
785
846
|
const manifestTarget = join(stagingDir, "manifest.json");
|
|
786
847
|
writeFileSync(manifestTarget, `${JSON.stringify(desired, null, 2)}
|
|
@@ -792,13 +853,15 @@ function stageRuntime(options = {}) {
|
|
|
792
853
|
const releaseName = `release-${createHash2("sha256").update(
|
|
793
854
|
`${version}\0${nodePath}\0${desired.statuslineFile ?? ""}\0${desired.stagedAt}\0${process.pid}\0${Math.random()}`
|
|
794
855
|
).digest("hex").slice(0, 16)}`;
|
|
795
|
-
|
|
856
|
+
releaseDir = join(paths.releasesDir, releaseName);
|
|
796
857
|
renameSync(stagingDir, releaseDir);
|
|
858
|
+
releaseDir = realpathSync(releaseDir);
|
|
797
859
|
atomicSymlink(join("releases", releaseName), paths.currentLink);
|
|
798
860
|
if (desired.statuslineFile) {
|
|
861
|
+
const statuslinePath = join(releaseDir, desired.statuslineFile);
|
|
799
862
|
atomicWrite(
|
|
800
863
|
paths.statuslineLauncher,
|
|
801
|
-
statuslineLauncherContent(
|
|
864
|
+
statuslineLauncherContent(statuslinePath, desired.nodePath),
|
|
802
865
|
RUNTIME_LAUNCHER_MODE
|
|
803
866
|
);
|
|
804
867
|
} else {
|
|
@@ -808,36 +871,91 @@ function stageRuntime(options = {}) {
|
|
|
808
871
|
rmSync(stagingDir, { recursive: true, force: true });
|
|
809
872
|
throw error;
|
|
810
873
|
}
|
|
811
|
-
return {
|
|
874
|
+
return {
|
|
875
|
+
changed: true,
|
|
876
|
+
manifest: desired,
|
|
877
|
+
paths,
|
|
878
|
+
daemonPath: join(releaseDir, desired.daemonFile)
|
|
879
|
+
};
|
|
812
880
|
}
|
|
813
881
|
function shellQuote(value) {
|
|
814
882
|
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
815
883
|
}
|
|
816
|
-
function statuslineLauncherContent(
|
|
884
|
+
function statuslineLauncherContent(statuslinePath, nodePath) {
|
|
817
885
|
return `#!/bin/sh
|
|
818
|
-
exec ${shellQuote(nodePath)} ${shellQuote(
|
|
886
|
+
exec ${shellQuote(nodePath)} ${shellQuote(statuslinePath)}
|
|
819
887
|
`;
|
|
820
888
|
}
|
|
821
889
|
function runtimeStatuslineCommand(options = {}) {
|
|
822
890
|
return shellQuote(runtimePaths(options).statuslineLauncher);
|
|
823
891
|
}
|
|
892
|
+
function generateDaemonLauncher(config) {
|
|
893
|
+
const apiUrl = config.apiUrl ? normalizeApiUrl(config.apiUrl) || void 0 : void 0;
|
|
894
|
+
const metadata = {
|
|
895
|
+
schemaVersion: LAUNCHER_SCHEMA_VERSION,
|
|
896
|
+
nodePath: config.nodePath,
|
|
897
|
+
daemonPath: config.daemonPath,
|
|
898
|
+
runtimeVersion: config.runtimeVersion,
|
|
899
|
+
apiUrl: apiUrl ?? null
|
|
900
|
+
};
|
|
901
|
+
const revision = createHash2("sha256").update(JSON.stringify(metadata)).digest("hex");
|
|
902
|
+
const encoded = Buffer.from(JSON.stringify({ ...metadata, revision })).toString("base64url");
|
|
903
|
+
return {
|
|
904
|
+
revision,
|
|
905
|
+
content: `#!/bin/sh
|
|
906
|
+
# prim-daemon-launcher: ${encoded}
|
|
907
|
+
export PRIM_RUNTIME_VERSION=${shellQuote(config.runtimeVersion)}
|
|
908
|
+
export PRIM_LAUNCH_REVISION=${shellQuote(revision)}
|
|
909
|
+
${apiUrl ? `export PRIM_API_URL=${shellQuote(apiUrl)}` : "unset PRIM_API_URL"}
|
|
910
|
+
exec ${shellQuote(config.nodePath)} ${shellQuote(config.daemonPath)}
|
|
911
|
+
`
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
function readDaemonLauncher(path) {
|
|
915
|
+
try {
|
|
916
|
+
const content = readFileSync2(path, "utf8");
|
|
917
|
+
const encoded = /^# prim-daemon-launcher: ([A-Za-z0-9_-]+)$/mu.exec(content)?.[1];
|
|
918
|
+
if (!encoded) return null;
|
|
919
|
+
const value = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
920
|
+
if (value.schemaVersion !== LAUNCHER_SCHEMA_VERSION || typeof value.nodePath !== "string" || typeof value.daemonPath !== "string" || typeof value.runtimeVersion !== "string" || value.apiUrl !== null && typeof value.apiUrl !== "string" || typeof value.revision !== "string") {
|
|
921
|
+
return null;
|
|
922
|
+
}
|
|
923
|
+
const config = {
|
|
924
|
+
nodePath: value.nodePath,
|
|
925
|
+
daemonPath: value.daemonPath,
|
|
926
|
+
runtimeVersion: value.runtimeVersion,
|
|
927
|
+
...value.apiUrl === null ? {} : { apiUrl: value.apiUrl }
|
|
928
|
+
};
|
|
929
|
+
const expected = generateDaemonLauncher(config);
|
|
930
|
+
return expected.revision === value.revision && expected.content === content ? config : null;
|
|
931
|
+
} catch {
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
function readUsableDaemonConfig(path) {
|
|
936
|
+
const config = readDaemonLauncher(path);
|
|
937
|
+
if (!config) return null;
|
|
938
|
+
const manifest = readRuntimeManifest(join(dirname(config.daemonPath), "manifest.json"));
|
|
939
|
+
try {
|
|
940
|
+
accessSync(config.nodePath, constants.X_OK);
|
|
941
|
+
return manifest?.version === config.runtimeVersion && manifest.nodePath === config.nodePath && manifest.daemonSha256 === sha256File(config.daemonPath) ? config : null;
|
|
942
|
+
} catch {
|
|
943
|
+
return null;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
824
946
|
function xmlEscape(value) {
|
|
825
947
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
826
948
|
}
|
|
827
949
|
function generateLaunchAgentPlist(config) {
|
|
828
|
-
const apiUrl = config.apiUrl ? `
|
|
829
|
-
<key>PRIM_API_URL</key>
|
|
830
|
-
<string>${xmlEscape(config.apiUrl)}</string>` : "";
|
|
831
950
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
832
951
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
833
952
|
<plist version="1.0">
|
|
834
953
|
<dict>
|
|
835
954
|
<key>Label</key>
|
|
836
|
-
<string>${LAUNCHD_LABEL}</string>
|
|
955
|
+
<string>${xmlEscape(config.label ?? LAUNCHD_LABEL)}</string>
|
|
837
956
|
<key>ProgramArguments</key>
|
|
838
957
|
<array>
|
|
839
|
-
<string>${xmlEscape(config.
|
|
840
|
-
<string>${xmlEscape(config.daemonPath)}</string>
|
|
958
|
+
<string>${xmlEscape(config.launcherPath)}</string>
|
|
841
959
|
</array>
|
|
842
960
|
<key>RunAtLoad</key>
|
|
843
961
|
<true/>
|
|
@@ -847,6 +965,8 @@ function generateLaunchAgentPlist(config) {
|
|
|
847
965
|
<integer>10</integer>
|
|
848
966
|
<key>ProcessType</key>
|
|
849
967
|
<string>Background</string>
|
|
968
|
+
<key>ExitTimeOut</key>
|
|
969
|
+
<integer>5</integer>
|
|
850
970
|
<key>Umask</key>
|
|
851
971
|
<integer>63</integer>
|
|
852
972
|
<key>WorkingDirectory</key>
|
|
@@ -855,11 +975,6 @@ function generateLaunchAgentPlist(config) {
|
|
|
855
975
|
<string>${xmlEscape(config.logPath)}</string>
|
|
856
976
|
<key>StandardErrorPath</key>
|
|
857
977
|
<string>${xmlEscape(config.logPath)}</string>
|
|
858
|
-
<key>EnvironmentVariables</key>
|
|
859
|
-
<dict>
|
|
860
|
-
<key>PRIM_RUNTIME_VERSION</key>
|
|
861
|
-
<string>${xmlEscape(config.runtimeVersion)}</string>${apiUrl}
|
|
862
|
-
</dict>
|
|
863
978
|
</dict>
|
|
864
979
|
</plist>
|
|
865
980
|
`;
|
|
@@ -867,45 +982,57 @@ function generateLaunchAgentPlist(config) {
|
|
|
867
982
|
function launchdPaths(options = {}) {
|
|
868
983
|
const home = options.homeDir ?? homedir();
|
|
869
984
|
const uid = options.uid ?? process.getuid?.();
|
|
985
|
+
const label = options.label ?? LAUNCHD_LABEL;
|
|
870
986
|
if (uid === void 0) throw new Error("cannot determine uid for launchd user domain");
|
|
871
987
|
return {
|
|
872
988
|
domainTarget: `gui/${uid}`,
|
|
873
|
-
serviceTarget: `gui/${uid}/${
|
|
874
|
-
plistPath: join(home, "Library", "LaunchAgents", `${
|
|
989
|
+
serviceTarget: `gui/${uid}/${label}`,
|
|
990
|
+
plistPath: join(home, "Library", "LaunchAgents", `${label}.plist`),
|
|
875
991
|
logPath: join(home, ".config", "prim", "daemon.log")
|
|
876
992
|
};
|
|
877
993
|
}
|
|
878
|
-
var runLaunchctl = (args) => {
|
|
879
|
-
const result = spawnSync("/bin/launchctl", args, { encoding: "utf8" });
|
|
994
|
+
var runLaunchctl = (args, timeoutMs2 = COMMAND_TIMEOUT_MS) => {
|
|
995
|
+
const result = spawnSync("/bin/launchctl", args, { encoding: "utf8", timeout: timeoutMs2 });
|
|
996
|
+
const error = result.error;
|
|
880
997
|
return {
|
|
881
998
|
status: result.status,
|
|
882
999
|
stdout: result.stdout ?? "",
|
|
883
|
-
stderr:
|
|
1000
|
+
stderr: error?.message ?? result.stderr ?? "",
|
|
1001
|
+
timedOut: error?.code === "ETIMEDOUT"
|
|
884
1002
|
};
|
|
885
1003
|
};
|
|
886
1004
|
function parseLaunchdService(result) {
|
|
887
|
-
if (result.status
|
|
1005
|
+
if (result.status === SERVICE_NOT_FOUND) return { loaded: false };
|
|
1006
|
+
if (result.status !== 0) throw new LaunchctlError("print", result);
|
|
888
1007
|
const state = /^\s*state = (.+?)\s*$/m.exec(result.stdout)?.[1];
|
|
889
1008
|
const rawPid = /^\s*pid = (\d+)\s*$/m.exec(result.stdout)?.[1];
|
|
1009
|
+
const program2 = /^\s*program = (.+?)\s*$/m.exec(result.stdout)?.[1];
|
|
1010
|
+
if (!program2) throw new Error("launchctl print did not report the registered program");
|
|
890
1011
|
const pid = rawPid ? Number(rawPid) : void 0;
|
|
891
|
-
return { loaded: true, state, pid,
|
|
1012
|
+
return { loaded: true, state, pid, program: program2 };
|
|
892
1013
|
}
|
|
893
1014
|
function getLaunchdService(options = {}) {
|
|
894
|
-
|
|
895
|
-
|
|
1015
|
+
return parseLaunchdService(
|
|
1016
|
+
(options.runner ?? runLaunchctl)(
|
|
1017
|
+
["print", launchdPaths(options).serviceTarget],
|
|
1018
|
+
options.timeoutMs ?? COMMAND_TIMEOUT_MS
|
|
1019
|
+
)
|
|
1020
|
+
);
|
|
896
1021
|
}
|
|
897
|
-
function atomicWrite(path, content, mode) {
|
|
1022
|
+
function atomicWrite(path, content, mode, validate) {
|
|
898
1023
|
try {
|
|
899
1024
|
if (readFileSync2(path, "utf8") === content) {
|
|
900
1025
|
chmodSync(path, mode);
|
|
1026
|
+
validate?.(path, COMMAND_TIMEOUT_MS);
|
|
901
1027
|
return false;
|
|
902
1028
|
}
|
|
903
1029
|
} catch {
|
|
904
1030
|
}
|
|
905
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
1031
|
+
mkdirSync(dirname(path), { recursive: true, mode: RUNTIME_DIR_MODE });
|
|
906
1032
|
const temp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
907
1033
|
writeFileSync(temp, content, { encoding: "utf8", mode, flag: "wx" });
|
|
908
1034
|
try {
|
|
1035
|
+
validate?.(temp, COMMAND_TIMEOUT_MS);
|
|
909
1036
|
renameSync(temp, path);
|
|
910
1037
|
} catch (error) {
|
|
911
1038
|
rmSync(temp, { force: true });
|
|
@@ -913,20 +1040,25 @@ function atomicWrite(path, content, mode) {
|
|
|
913
1040
|
}
|
|
914
1041
|
return true;
|
|
915
1042
|
}
|
|
916
|
-
function launchctlOrThrow(runner, args, operation) {
|
|
917
|
-
const result = runner(args);
|
|
918
|
-
if (result.status !== 0) {
|
|
919
|
-
const detail = result.stderr.trim() || result.stdout.trim() || `exit ${String(result.status)}`;
|
|
920
|
-
throw new Error(`launchctl ${operation} failed: ${detail}`);
|
|
921
|
-
}
|
|
922
|
-
return result;
|
|
923
|
-
}
|
|
924
1043
|
function defaultSleep(ms) {
|
|
925
1044
|
return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
926
1045
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1046
|
+
function validatePlist(path, timeoutMs2) {
|
|
1047
|
+
if (process.platform !== "darwin") return;
|
|
1048
|
+
const result = spawnSync("/usr/bin/plutil", ["-lint", path], {
|
|
1049
|
+
encoding: "utf8",
|
|
1050
|
+
timeout: timeoutMs2
|
|
1051
|
+
});
|
|
1052
|
+
if (result.status !== 0) {
|
|
1053
|
+
throw new Error(
|
|
1054
|
+
`invalid launchd plist: ${result.error?.message ?? result.stderr?.trim() ?? result.stdout?.trim() ?? `exit ${String(result.status)}`}`
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
async function inspectDaemon(timeoutMs2) {
|
|
1059
|
+
const value = await daemonRequest("status_snapshot", {}, { timeoutMs: timeoutMs2 });
|
|
1060
|
+
if (!value || !Number.isInteger(value.pid) || (value.pid ?? 0) <= 0) return null;
|
|
1061
|
+
return value;
|
|
930
1062
|
}
|
|
931
1063
|
function processIsAlive(pid) {
|
|
932
1064
|
try {
|
|
@@ -936,11 +1068,31 @@ function processIsAlive(pid) {
|
|
|
936
1068
|
return error.code === "EPERM";
|
|
937
1069
|
}
|
|
938
1070
|
}
|
|
1071
|
+
function canonicalProspectivePath(path) {
|
|
1072
|
+
const suffix = [];
|
|
1073
|
+
let ancestor = resolve(path);
|
|
1074
|
+
while (!existsSync2(ancestor)) {
|
|
1075
|
+
const parent = dirname(ancestor);
|
|
1076
|
+
if (parent === ancestor) return resolve(path);
|
|
1077
|
+
suffix.unshift(basename(ancestor));
|
|
1078
|
+
ancestor = parent;
|
|
1079
|
+
}
|
|
1080
|
+
try {
|
|
1081
|
+
return join(realpathSync(ancestor), ...suffix);
|
|
1082
|
+
} catch {
|
|
1083
|
+
return resolve(path);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
939
1086
|
async function withDaemonLifecycleLock(operation, options = {}) {
|
|
940
|
-
const
|
|
941
|
-
|
|
1087
|
+
const paths = daemonControlPaths(options);
|
|
1088
|
+
const sameLock = canonicalProspectivePath(paths.lifecycleLockDir) === canonicalProspectivePath(paths.legacyLifecycleLockDir);
|
|
1089
|
+
return withFileLock(
|
|
1090
|
+
paths.lifecycleLockDir,
|
|
1091
|
+
() => sameLock ? operation() : withFileLock(paths.legacyLifecycleLockDir, operation, options.lifecycleLock),
|
|
1092
|
+
options.lifecycleLock
|
|
1093
|
+
);
|
|
942
1094
|
}
|
|
943
|
-
async function stopVerifiedLegacyDaemon(homeDir) {
|
|
1095
|
+
async function stopVerifiedLegacyDaemon(homeDir, deadlineMs, nowMs, sleep2) {
|
|
944
1096
|
const pidPath = join(homeDir, ".config", "prim", "daemon.pid");
|
|
945
1097
|
let pid;
|
|
946
1098
|
try {
|
|
@@ -948,122 +1100,314 @@ async function stopVerifiedLegacyDaemon(homeDir) {
|
|
|
948
1100
|
} catch {
|
|
949
1101
|
throw new Error("an unsupervised daemon answered, but it has no verifiable pidfile");
|
|
950
1102
|
}
|
|
951
|
-
|
|
1103
|
+
if (nowMs() >= deadlineMs) throw new Error("legacy verification exceeded the lifecycle deadline");
|
|
1104
|
+
const snapshot = await daemonRequest(
|
|
1105
|
+
"status_snapshot",
|
|
1106
|
+
{},
|
|
1107
|
+
{
|
|
1108
|
+
timeoutMs: timeoutMs(nowMs, deadlineMs, 500)
|
|
1109
|
+
}
|
|
1110
|
+
);
|
|
952
1111
|
if (!Number.isInteger(pid) || pid <= 0 || !processIsAlive(pid) || snapshot?.pid !== pid) {
|
|
953
1112
|
throw new Error("refusing to signal an unsupervised daemon whose pid cannot be verified");
|
|
954
1113
|
}
|
|
1114
|
+
if (nowMs() >= deadlineMs) throw new Error("legacy verification exceeded the lifecycle deadline");
|
|
955
1115
|
process.kill(pid, "SIGTERM");
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
await defaultSleep(100);
|
|
1116
|
+
while (nowMs() < deadlineMs && processIsAlive(pid)) {
|
|
1117
|
+
await sleep2(Math.min(READY_POLL_MS, deadlineMs - nowMs()));
|
|
959
1118
|
}
|
|
960
1119
|
if (processIsAlive(pid)) {
|
|
961
|
-
throw new Error(`verified legacy daemon pid=${pid} did not stop
|
|
1120
|
+
throw new Error(`verified legacy daemon pid=${pid} did not stop before the lifecycle deadline`);
|
|
962
1121
|
}
|
|
963
1122
|
return true;
|
|
964
1123
|
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1124
|
+
function timeoutMs(nowMs, deadlineMs, cap = COMMAND_TIMEOUT_MS) {
|
|
1125
|
+
return Math.max(1, Math.min(cap, Math.ceil(deadlineMs - nowMs())));
|
|
1126
|
+
}
|
|
1127
|
+
function desiredDaemonIsRunning(transition, observed) {
|
|
1128
|
+
const { service, identity } = observed;
|
|
1129
|
+
return Boolean(
|
|
1130
|
+
service.loaded && service.program === transition.launcherPath && service.pid !== void 0 && identity?.pid === service.pid && identity.version === transition.version && identity.launchRevision === transition.revision
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
function getService(transition) {
|
|
1134
|
+
return parseLaunchdService(
|
|
1135
|
+
transition.runner(
|
|
1136
|
+
["print", transition.paths.serviceTarget],
|
|
1137
|
+
timeoutMs(transition.nowMs, transition.deadlineMs)
|
|
1138
|
+
)
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
async function observe(transition) {
|
|
1142
|
+
const service = getService(transition);
|
|
1143
|
+
const identity = service.loaded && service.program === transition.launcherPath ? await transition.inspect(
|
|
1144
|
+
timeoutMs(transition.nowMs, transition.deadlineMs, READY_PROBE_TIMEOUT_MS)
|
|
1145
|
+
) : null;
|
|
1146
|
+
return { service, identity };
|
|
1147
|
+
}
|
|
1148
|
+
async function tryObserve(transition) {
|
|
1149
|
+
try {
|
|
1150
|
+
return await observe(transition);
|
|
1151
|
+
} catch (error) {
|
|
1152
|
+
if (!(error instanceof LaunchctlError)) throw error;
|
|
1153
|
+
return null;
|
|
970
1154
|
}
|
|
971
|
-
|
|
1155
|
+
}
|
|
1156
|
+
async function bootstrap(transition, oldProgram) {
|
|
1157
|
+
let retryMs = READY_POLL_MS;
|
|
1158
|
+
let lastError = null;
|
|
1159
|
+
while (transition.nowMs() < transition.deadlineMs) {
|
|
1160
|
+
const result = transition.runner(
|
|
1161
|
+
["bootstrap", transition.paths.domainTarget, transition.paths.plistPath],
|
|
1162
|
+
timeoutMs(transition.nowMs, transition.deadlineMs)
|
|
1163
|
+
);
|
|
1164
|
+
if (result.status === 0) return;
|
|
1165
|
+
const error = new LaunchctlError("bootstrap", result);
|
|
1166
|
+
lastError = error;
|
|
1167
|
+
const observed = await tryObserve(transition);
|
|
1168
|
+
const retryable = error.status === TRANSITION_IN_PROGRESS || error.timedOut;
|
|
1169
|
+
if (observed?.service.loaded) {
|
|
1170
|
+
if (observed.service.program === transition.launcherPath) {
|
|
1171
|
+
if (desiredDaemonIsRunning(transition, observed)) return;
|
|
1172
|
+
if (retryable) return;
|
|
1173
|
+
} else if (observed.service.program !== oldProgram) {
|
|
1174
|
+
throw new Error(
|
|
1175
|
+
`refusing to replace unexpected launchd program ${observed.service.program}`
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
if (!retryable) throw error;
|
|
1180
|
+
const remaining = transition.deadlineMs - transition.nowMs();
|
|
1181
|
+
if (remaining <= 0) break;
|
|
1182
|
+
await transition.sleep(Math.min(retryMs, remaining));
|
|
1183
|
+
retryMs = Math.min(retryMs * 2, RETRY_MAX_MS);
|
|
1184
|
+
}
|
|
1185
|
+
throw new Error(
|
|
1186
|
+
`launchctl bootstrap did not converge within ${TRANSITION_TIMEOUT_MS}ms${lastError ? `: ${lastError.message}` : ""}`
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
async function reloadDefinition(transition, oldProgram) {
|
|
1190
|
+
const result = transition.runner(
|
|
1191
|
+
["bootout", transition.paths.serviceTarget],
|
|
1192
|
+
timeoutMs(transition.nowMs, transition.deadlineMs)
|
|
1193
|
+
);
|
|
1194
|
+
if (result.status !== 0) {
|
|
1195
|
+
const error = new LaunchctlError("bootout", result);
|
|
1196
|
+
const observed = await tryObserve(transition);
|
|
1197
|
+
const retryable = error.status === TRANSITION_IN_PROGRESS || error.timedOut;
|
|
1198
|
+
if (observed?.service.loaded) {
|
|
1199
|
+
if (observed.service.program === transition.launcherPath) {
|
|
1200
|
+
if (desiredDaemonIsRunning(transition, observed)) return;
|
|
1201
|
+
if (retryable) return;
|
|
1202
|
+
}
|
|
1203
|
+
if (observed.service.program !== oldProgram || !retryable) throw error;
|
|
1204
|
+
} else if (!observed && !retryable) {
|
|
1205
|
+
throw error;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
await bootstrap(transition, oldProgram);
|
|
1209
|
+
}
|
|
1210
|
+
async function waitForDesired(transition, repairMismatch = false) {
|
|
1211
|
+
let observed = { service: { loaded: false }, identity: null };
|
|
1212
|
+
let repaired = false;
|
|
1213
|
+
do {
|
|
1214
|
+
const current = await tryObserve(transition);
|
|
1215
|
+
if (current) {
|
|
1216
|
+
observed = current;
|
|
1217
|
+
if (desiredDaemonIsRunning(transition, observed)) return observed;
|
|
1218
|
+
if (observed.service.loaded && observed.service.program !== transition.launcherPath) {
|
|
1219
|
+
throw new Error(`launchd registered unexpected program ${observed.service.program}`);
|
|
1220
|
+
}
|
|
1221
|
+
if (repairMismatch && !repaired && observed.service.loaded && observed.service.program === transition.launcherPath && observed.identity) {
|
|
1222
|
+
repaired = true;
|
|
1223
|
+
await kickstart(transition);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
const remaining = transition.deadlineMs - transition.nowMs();
|
|
1227
|
+
if (remaining <= 0) break;
|
|
1228
|
+
await transition.sleep(Math.min(READY_POLL_MS, remaining));
|
|
1229
|
+
} while (transition.nowMs() < transition.deadlineMs);
|
|
1230
|
+
return observed;
|
|
1231
|
+
}
|
|
1232
|
+
async function kickstart(transition) {
|
|
1233
|
+
const result = transition.runner(
|
|
1234
|
+
["kickstart", "-k", transition.paths.serviceTarget],
|
|
1235
|
+
timeoutMs(transition.nowMs, transition.deadlineMs)
|
|
1236
|
+
);
|
|
1237
|
+
if (result.status === 0) return;
|
|
1238
|
+
const error = new LaunchctlError("kickstart", result);
|
|
1239
|
+
const observed = await tryObserve(transition);
|
|
1240
|
+
if (observed && desiredDaemonIsRunning(transition, observed)) return;
|
|
1241
|
+
if (!error.timedOut) throw error;
|
|
1242
|
+
const ready = await waitForDesired(transition);
|
|
1243
|
+
if (!desiredDaemonIsRunning(transition, ready)) throw error;
|
|
1244
|
+
}
|
|
1245
|
+
async function waitForAbsence(context) {
|
|
1246
|
+
let lastError = null;
|
|
1247
|
+
while (context.nowMs() < context.deadlineMs) {
|
|
1248
|
+
try {
|
|
1249
|
+
const service = getLaunchdService({
|
|
1250
|
+
...context.options,
|
|
1251
|
+
runner: context.runner,
|
|
1252
|
+
timeoutMs: timeoutMs(context.nowMs, context.deadlineMs)
|
|
1253
|
+
});
|
|
1254
|
+
if (!service.loaded) {
|
|
1255
|
+
const identity = await context.inspect(
|
|
1256
|
+
timeoutMs(context.nowMs, context.deadlineMs, READY_PROBE_TIMEOUT_MS)
|
|
1257
|
+
);
|
|
1258
|
+
const socketReleased = context.previousPid === void 0 || identity?.pid !== context.previousPid;
|
|
1259
|
+
const processExited = context.previousPid === void 0 || !processIsAlive(context.previousPid);
|
|
1260
|
+
if (socketReleased && processExited) return;
|
|
1261
|
+
}
|
|
1262
|
+
} catch (error) {
|
|
1263
|
+
if (!(error instanceof LaunchctlError)) throw error;
|
|
1264
|
+
lastError = error;
|
|
1265
|
+
}
|
|
1266
|
+
const remaining = context.deadlineMs - context.nowMs();
|
|
1267
|
+
if (remaining <= 0) break;
|
|
1268
|
+
await context.sleep(Math.min(READY_POLL_MS, remaining));
|
|
1269
|
+
}
|
|
1270
|
+
throw new Error(
|
|
1271
|
+
`launchctl bootout did not converge within ${TRANSITION_TIMEOUT_MS}ms${lastError ? `: ${lastError.message}` : ""}`
|
|
1272
|
+
);
|
|
972
1273
|
}
|
|
973
1274
|
function daemonExplicitlyDisabled(options = {}) {
|
|
974
|
-
|
|
1275
|
+
const paths = daemonControlPaths(options);
|
|
1276
|
+
return existsSync2(paths.disabledMarker) || existsSync2(paths.legacyDisabledMarker);
|
|
975
1277
|
}
|
|
976
1278
|
function setDaemonExplicitlyDisabled(disabled, options = {}) {
|
|
977
|
-
const
|
|
1279
|
+
const paths = daemonControlPaths(options);
|
|
978
1280
|
if (!disabled) {
|
|
979
|
-
rmSync(
|
|
1281
|
+
rmSync(paths.disabledMarker, { force: true });
|
|
1282
|
+
rmSync(paths.legacyDisabledMarker, { force: true });
|
|
980
1283
|
return;
|
|
981
1284
|
}
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
chmodSync(marker, RUNTIME_FILE_MODE);
|
|
1285
|
+
const content = "disabled by `prim daemon stop`\n";
|
|
1286
|
+
if (paths.legacyDisabledMarker !== paths.disabledMarker) {
|
|
1287
|
+
atomicWrite(paths.legacyDisabledMarker, content, RUNTIME_FILE_MODE);
|
|
1288
|
+
}
|
|
1289
|
+
atomicWrite(paths.disabledMarker, content, RUNTIME_FILE_MODE);
|
|
988
1290
|
}
|
|
989
1291
|
async function ensureMacDaemon(options = {}) {
|
|
990
1292
|
return withDaemonLifecycleLock(() => ensureMacDaemonLocked(options), options);
|
|
991
1293
|
}
|
|
992
1294
|
async function ensureMacDaemonLocked(options) {
|
|
1295
|
+
const runner = options.runner ?? runLaunchctl;
|
|
1296
|
+
let service = getLaunchdService({ ...options, runner });
|
|
993
1297
|
if (!options.explicitlyStarted && daemonExplicitlyDisabled(options)) {
|
|
1298
|
+
const control2 = daemonControlPaths(options);
|
|
1299
|
+
if (!existsSync2(control2.disabledMarker)) {
|
|
1300
|
+
atomicWrite(control2.disabledMarker, "disabled by `prim daemon stop`\n", RUNTIME_FILE_MODE);
|
|
1301
|
+
}
|
|
994
1302
|
return {
|
|
995
1303
|
state: "disabled",
|
|
996
1304
|
action: "none",
|
|
997
1305
|
runtimeChanged: false,
|
|
998
1306
|
plistChanged: false,
|
|
999
1307
|
responding: false,
|
|
1000
|
-
service
|
|
1308
|
+
service
|
|
1001
1309
|
};
|
|
1002
1310
|
}
|
|
1003
1311
|
if (options.explicitlyStarted) setDaemonExplicitlyDisabled(false, options);
|
|
1004
|
-
const
|
|
1312
|
+
const control = daemonControlPaths(options);
|
|
1313
|
+
const requestedSource = options.daemonSource ?? binFile("prim-daemon-server");
|
|
1314
|
+
if (!options.version && !requestedSource) {
|
|
1315
|
+
throw new Error("cannot determine runtime version: daemon is unavailable");
|
|
1316
|
+
}
|
|
1317
|
+
const requestedVersion = options.version ?? findPackageVersion(requestedSource);
|
|
1318
|
+
const selected = options.explicitlyStarted ? null : readDaemonLauncher(control.launcher);
|
|
1319
|
+
const precedence = selected ? compareSemver(selected.runtimeVersion, requestedVersion) : null;
|
|
1320
|
+
const retainSelected = selected !== null && (precedence === 1 || precedence === void 0 && selected.runtimeVersion !== requestedVersion);
|
|
1321
|
+
let runtime;
|
|
1322
|
+
let daemonConfig;
|
|
1323
|
+
const configuredApiUrl = (options.env ?? process.env).PRIM_API_URL;
|
|
1324
|
+
const apiUrl = configuredApiUrl ? normalizeApiUrl(configuredApiUrl) || void 0 : void 0;
|
|
1325
|
+
if (retainSelected && selected) {
|
|
1326
|
+
const usable = readUsableDaemonConfig(control.launcher);
|
|
1327
|
+
if (!usable) {
|
|
1328
|
+
throw new Error(
|
|
1329
|
+
`selected daemon runtime ${selected.runtimeVersion} is unavailable or corrupt; refusing automatic downgrade to ${requestedVersion}`
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
daemonConfig = {
|
|
1333
|
+
nodePath: usable.nodePath,
|
|
1334
|
+
daemonPath: usable.daemonPath,
|
|
1335
|
+
runtimeVersion: usable.runtimeVersion,
|
|
1336
|
+
...apiUrl ? { apiUrl } : {}
|
|
1337
|
+
};
|
|
1338
|
+
} else {
|
|
1339
|
+
runtime = stageRuntime({ ...options, version: requestedVersion });
|
|
1340
|
+
daemonConfig = {
|
|
1341
|
+
nodePath: runtime.manifest.nodePath,
|
|
1342
|
+
daemonPath: runtime.daemonPath,
|
|
1343
|
+
runtimeVersion: runtime.manifest.version,
|
|
1344
|
+
...apiUrl ? { apiUrl } : {}
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1005
1347
|
const servicePaths = launchdPaths(options);
|
|
1006
1348
|
mkdirSync(dirname(servicePaths.logPath), { recursive: true, mode: RUNTIME_DIR_MODE });
|
|
1007
1349
|
writeFileSync(servicePaths.logPath, "", { mode: RUNTIME_FILE_MODE, flag: "a" });
|
|
1008
1350
|
chmodSync(servicePaths.logPath, RUNTIME_FILE_MODE);
|
|
1351
|
+
const launcher = generateDaemonLauncher(daemonConfig);
|
|
1352
|
+
atomicWrite(control.launcher, launcher.content, RUNTIME_LAUNCHER_MODE);
|
|
1009
1353
|
const plist = generateLaunchAgentPlist({
|
|
1010
|
-
|
|
1011
|
-
daemonPath: runtime.paths.daemonEntry,
|
|
1012
|
-
runtimeVersion: runtime.manifest.version,
|
|
1354
|
+
launcherPath: control.launcher,
|
|
1013
1355
|
logPath: servicePaths.logPath,
|
|
1014
1356
|
workingDirectory: options.homeDir ?? homedir(),
|
|
1015
|
-
|
|
1357
|
+
label: options.label
|
|
1016
1358
|
});
|
|
1017
|
-
const plistChanged = atomicWrite(
|
|
1018
|
-
|
|
1019
|
-
|
|
1359
|
+
const plistChanged = atomicWrite(
|
|
1360
|
+
servicePaths.plistPath,
|
|
1361
|
+
plist,
|
|
1362
|
+
PLIST_FILE_MODE,
|
|
1363
|
+
options.validatePlist ?? validatePlist
|
|
1364
|
+
);
|
|
1365
|
+
const sleep2 = options.sleep ?? defaultSleep;
|
|
1366
|
+
const nowMs = options.nowMs ?? performance.now.bind(performance);
|
|
1367
|
+
const deadlineMs = nowMs() + TRANSITION_TIMEOUT_MS;
|
|
1368
|
+
const inspect = options.inspectDaemon ?? inspectDaemon;
|
|
1369
|
+
const transition = {
|
|
1370
|
+
paths: servicePaths,
|
|
1371
|
+
runner,
|
|
1372
|
+
inspect,
|
|
1373
|
+
sleep: sleep2,
|
|
1374
|
+
nowMs,
|
|
1375
|
+
deadlineMs,
|
|
1376
|
+
launcherPath: control.launcher,
|
|
1377
|
+
version: daemonConfig.runtimeVersion,
|
|
1378
|
+
revision: launcher.revision
|
|
1379
|
+
};
|
|
1380
|
+
const migrateLegacy = options.migrateLegacy ?? (() => stopVerifiedLegacyDaemon(options.homeDir ?? homedir(), deadlineMs, nowMs, sleep2));
|
|
1020
1381
|
let action = "none";
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
"bootstrap"
|
|
1027
|
-
);
|
|
1028
|
-
action = "reload";
|
|
1029
|
-
} else if (!service.loaded) {
|
|
1030
|
-
if (await (options.probe ?? daemonIsLive)(READY_PROBE_TIMEOUT_MS)) {
|
|
1031
|
-
await (options.migrateLegacy ?? (() => stopVerifiedLegacyDaemon(options.homeDir ?? homedir())))();
|
|
1382
|
+
const observed = await observe(transition);
|
|
1383
|
+
service = observed.service;
|
|
1384
|
+
if (!service.loaded) {
|
|
1385
|
+
if (await inspect(timeoutMs(nowMs, deadlineMs, READY_PROBE_TIMEOUT_MS))) {
|
|
1386
|
+
await migrateLegacy();
|
|
1032
1387
|
}
|
|
1033
|
-
|
|
1034
|
-
runner,
|
|
1035
|
-
["bootstrap", servicePaths.domainTarget, servicePaths.plistPath],
|
|
1036
|
-
"bootstrap"
|
|
1037
|
-
);
|
|
1388
|
+
await bootstrap(transition);
|
|
1038
1389
|
action = "bootstrap";
|
|
1039
|
-
} else {
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
const responding = await waitForReady(
|
|
1052
|
-
options.probe ?? daemonIsLive,
|
|
1053
|
-
options.sleep ?? defaultSleep
|
|
1054
|
-
);
|
|
1055
|
-
service = getLaunchdService({ ...options, runner });
|
|
1056
|
-
const socketPid = responding ? await (options.identify ?? identifyDaemon)(READY_PROBE_TIMEOUT_MS) : null;
|
|
1057
|
-
const running = responding && service.loaded && service.pid !== void 0 && socketPid !== null && service.pid === socketPid;
|
|
1390
|
+
} else if (service.program !== control.launcher) {
|
|
1391
|
+
if (!service.program) throw new Error("launchd did not report the registered program");
|
|
1392
|
+
await reloadDefinition(transition, service.program);
|
|
1393
|
+
action = "reload";
|
|
1394
|
+
} else if (options.forceRestart || !desiredDaemonIsRunning(transition, observed)) {
|
|
1395
|
+
await kickstart(transition);
|
|
1396
|
+
action = "kickstart";
|
|
1397
|
+
}
|
|
1398
|
+
const ready = await waitForDesired(transition, action === "bootstrap" || action === "reload");
|
|
1399
|
+
service = ready.service;
|
|
1400
|
+
const responding = ready.identity !== null;
|
|
1401
|
+
const running = desiredDaemonIsRunning(transition, ready);
|
|
1058
1402
|
return {
|
|
1059
1403
|
state: running ? "running" : "unhealthy",
|
|
1060
1404
|
action,
|
|
1061
|
-
runtimeChanged: runtime
|
|
1405
|
+
runtimeChanged: runtime?.changed ?? false,
|
|
1062
1406
|
plistChanged,
|
|
1063
1407
|
responding,
|
|
1064
|
-
...
|
|
1408
|
+
...ready.identity ? { socketPid: ready.identity.pid } : {},
|
|
1065
1409
|
service,
|
|
1066
|
-
runtime
|
|
1410
|
+
...runtime ? { runtime } : {}
|
|
1067
1411
|
};
|
|
1068
1412
|
}
|
|
1069
1413
|
async function bootoutMacDaemon(options = {}) {
|
|
@@ -1072,14 +1416,39 @@ async function bootoutMacDaemon(options = {}) {
|
|
|
1072
1416
|
async function bootoutMacDaemonLocked(options) {
|
|
1073
1417
|
setDaemonExplicitlyDisabled(true, options);
|
|
1074
1418
|
const runner = options.runner ?? runLaunchctl;
|
|
1419
|
+
const inspect = options.inspectDaemon ?? inspectDaemon;
|
|
1420
|
+
const sleep2 = options.sleep ?? defaultSleep;
|
|
1421
|
+
const nowMs = options.nowMs ?? performance.now.bind(performance);
|
|
1422
|
+
const deadlineMs = nowMs() + TRANSITION_TIMEOUT_MS;
|
|
1075
1423
|
const paths = launchdPaths(options);
|
|
1424
|
+
const migrateLegacy = options.migrateLegacy ?? (() => stopVerifiedLegacyDaemon(options.homeDir ?? homedir(), deadlineMs, nowMs, sleep2));
|
|
1076
1425
|
const service = getLaunchdService({ ...options, runner });
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1426
|
+
const wasLoaded = service.loaded;
|
|
1427
|
+
if (wasLoaded) {
|
|
1428
|
+
const result = runner(["bootout", paths.serviceTarget], timeoutMs(nowMs, deadlineMs));
|
|
1429
|
+
if (result.status !== 0) {
|
|
1430
|
+
const error = new LaunchctlError("bootout", result);
|
|
1431
|
+
const after = getLaunchdService({
|
|
1432
|
+
...options,
|
|
1433
|
+
runner,
|
|
1434
|
+
timeoutMs: timeoutMs(nowMs, deadlineMs)
|
|
1435
|
+
});
|
|
1436
|
+
if (!error.timedOut && error.status !== TRANSITION_IN_PROGRESS && after.loaded) {
|
|
1437
|
+
throw error;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
await waitForAbsence({
|
|
1441
|
+
options,
|
|
1442
|
+
runner,
|
|
1443
|
+
inspect,
|
|
1444
|
+
sleep: sleep2,
|
|
1445
|
+
nowMs,
|
|
1446
|
+
deadlineMs,
|
|
1447
|
+
previousPid: service.pid
|
|
1448
|
+
});
|
|
1080
1449
|
}
|
|
1081
|
-
const legacyStopped = await (
|
|
1082
|
-
return { wasLoaded
|
|
1450
|
+
const legacyStopped = await inspect(timeoutMs(nowMs, deadlineMs, READY_PROBE_TIMEOUT_MS)) ? await migrateLegacy() : false;
|
|
1451
|
+
return { wasLoaded, legacyStopped, service };
|
|
1083
1452
|
}
|
|
1084
1453
|
|
|
1085
1454
|
// src/commands/claude-install.ts
|
|
@@ -1715,7 +2084,7 @@ var LOG_FILE_MODE = 384;
|
|
|
1715
2084
|
var STOP_TIMEOUT_MS = 5e3;
|
|
1716
2085
|
var STOP_POLL_MS = 100;
|
|
1717
2086
|
var STATUS_PROBE_TIMEOUT_MS = 500;
|
|
1718
|
-
var
|
|
2087
|
+
var READY_TIMEOUT_MS = 5e3;
|
|
1719
2088
|
var READY_POLL_MS2 = 100;
|
|
1720
2089
|
var READY_PROBE_TIMEOUT_MS2 = 250;
|
|
1721
2090
|
var HEALTHY_TIMEOUT_MS = 3e4;
|
|
@@ -1729,13 +2098,6 @@ function formatDaemonLifecycleMessage(message, decisionIngestion) {
|
|
|
1729
2098
|
function formatCurrentDaemonLifecycleMessage(message) {
|
|
1730
2099
|
return formatDaemonLifecycleMessage(message, decisionIngestionStatus(process.cwd()));
|
|
1731
2100
|
}
|
|
1732
|
-
var MAX_HEALTH_ERROR_LENGTH = 240;
|
|
1733
|
-
function boundedHealthError(value) {
|
|
1734
|
-
if (!value) return void 0;
|
|
1735
|
-
const clean = stripControlChars(value).replace(/\s+/g, " ").trim();
|
|
1736
|
-
if (!clean) return void 0;
|
|
1737
|
-
return clean.length <= MAX_HEALTH_ERROR_LENGTH ? clean : `${clean.slice(0, MAX_HEALTH_ERROR_LENGTH - 1)}\u2026`;
|
|
1738
|
-
}
|
|
1739
2101
|
function daemonDegradedReason(snapshot) {
|
|
1740
2102
|
if (!snapshot || snapshot.healthy !== false) return void 0;
|
|
1741
2103
|
if (snapshot.needsReauth) {
|
|
@@ -1795,8 +2157,8 @@ function openDaemonLog(configDir = CONFIG_DIR) {
|
|
|
1795
2157
|
}
|
|
1796
2158
|
return openSync2(join4(configDir, "daemon.log"), "a", LOG_FILE_MODE);
|
|
1797
2159
|
}
|
|
1798
|
-
async function
|
|
1799
|
-
const deadline = Date.now() +
|
|
2160
|
+
async function waitForReady() {
|
|
2161
|
+
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
1800
2162
|
while (Date.now() < deadline) {
|
|
1801
2163
|
if (await daemonIsLive(READY_PROBE_TIMEOUT_MS2)) {
|
|
1802
2164
|
return true;
|
|
@@ -1900,7 +2262,7 @@ async function detachedDaemonStart(opts) {
|
|
|
1900
2262
|
if (logFd !== void 0) {
|
|
1901
2263
|
closeSync2(logFd);
|
|
1902
2264
|
}
|
|
1903
|
-
const live = await
|
|
2265
|
+
const live = await waitForReady();
|
|
1904
2266
|
if (live) {
|
|
1905
2267
|
const after = readPidfile();
|
|
1906
2268
|
process.stderr.write(
|
|
@@ -1911,7 +2273,7 @@ async function detachedDaemonStart(opts) {
|
|
|
1911
2273
|
return;
|
|
1912
2274
|
}
|
|
1913
2275
|
process.stderr.write(
|
|
1914
|
-
`[prim] \u2717 daemon start: spawned but the socket did not respond within ${
|
|
2276
|
+
`[prim] \u2717 daemon start: spawned but the socket did not respond within ${READY_TIMEOUT_MS}ms (check that \`${DAEMON_BIN}\` resolves, and see ${LOG_PATH})
|
|
1915
2277
|
`
|
|
1916
2278
|
);
|
|
1917
2279
|
console.log(JSON.stringify({ started: false }, null, 2));
|
|
@@ -1993,8 +2355,9 @@ async function macDaemonStart(forceRestart = false) {
|
|
|
1993
2355
|
const healthy = daemonStartIsHealthy(serviceReady, snapshot, expectedVersion);
|
|
1994
2356
|
if (healthy) {
|
|
1995
2357
|
const verb = result.action === "none" ? "already running" : "started";
|
|
2358
|
+
const servicePid = result.service.loaded ? result.service.pid : void 0;
|
|
1996
2359
|
process.stderr.write(
|
|
1997
|
-
`${formatCurrentDaemonLifecycleMessage(`[prim] \u2713 daemon ${verb} under launchd (pid=${snapshot?.pid ??
|
|
2360
|
+
`${formatCurrentDaemonLifecycleMessage(`[prim] \u2713 daemon ${verb} under launchd (pid=${snapshot?.pid ?? servicePid ?? "?"})`)}
|
|
1998
2361
|
`
|
|
1999
2362
|
);
|
|
2000
2363
|
} else {
|
|
@@ -2011,7 +2374,7 @@ async function macDaemonStart(forceRestart = false) {
|
|
|
2011
2374
|
started: healthy,
|
|
2012
2375
|
supervised: true,
|
|
2013
2376
|
action: result.action,
|
|
2014
|
-
pid: snapshot?.pid ?? result.service.pid,
|
|
2377
|
+
pid: snapshot?.pid ?? (result.service.loaded ? result.service.pid : void 0),
|
|
2015
2378
|
loaded: result.service.loaded,
|
|
2016
2379
|
responding: result.responding,
|
|
2017
2380
|
healthy,
|
|
@@ -2083,6 +2446,19 @@ function classifyStatus(pidAlive, responding, snapshot, pid) {
|
|
|
2083
2446
|
return { json: { running: true, responding: true, ...snapshot }, exitCode: EXIT_OK2 };
|
|
2084
2447
|
}
|
|
2085
2448
|
function classifyLaunchdStatus(service, responding, snapshot, disabled) {
|
|
2449
|
+
if (!service) {
|
|
2450
|
+
return {
|
|
2451
|
+
json: {
|
|
2452
|
+
running: responding,
|
|
2453
|
+
responding,
|
|
2454
|
+
supervised: true,
|
|
2455
|
+
state: "unknown",
|
|
2456
|
+
disabled,
|
|
2457
|
+
...snapshot ?? {}
|
|
2458
|
+
},
|
|
2459
|
+
exitCode: EXIT_BOOTING
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
2086
2462
|
if (!service.loaded) {
|
|
2087
2463
|
if (responding) {
|
|
2088
2464
|
return {
|
|
@@ -2210,7 +2586,13 @@ async function detachedDaemonStatus() {
|
|
|
2210
2586
|
}
|
|
2211
2587
|
}
|
|
2212
2588
|
async function macDaemonStatus() {
|
|
2213
|
-
|
|
2589
|
+
let service;
|
|
2590
|
+
let launchdError;
|
|
2591
|
+
try {
|
|
2592
|
+
service = getLaunchdService();
|
|
2593
|
+
} catch (error) {
|
|
2594
|
+
launchdError = error;
|
|
2595
|
+
}
|
|
2214
2596
|
const responding = await daemonIsLive(STATUS_PROBE_TIMEOUT_MS);
|
|
2215
2597
|
const snapshot = responding ? await daemonRequest(
|
|
2216
2598
|
"status_snapshot",
|
|
@@ -2219,7 +2601,13 @@ async function macDaemonStatus() {
|
|
|
2219
2601
|
) : null;
|
|
2220
2602
|
const disabled = daemonExplicitlyDisabled();
|
|
2221
2603
|
const { json, exitCode } = classifyLaunchdStatus(service, responding, snapshot, disabled);
|
|
2222
|
-
if (!service
|
|
2604
|
+
if (!service) {
|
|
2605
|
+
const detail = boundedHealthError(
|
|
2606
|
+
launchdError instanceof Error ? launchdError.message : String(launchdError)
|
|
2607
|
+
);
|
|
2608
|
+
process.stderr.write(`[prim] \u2717 launchd status unavailable${detail ? `: ${detail}` : ""}
|
|
2609
|
+
`);
|
|
2610
|
+
} else if (!service.loaded && responding) {
|
|
2223
2611
|
process.stderr.write(
|
|
2224
2612
|
`[prim] \u25CC daemon pid=${snapshot?.pid ?? "?"} is live but not supervised by launchd
|
|
2225
2613
|
`
|
|
@@ -3359,8 +3747,13 @@ async function checkDaemon() {
|
|
|
3359
3747
|
if (process.platform === "darwin") {
|
|
3360
3748
|
try {
|
|
3361
3749
|
service = getLaunchdService();
|
|
3362
|
-
} catch {
|
|
3363
|
-
|
|
3750
|
+
} catch (error) {
|
|
3751
|
+
const detail = boundedHealthError(error instanceof Error ? error.message : String(error));
|
|
3752
|
+
return {
|
|
3753
|
+
name: "daemon",
|
|
3754
|
+
status: "fail",
|
|
3755
|
+
detail: `launchd status unavailable${detail ? `: ${detail}` : ""}`
|
|
3756
|
+
};
|
|
3364
3757
|
}
|
|
3365
3758
|
}
|
|
3366
3759
|
const snapshot = await daemonRequest(
|