@primitive.ai/prim 0.1.0-alpha.60 → 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.
|
@@ -64,6 +64,13 @@ function stripControlChars(text) {
|
|
|
64
64
|
}
|
|
65
65
|
return out;
|
|
66
66
|
}
|
|
67
|
+
var MAX_HEALTH_ERROR_LENGTH = 240;
|
|
68
|
+
function boundedHealthError(value) {
|
|
69
|
+
if (!value) return void 0;
|
|
70
|
+
const clean = stripControlChars(value).replace(/\s+/g, " ").trim();
|
|
71
|
+
if (!clean) return void 0;
|
|
72
|
+
return clean.length <= MAX_HEALTH_ERROR_LENGTH ? clean : `${clean.slice(0, MAX_HEALTH_ERROR_LENGTH - 1)}\u2026`;
|
|
73
|
+
}
|
|
67
74
|
function stripAnsi(text) {
|
|
68
75
|
return text.replace(/\u001b\[[0-9;]*m/g, "");
|
|
69
76
|
}
|
|
@@ -97,6 +104,7 @@ export {
|
|
|
97
104
|
bold,
|
|
98
105
|
colorForArea,
|
|
99
106
|
stripControlChars,
|
|
107
|
+
boundedHealthError,
|
|
100
108
|
stripAnsi,
|
|
101
109
|
requireDurableIngestAcknowledgement
|
|
102
110
|
};
|
|
@@ -56,6 +56,45 @@ import { homedir } from "os";
|
|
|
56
56
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
|
|
57
57
|
import { fileURLToPath } from "url";
|
|
58
58
|
import { parse as parseYaml } from "yaml";
|
|
59
|
+
|
|
60
|
+
// src/lib/semver.ts
|
|
61
|
+
function parseSemver(value) {
|
|
62
|
+
if (typeof value !== "string" || value.length > 256) return;
|
|
63
|
+
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u.exec(
|
|
64
|
+
value
|
|
65
|
+
);
|
|
66
|
+
if (!match) return;
|
|
67
|
+
const prerelease = match[4]?.split(".") ?? [];
|
|
68
|
+
if (prerelease.some((part) => /^\d+$/u.test(part) && part.length > 1 && part.startsWith("0"))) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
return { core: [BigInt(match[1]), BigInt(match[2]), BigInt(match[3])], prerelease };
|
|
72
|
+
}
|
|
73
|
+
function compareSemver(left, right) {
|
|
74
|
+
const a = parseSemver(left);
|
|
75
|
+
const b = parseSemver(right);
|
|
76
|
+
if (!a || !b) return;
|
|
77
|
+
for (let i = 0; i < a.core.length; i += 1) {
|
|
78
|
+
if (a.core[i] !== b.core[i]) return a.core[i] < b.core[i] ? -1 : 1;
|
|
79
|
+
}
|
|
80
|
+
if (a.prerelease.length === 0 || b.prerelease.length === 0) {
|
|
81
|
+
return a.prerelease.length === b.prerelease.length ? 0 : a.prerelease.length === 0 ? 1 : -1;
|
|
82
|
+
}
|
|
83
|
+
for (let i = 0; i < Math.max(a.prerelease.length, b.prerelease.length); i += 1) {
|
|
84
|
+
const x = a.prerelease[i];
|
|
85
|
+
const y = b.prerelease[i];
|
|
86
|
+
if (x === void 0 || y === void 0) return x === void 0 ? -1 : 1;
|
|
87
|
+
if (x === y) continue;
|
|
88
|
+
const xNumeric = /^\d+$/u.test(x);
|
|
89
|
+
const yNumeric = /^\d+$/u.test(y);
|
|
90
|
+
if (xNumeric && yNumeric) return BigInt(x) < BigInt(y) ? -1 : 1;
|
|
91
|
+
if (xNumeric !== yNumeric) return xNumeric ? -1 : 1;
|
|
92
|
+
return x < y ? -1 : 1;
|
|
93
|
+
}
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/commands/claude-plugin.ts
|
|
59
98
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
60
99
|
var PLUGIN_DESCRIPTION = "Primitive decision-graph guidance for the prim CLI \u2014 a model-invoked skill installed by prim skill install.";
|
|
61
100
|
var MAX_MANIFEST_BYTES = 64 * 1024;
|
|
@@ -100,42 +139,6 @@ function pluginPaths(dir) {
|
|
|
100
139
|
skillPath: join(dir, "SKILL.md")
|
|
101
140
|
};
|
|
102
141
|
}
|
|
103
|
-
function parseSemver(value) {
|
|
104
|
-
if (typeof value !== "string" || value.length > 256) return;
|
|
105
|
-
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u.exec(
|
|
106
|
-
value
|
|
107
|
-
);
|
|
108
|
-
if (!match) return;
|
|
109
|
-
const prerelease = match[4]?.split(".") ?? [];
|
|
110
|
-
if (prerelease.some((part) => /^\d+$/u.test(part) && part.length > 1 && part.startsWith("0"))) {
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
return { core: [BigInt(match[1]), BigInt(match[2]), BigInt(match[3])], prerelease };
|
|
114
|
-
}
|
|
115
|
-
function compareSemver(left, right) {
|
|
116
|
-
const a = parseSemver(left);
|
|
117
|
-
const b = parseSemver(right);
|
|
118
|
-
if (!a || !b) return;
|
|
119
|
-
for (let i = 0; i < a.core.length; i += 1) {
|
|
120
|
-
if (a.core[i] !== b.core[i]) return a.core[i] < b.core[i] ? -1 : 1;
|
|
121
|
-
}
|
|
122
|
-
if (a.prerelease.length === 0 || b.prerelease.length === 0) {
|
|
123
|
-
return a.prerelease.length === b.prerelease.length ? 0 : a.prerelease.length === 0 ? 1 : -1;
|
|
124
|
-
}
|
|
125
|
-
const length = Math.max(a.prerelease.length, b.prerelease.length);
|
|
126
|
-
for (let i = 0; i < length; i += 1) {
|
|
127
|
-
const x = a.prerelease[i];
|
|
128
|
-
const y = b.prerelease[i];
|
|
129
|
-
if (x === void 0 || y === void 0) return x === void 0 ? -1 : 1;
|
|
130
|
-
if (x === y) continue;
|
|
131
|
-
const xNumeric = /^\d+$/u.test(x);
|
|
132
|
-
const yNumeric = /^\d+$/u.test(y);
|
|
133
|
-
if (xNumeric && yNumeric) return BigInt(x) < BigInt(y) ? -1 : 1;
|
|
134
|
-
if (xNumeric !== yNumeric) return xNumeric ? -1 : 1;
|
|
135
|
-
return x < y ? -1 : 1;
|
|
136
|
-
}
|
|
137
|
-
return 0;
|
|
138
|
-
}
|
|
139
142
|
function pathIsInside(root, path) {
|
|
140
143
|
const rel = relative(root, path);
|
|
141
144
|
return rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
@@ -610,6 +613,7 @@ async function bindRepository(root, options) {
|
|
|
610
613
|
export {
|
|
611
614
|
bindRepository,
|
|
612
615
|
printJson,
|
|
616
|
+
compareSemver,
|
|
613
617
|
refreshClaudePlugins,
|
|
614
618
|
hasUsableCodexGuidance,
|
|
615
619
|
registerSkillCommands
|
package/dist/daemon/server.js
CHANGED
|
@@ -1517,6 +1517,7 @@ function handleStatusSnapshot(params) {
|
|
|
1517
1517
|
sessionId: activeSessionId,
|
|
1518
1518
|
lastHeartbeatAt,
|
|
1519
1519
|
version: runtimeVersion,
|
|
1520
|
+
launchRevision: process.env.PRIM_LAUNCH_REVISION,
|
|
1520
1521
|
healthy: daemonHealth.healthy,
|
|
1521
1522
|
needsReauth: daemonHealth.needsReauth === true,
|
|
1522
1523
|
heartbeat: { ...daemonHealth.heartbeat },
|
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,
|
|
@@ -24,9 +25,10 @@ import {
|
|
|
24
25
|
} from "./chunk-S4LFRVGD.js";
|
|
25
26
|
import {
|
|
26
27
|
bindRepository,
|
|
28
|
+
compareSemver,
|
|
27
29
|
printJson,
|
|
28
30
|
registerSkillCommands
|
|
29
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-N4R7EXZT.js";
|
|
30
32
|
import {
|
|
31
33
|
fetchFeedbackCapability
|
|
32
34
|
} from "./chunk-R4CUX6FY.js";
|
|
@@ -636,28 +638,58 @@ import { dirname as dirname2, join as join2 } from "path";
|
|
|
636
638
|
import { spawnSync } from "child_process";
|
|
637
639
|
import { createHash as createHash2 } from "crypto";
|
|
638
640
|
import {
|
|
641
|
+
constants,
|
|
642
|
+
accessSync,
|
|
639
643
|
chmodSync,
|
|
640
644
|
copyFileSync,
|
|
641
645
|
existsSync as existsSync2,
|
|
642
646
|
mkdirSync,
|
|
643
647
|
mkdtempSync,
|
|
644
648
|
readFileSync as readFileSync2,
|
|
649
|
+
realpathSync,
|
|
645
650
|
renameSync,
|
|
646
651
|
rmSync,
|
|
647
652
|
symlinkSync,
|
|
648
653
|
writeFileSync
|
|
649
654
|
} from "fs";
|
|
650
655
|
import { homedir } from "os";
|
|
651
|
-
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
|
|
652
664
|
var LAUNCHD_LABEL = "ai.getprimitive.prim-daemon";
|
|
653
|
-
var RUNTIME_SCHEMA_VERSION =
|
|
665
|
+
var RUNTIME_SCHEMA_VERSION = 2;
|
|
654
666
|
var RUNTIME_DIR_MODE = 448;
|
|
655
667
|
var RUNTIME_FILE_MODE = 384;
|
|
656
668
|
var RUNTIME_LAUNCHER_MODE = 448;
|
|
657
669
|
var PLIST_FILE_MODE = 384;
|
|
658
|
-
var
|
|
670
|
+
var LAUNCHER_SCHEMA_VERSION = 1;
|
|
671
|
+
var TRANSITION_TIMEOUT_MS = 15e3;
|
|
672
|
+
var COMMAND_TIMEOUT_MS = 5e3;
|
|
659
673
|
var READY_POLL_MS = 100;
|
|
660
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
|
+
};
|
|
661
693
|
function xdgDataHome(options) {
|
|
662
694
|
const home = options.homeDir ?? homedir();
|
|
663
695
|
const configured = (options.env ?? process.env).XDG_DATA_HOME;
|
|
@@ -672,14 +704,22 @@ function runtimePaths(options = {}) {
|
|
|
672
704
|
runtimeDir,
|
|
673
705
|
releasesDir: join(runtimeDir, "releases"),
|
|
674
706
|
currentLink,
|
|
675
|
-
daemonEntry: join(currentLink, "prim-daemon-server"),
|
|
676
|
-
statuslineEntry: join(currentLink, "prim-statusline"),
|
|
677
707
|
statuslineLauncher: join(runtimeDir, "prim-statusline"),
|
|
678
|
-
manifestPath: join(currentLink, "manifest.json"),
|
|
679
708
|
disabledMarker: join(dataDir, "daemon.disabled"),
|
|
680
709
|
lifecycleLockDir: join(dataDir, "daemon.lifecycle.lock")
|
|
681
710
|
};
|
|
682
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
|
+
}
|
|
683
723
|
function findPackageVersion(entryFile) {
|
|
684
724
|
let dir = dirname(resolve(entryFile));
|
|
685
725
|
for (let depth = 0; depth < 6; depth++) {
|
|
@@ -702,7 +742,7 @@ function findPackageVersion(entryFile) {
|
|
|
702
742
|
function readRuntimeManifest(path) {
|
|
703
743
|
try {
|
|
704
744
|
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
705
|
-
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)) {
|
|
706
746
|
return null;
|
|
707
747
|
}
|
|
708
748
|
return parsed;
|
|
@@ -710,10 +750,18 @@ function readRuntimeManifest(path) {
|
|
|
710
750
|
return null;
|
|
711
751
|
}
|
|
712
752
|
}
|
|
713
|
-
function sameRuntime(current, desired,
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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");
|
|
717
765
|
}
|
|
718
766
|
function atomicSymlink(target, linkPath) {
|
|
719
767
|
const tempLink = `${linkPath}.tmp-${process.pid}-${createHash2("sha256").update(`${target}\0${Date.now()}\0${Math.random()}`).digest("hex").slice(0, 12)}`;
|
|
@@ -740,35 +788,60 @@ function stageRuntime(options = {}) {
|
|
|
740
788
|
version,
|
|
741
789
|
nodePath,
|
|
742
790
|
daemonFile: "prim-daemon-server",
|
|
743
|
-
|
|
791
|
+
daemonSha256: sha256File(daemonSource),
|
|
792
|
+
...statuslineSource ? {
|
|
793
|
+
statuslineFile: "prim-statusline",
|
|
794
|
+
statuslineSha256: sha256File(statuslineSource)
|
|
795
|
+
} : {},
|
|
744
796
|
stagedAt: (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString()
|
|
745
797
|
};
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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) {
|
|
749
810
|
atomicWrite(
|
|
750
811
|
paths.statuslineLauncher,
|
|
751
|
-
statuslineLauncherContent(
|
|
812
|
+
statuslineLauncherContent(statuslinePath, manifest.nodePath),
|
|
752
813
|
RUNTIME_LAUNCHER_MODE
|
|
753
814
|
);
|
|
754
815
|
} else {
|
|
755
816
|
rmSync(paths.statuslineLauncher, { force: true });
|
|
756
817
|
}
|
|
757
|
-
return {
|
|
818
|
+
return {
|
|
819
|
+
changed: false,
|
|
820
|
+
manifest,
|
|
821
|
+
paths,
|
|
822
|
+
daemonPath: currentDaemon
|
|
823
|
+
};
|
|
758
824
|
}
|
|
759
825
|
mkdirSync(paths.releasesDir, { recursive: true, mode: RUNTIME_DIR_MODE });
|
|
760
826
|
chmodSync(paths.runtimeDir, RUNTIME_DIR_MODE);
|
|
761
827
|
chmodSync(paths.releasesDir, RUNTIME_DIR_MODE);
|
|
762
828
|
const stagingDir = mkdtempSync(join(paths.releasesDir, ".stage-"));
|
|
763
829
|
chmodSync(stagingDir, RUNTIME_DIR_MODE);
|
|
830
|
+
let releaseDir;
|
|
764
831
|
try {
|
|
765
832
|
const daemonTarget = join(stagingDir, desired.daemonFile);
|
|
766
833
|
copyFileSync(daemonSource, daemonTarget);
|
|
767
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
|
+
}
|
|
768
838
|
if (statuslineSource && desired.statuslineFile) {
|
|
769
839
|
const statuslineTarget = join(stagingDir, desired.statuslineFile);
|
|
770
840
|
copyFileSync(statuslineSource, statuslineTarget);
|
|
771
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
|
+
}
|
|
772
845
|
}
|
|
773
846
|
const manifestTarget = join(stagingDir, "manifest.json");
|
|
774
847
|
writeFileSync(manifestTarget, `${JSON.stringify(desired, null, 2)}
|
|
@@ -780,13 +853,15 @@ function stageRuntime(options = {}) {
|
|
|
780
853
|
const releaseName = `release-${createHash2("sha256").update(
|
|
781
854
|
`${version}\0${nodePath}\0${desired.statuslineFile ?? ""}\0${desired.stagedAt}\0${process.pid}\0${Math.random()}`
|
|
782
855
|
).digest("hex").slice(0, 16)}`;
|
|
783
|
-
|
|
856
|
+
releaseDir = join(paths.releasesDir, releaseName);
|
|
784
857
|
renameSync(stagingDir, releaseDir);
|
|
858
|
+
releaseDir = realpathSync(releaseDir);
|
|
785
859
|
atomicSymlink(join("releases", releaseName), paths.currentLink);
|
|
786
860
|
if (desired.statuslineFile) {
|
|
861
|
+
const statuslinePath = join(releaseDir, desired.statuslineFile);
|
|
787
862
|
atomicWrite(
|
|
788
863
|
paths.statuslineLauncher,
|
|
789
|
-
statuslineLauncherContent(
|
|
864
|
+
statuslineLauncherContent(statuslinePath, desired.nodePath),
|
|
790
865
|
RUNTIME_LAUNCHER_MODE
|
|
791
866
|
);
|
|
792
867
|
} else {
|
|
@@ -796,36 +871,91 @@ function stageRuntime(options = {}) {
|
|
|
796
871
|
rmSync(stagingDir, { recursive: true, force: true });
|
|
797
872
|
throw error;
|
|
798
873
|
}
|
|
799
|
-
return {
|
|
874
|
+
return {
|
|
875
|
+
changed: true,
|
|
876
|
+
manifest: desired,
|
|
877
|
+
paths,
|
|
878
|
+
daemonPath: join(releaseDir, desired.daemonFile)
|
|
879
|
+
};
|
|
800
880
|
}
|
|
801
881
|
function shellQuote(value) {
|
|
802
882
|
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
803
883
|
}
|
|
804
|
-
function statuslineLauncherContent(
|
|
884
|
+
function statuslineLauncherContent(statuslinePath, nodePath) {
|
|
805
885
|
return `#!/bin/sh
|
|
806
|
-
exec ${shellQuote(nodePath)} ${shellQuote(
|
|
886
|
+
exec ${shellQuote(nodePath)} ${shellQuote(statuslinePath)}
|
|
807
887
|
`;
|
|
808
888
|
}
|
|
809
889
|
function runtimeStatuslineCommand(options = {}) {
|
|
810
890
|
return shellQuote(runtimePaths(options).statuslineLauncher);
|
|
811
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
|
+
}
|
|
812
946
|
function xmlEscape(value) {
|
|
813
947
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
814
948
|
}
|
|
815
949
|
function generateLaunchAgentPlist(config) {
|
|
816
|
-
const apiUrl = config.apiUrl ? `
|
|
817
|
-
<key>PRIM_API_URL</key>
|
|
818
|
-
<string>${xmlEscape(config.apiUrl)}</string>` : "";
|
|
819
950
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
820
951
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
821
952
|
<plist version="1.0">
|
|
822
953
|
<dict>
|
|
823
954
|
<key>Label</key>
|
|
824
|
-
<string>${LAUNCHD_LABEL}</string>
|
|
955
|
+
<string>${xmlEscape(config.label ?? LAUNCHD_LABEL)}</string>
|
|
825
956
|
<key>ProgramArguments</key>
|
|
826
957
|
<array>
|
|
827
|
-
<string>${xmlEscape(config.
|
|
828
|
-
<string>${xmlEscape(config.daemonPath)}</string>
|
|
958
|
+
<string>${xmlEscape(config.launcherPath)}</string>
|
|
829
959
|
</array>
|
|
830
960
|
<key>RunAtLoad</key>
|
|
831
961
|
<true/>
|
|
@@ -835,6 +965,8 @@ function generateLaunchAgentPlist(config) {
|
|
|
835
965
|
<integer>10</integer>
|
|
836
966
|
<key>ProcessType</key>
|
|
837
967
|
<string>Background</string>
|
|
968
|
+
<key>ExitTimeOut</key>
|
|
969
|
+
<integer>5</integer>
|
|
838
970
|
<key>Umask</key>
|
|
839
971
|
<integer>63</integer>
|
|
840
972
|
<key>WorkingDirectory</key>
|
|
@@ -843,11 +975,6 @@ function generateLaunchAgentPlist(config) {
|
|
|
843
975
|
<string>${xmlEscape(config.logPath)}</string>
|
|
844
976
|
<key>StandardErrorPath</key>
|
|
845
977
|
<string>${xmlEscape(config.logPath)}</string>
|
|
846
|
-
<key>EnvironmentVariables</key>
|
|
847
|
-
<dict>
|
|
848
|
-
<key>PRIM_RUNTIME_VERSION</key>
|
|
849
|
-
<string>${xmlEscape(config.runtimeVersion)}</string>${apiUrl}
|
|
850
|
-
</dict>
|
|
851
978
|
</dict>
|
|
852
979
|
</plist>
|
|
853
980
|
`;
|
|
@@ -855,45 +982,57 @@ function generateLaunchAgentPlist(config) {
|
|
|
855
982
|
function launchdPaths(options = {}) {
|
|
856
983
|
const home = options.homeDir ?? homedir();
|
|
857
984
|
const uid = options.uid ?? process.getuid?.();
|
|
985
|
+
const label = options.label ?? LAUNCHD_LABEL;
|
|
858
986
|
if (uid === void 0) throw new Error("cannot determine uid for launchd user domain");
|
|
859
987
|
return {
|
|
860
988
|
domainTarget: `gui/${uid}`,
|
|
861
|
-
serviceTarget: `gui/${uid}/${
|
|
862
|
-
plistPath: join(home, "Library", "LaunchAgents", `${
|
|
989
|
+
serviceTarget: `gui/${uid}/${label}`,
|
|
990
|
+
plistPath: join(home, "Library", "LaunchAgents", `${label}.plist`),
|
|
863
991
|
logPath: join(home, ".config", "prim", "daemon.log")
|
|
864
992
|
};
|
|
865
993
|
}
|
|
866
|
-
var runLaunchctl = (args) => {
|
|
867
|
-
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;
|
|
868
997
|
return {
|
|
869
998
|
status: result.status,
|
|
870
999
|
stdout: result.stdout ?? "",
|
|
871
|
-
stderr:
|
|
1000
|
+
stderr: error?.message ?? result.stderr ?? "",
|
|
1001
|
+
timedOut: error?.code === "ETIMEDOUT"
|
|
872
1002
|
};
|
|
873
1003
|
};
|
|
874
1004
|
function parseLaunchdService(result) {
|
|
875
|
-
if (result.status
|
|
1005
|
+
if (result.status === SERVICE_NOT_FOUND) return { loaded: false };
|
|
1006
|
+
if (result.status !== 0) throw new LaunchctlError("print", result);
|
|
876
1007
|
const state = /^\s*state = (.+?)\s*$/m.exec(result.stdout)?.[1];
|
|
877
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");
|
|
878
1011
|
const pid = rawPid ? Number(rawPid) : void 0;
|
|
879
|
-
return { loaded: true, state, pid,
|
|
1012
|
+
return { loaded: true, state, pid, program: program2 };
|
|
880
1013
|
}
|
|
881
1014
|
function getLaunchdService(options = {}) {
|
|
882
|
-
|
|
883
|
-
|
|
1015
|
+
return parseLaunchdService(
|
|
1016
|
+
(options.runner ?? runLaunchctl)(
|
|
1017
|
+
["print", launchdPaths(options).serviceTarget],
|
|
1018
|
+
options.timeoutMs ?? COMMAND_TIMEOUT_MS
|
|
1019
|
+
)
|
|
1020
|
+
);
|
|
884
1021
|
}
|
|
885
|
-
function atomicWrite(path, content, mode) {
|
|
1022
|
+
function atomicWrite(path, content, mode, validate) {
|
|
886
1023
|
try {
|
|
887
1024
|
if (readFileSync2(path, "utf8") === content) {
|
|
888
1025
|
chmodSync(path, mode);
|
|
1026
|
+
validate?.(path, COMMAND_TIMEOUT_MS);
|
|
889
1027
|
return false;
|
|
890
1028
|
}
|
|
891
1029
|
} catch {
|
|
892
1030
|
}
|
|
893
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
1031
|
+
mkdirSync(dirname(path), { recursive: true, mode: RUNTIME_DIR_MODE });
|
|
894
1032
|
const temp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
895
1033
|
writeFileSync(temp, content, { encoding: "utf8", mode, flag: "wx" });
|
|
896
1034
|
try {
|
|
1035
|
+
validate?.(temp, COMMAND_TIMEOUT_MS);
|
|
897
1036
|
renameSync(temp, path);
|
|
898
1037
|
} catch (error) {
|
|
899
1038
|
rmSync(temp, { force: true });
|
|
@@ -901,20 +1040,25 @@ function atomicWrite(path, content, mode) {
|
|
|
901
1040
|
}
|
|
902
1041
|
return true;
|
|
903
1042
|
}
|
|
904
|
-
function launchctlOrThrow(runner, args, operation) {
|
|
905
|
-
const result = runner(args);
|
|
906
|
-
if (result.status !== 0) {
|
|
907
|
-
const detail = result.stderr.trim() || result.stdout.trim() || `exit ${String(result.status)}`;
|
|
908
|
-
throw new Error(`launchctl ${operation} failed: ${detail}`);
|
|
909
|
-
}
|
|
910
|
-
return result;
|
|
911
|
-
}
|
|
912
1043
|
function defaultSleep(ms) {
|
|
913
1044
|
return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
914
1045
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
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;
|
|
918
1062
|
}
|
|
919
1063
|
function processIsAlive(pid) {
|
|
920
1064
|
try {
|
|
@@ -924,11 +1068,31 @@ function processIsAlive(pid) {
|
|
|
924
1068
|
return error.code === "EPERM";
|
|
925
1069
|
}
|
|
926
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
|
+
}
|
|
927
1086
|
async function withDaemonLifecycleLock(operation, options = {}) {
|
|
928
|
-
const
|
|
929
|
-
|
|
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
|
+
);
|
|
930
1094
|
}
|
|
931
|
-
async function stopVerifiedLegacyDaemon(homeDir) {
|
|
1095
|
+
async function stopVerifiedLegacyDaemon(homeDir, deadlineMs, nowMs, sleep2) {
|
|
932
1096
|
const pidPath = join(homeDir, ".config", "prim", "daemon.pid");
|
|
933
1097
|
let pid;
|
|
934
1098
|
try {
|
|
@@ -936,122 +1100,314 @@ async function stopVerifiedLegacyDaemon(homeDir) {
|
|
|
936
1100
|
} catch {
|
|
937
1101
|
throw new Error("an unsupervised daemon answered, but it has no verifiable pidfile");
|
|
938
1102
|
}
|
|
939
|
-
|
|
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
|
+
);
|
|
940
1111
|
if (!Number.isInteger(pid) || pid <= 0 || !processIsAlive(pid) || snapshot?.pid !== pid) {
|
|
941
1112
|
throw new Error("refusing to signal an unsupervised daemon whose pid cannot be verified");
|
|
942
1113
|
}
|
|
1114
|
+
if (nowMs() >= deadlineMs) throw new Error("legacy verification exceeded the lifecycle deadline");
|
|
943
1115
|
process.kill(pid, "SIGTERM");
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
await defaultSleep(100);
|
|
1116
|
+
while (nowMs() < deadlineMs && processIsAlive(pid)) {
|
|
1117
|
+
await sleep2(Math.min(READY_POLL_MS, deadlineMs - nowMs()));
|
|
947
1118
|
}
|
|
948
1119
|
if (processIsAlive(pid)) {
|
|
949
|
-
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`);
|
|
950
1121
|
}
|
|
951
1122
|
return true;
|
|
952
1123
|
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
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;
|
|
958
1154
|
}
|
|
959
|
-
|
|
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
|
+
);
|
|
960
1273
|
}
|
|
961
1274
|
function daemonExplicitlyDisabled(options = {}) {
|
|
962
|
-
|
|
1275
|
+
const paths = daemonControlPaths(options);
|
|
1276
|
+
return existsSync2(paths.disabledMarker) || existsSync2(paths.legacyDisabledMarker);
|
|
963
1277
|
}
|
|
964
1278
|
function setDaemonExplicitlyDisabled(disabled, options = {}) {
|
|
965
|
-
const
|
|
1279
|
+
const paths = daemonControlPaths(options);
|
|
966
1280
|
if (!disabled) {
|
|
967
|
-
rmSync(
|
|
1281
|
+
rmSync(paths.disabledMarker, { force: true });
|
|
1282
|
+
rmSync(paths.legacyDisabledMarker, { force: true });
|
|
968
1283
|
return;
|
|
969
1284
|
}
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
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);
|
|
976
1290
|
}
|
|
977
1291
|
async function ensureMacDaemon(options = {}) {
|
|
978
1292
|
return withDaemonLifecycleLock(() => ensureMacDaemonLocked(options), options);
|
|
979
1293
|
}
|
|
980
1294
|
async function ensureMacDaemonLocked(options) {
|
|
1295
|
+
const runner = options.runner ?? runLaunchctl;
|
|
1296
|
+
let service = getLaunchdService({ ...options, runner });
|
|
981
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
|
+
}
|
|
982
1302
|
return {
|
|
983
1303
|
state: "disabled",
|
|
984
1304
|
action: "none",
|
|
985
1305
|
runtimeChanged: false,
|
|
986
1306
|
plistChanged: false,
|
|
987
1307
|
responding: false,
|
|
988
|
-
service
|
|
1308
|
+
service
|
|
989
1309
|
};
|
|
990
1310
|
}
|
|
991
1311
|
if (options.explicitlyStarted) setDaemonExplicitlyDisabled(false, options);
|
|
992
|
-
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
|
+
}
|
|
993
1347
|
const servicePaths = launchdPaths(options);
|
|
994
1348
|
mkdirSync(dirname(servicePaths.logPath), { recursive: true, mode: RUNTIME_DIR_MODE });
|
|
995
1349
|
writeFileSync(servicePaths.logPath, "", { mode: RUNTIME_FILE_MODE, flag: "a" });
|
|
996
1350
|
chmodSync(servicePaths.logPath, RUNTIME_FILE_MODE);
|
|
1351
|
+
const launcher = generateDaemonLauncher(daemonConfig);
|
|
1352
|
+
atomicWrite(control.launcher, launcher.content, RUNTIME_LAUNCHER_MODE);
|
|
997
1353
|
const plist = generateLaunchAgentPlist({
|
|
998
|
-
|
|
999
|
-
daemonPath: runtime.paths.daemonEntry,
|
|
1000
|
-
runtimeVersion: runtime.manifest.version,
|
|
1354
|
+
launcherPath: control.launcher,
|
|
1001
1355
|
logPath: servicePaths.logPath,
|
|
1002
1356
|
workingDirectory: options.homeDir ?? homedir(),
|
|
1003
|
-
|
|
1357
|
+
label: options.label
|
|
1004
1358
|
});
|
|
1005
|
-
const plistChanged = atomicWrite(
|
|
1006
|
-
|
|
1007
|
-
|
|
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));
|
|
1008
1381
|
let action = "none";
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
"bootstrap"
|
|
1015
|
-
);
|
|
1016
|
-
action = "reload";
|
|
1017
|
-
} else if (!service.loaded) {
|
|
1018
|
-
if (await (options.probe ?? daemonIsLive)(READY_PROBE_TIMEOUT_MS)) {
|
|
1019
|
-
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();
|
|
1020
1387
|
}
|
|
1021
|
-
|
|
1022
|
-
runner,
|
|
1023
|
-
["bootstrap", servicePaths.domainTarget, servicePaths.plistPath],
|
|
1024
|
-
"bootstrap"
|
|
1025
|
-
);
|
|
1388
|
+
await bootstrap(transition);
|
|
1026
1389
|
action = "bootstrap";
|
|
1027
|
-
} else {
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
const responding = await waitForReady(
|
|
1040
|
-
options.probe ?? daemonIsLive,
|
|
1041
|
-
options.sleep ?? defaultSleep
|
|
1042
|
-
);
|
|
1043
|
-
service = getLaunchdService({ ...options, runner });
|
|
1044
|
-
const socketPid = responding ? await (options.identify ?? identifyDaemon)(READY_PROBE_TIMEOUT_MS) : null;
|
|
1045
|
-
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);
|
|
1046
1402
|
return {
|
|
1047
1403
|
state: running ? "running" : "unhealthy",
|
|
1048
1404
|
action,
|
|
1049
|
-
runtimeChanged: runtime
|
|
1405
|
+
runtimeChanged: runtime?.changed ?? false,
|
|
1050
1406
|
plistChanged,
|
|
1051
1407
|
responding,
|
|
1052
|
-
...
|
|
1408
|
+
...ready.identity ? { socketPid: ready.identity.pid } : {},
|
|
1053
1409
|
service,
|
|
1054
|
-
runtime
|
|
1410
|
+
...runtime ? { runtime } : {}
|
|
1055
1411
|
};
|
|
1056
1412
|
}
|
|
1057
1413
|
async function bootoutMacDaemon(options = {}) {
|
|
@@ -1060,14 +1416,39 @@ async function bootoutMacDaemon(options = {}) {
|
|
|
1060
1416
|
async function bootoutMacDaemonLocked(options) {
|
|
1061
1417
|
setDaemonExplicitlyDisabled(true, options);
|
|
1062
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;
|
|
1063
1423
|
const paths = launchdPaths(options);
|
|
1424
|
+
const migrateLegacy = options.migrateLegacy ?? (() => stopVerifiedLegacyDaemon(options.homeDir ?? homedir(), deadlineMs, nowMs, sleep2));
|
|
1064
1425
|
const service = getLaunchdService({ ...options, runner });
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
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
|
+
});
|
|
1068
1449
|
}
|
|
1069
|
-
const legacyStopped = await (
|
|
1070
|
-
return { wasLoaded
|
|
1450
|
+
const legacyStopped = await inspect(timeoutMs(nowMs, deadlineMs, READY_PROBE_TIMEOUT_MS)) ? await migrateLegacy() : false;
|
|
1451
|
+
return { wasLoaded, legacyStopped, service };
|
|
1071
1452
|
}
|
|
1072
1453
|
|
|
1073
1454
|
// src/commands/claude-install.ts
|
|
@@ -1703,7 +2084,7 @@ var LOG_FILE_MODE = 384;
|
|
|
1703
2084
|
var STOP_TIMEOUT_MS = 5e3;
|
|
1704
2085
|
var STOP_POLL_MS = 100;
|
|
1705
2086
|
var STATUS_PROBE_TIMEOUT_MS = 500;
|
|
1706
|
-
var
|
|
2087
|
+
var READY_TIMEOUT_MS = 5e3;
|
|
1707
2088
|
var READY_POLL_MS2 = 100;
|
|
1708
2089
|
var READY_PROBE_TIMEOUT_MS2 = 250;
|
|
1709
2090
|
var HEALTHY_TIMEOUT_MS = 3e4;
|
|
@@ -1717,13 +2098,6 @@ function formatDaemonLifecycleMessage(message, decisionIngestion) {
|
|
|
1717
2098
|
function formatCurrentDaemonLifecycleMessage(message) {
|
|
1718
2099
|
return formatDaemonLifecycleMessage(message, decisionIngestionStatus(process.cwd()));
|
|
1719
2100
|
}
|
|
1720
|
-
var MAX_HEALTH_ERROR_LENGTH = 240;
|
|
1721
|
-
function boundedHealthError(value) {
|
|
1722
|
-
if (!value) return void 0;
|
|
1723
|
-
const clean = stripControlChars(value).replace(/\s+/g, " ").trim();
|
|
1724
|
-
if (!clean) return void 0;
|
|
1725
|
-
return clean.length <= MAX_HEALTH_ERROR_LENGTH ? clean : `${clean.slice(0, MAX_HEALTH_ERROR_LENGTH - 1)}\u2026`;
|
|
1726
|
-
}
|
|
1727
2101
|
function daemonDegradedReason(snapshot) {
|
|
1728
2102
|
if (!snapshot || snapshot.healthy !== false) return void 0;
|
|
1729
2103
|
if (snapshot.needsReauth) {
|
|
@@ -1783,8 +2157,8 @@ function openDaemonLog(configDir = CONFIG_DIR) {
|
|
|
1783
2157
|
}
|
|
1784
2158
|
return openSync2(join4(configDir, "daemon.log"), "a", LOG_FILE_MODE);
|
|
1785
2159
|
}
|
|
1786
|
-
async function
|
|
1787
|
-
const deadline = Date.now() +
|
|
2160
|
+
async function waitForReady() {
|
|
2161
|
+
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
1788
2162
|
while (Date.now() < deadline) {
|
|
1789
2163
|
if (await daemonIsLive(READY_PROBE_TIMEOUT_MS2)) {
|
|
1790
2164
|
return true;
|
|
@@ -1888,7 +2262,7 @@ async function detachedDaemonStart(opts) {
|
|
|
1888
2262
|
if (logFd !== void 0) {
|
|
1889
2263
|
closeSync2(logFd);
|
|
1890
2264
|
}
|
|
1891
|
-
const live = await
|
|
2265
|
+
const live = await waitForReady();
|
|
1892
2266
|
if (live) {
|
|
1893
2267
|
const after = readPidfile();
|
|
1894
2268
|
process.stderr.write(
|
|
@@ -1899,7 +2273,7 @@ async function detachedDaemonStart(opts) {
|
|
|
1899
2273
|
return;
|
|
1900
2274
|
}
|
|
1901
2275
|
process.stderr.write(
|
|
1902
|
-
`[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})
|
|
1903
2277
|
`
|
|
1904
2278
|
);
|
|
1905
2279
|
console.log(JSON.stringify({ started: false }, null, 2));
|
|
@@ -1981,8 +2355,9 @@ async function macDaemonStart(forceRestart = false) {
|
|
|
1981
2355
|
const healthy = daemonStartIsHealthy(serviceReady, snapshot, expectedVersion);
|
|
1982
2356
|
if (healthy) {
|
|
1983
2357
|
const verb = result.action === "none" ? "already running" : "started";
|
|
2358
|
+
const servicePid = result.service.loaded ? result.service.pid : void 0;
|
|
1984
2359
|
process.stderr.write(
|
|
1985
|
-
`${formatCurrentDaemonLifecycleMessage(`[prim] \u2713 daemon ${verb} under launchd (pid=${snapshot?.pid ??
|
|
2360
|
+
`${formatCurrentDaemonLifecycleMessage(`[prim] \u2713 daemon ${verb} under launchd (pid=${snapshot?.pid ?? servicePid ?? "?"})`)}
|
|
1986
2361
|
`
|
|
1987
2362
|
);
|
|
1988
2363
|
} else {
|
|
@@ -1999,7 +2374,7 @@ async function macDaemonStart(forceRestart = false) {
|
|
|
1999
2374
|
started: healthy,
|
|
2000
2375
|
supervised: true,
|
|
2001
2376
|
action: result.action,
|
|
2002
|
-
pid: snapshot?.pid ?? result.service.pid,
|
|
2377
|
+
pid: snapshot?.pid ?? (result.service.loaded ? result.service.pid : void 0),
|
|
2003
2378
|
loaded: result.service.loaded,
|
|
2004
2379
|
responding: result.responding,
|
|
2005
2380
|
healthy,
|
|
@@ -2071,6 +2446,19 @@ function classifyStatus(pidAlive, responding, snapshot, pid) {
|
|
|
2071
2446
|
return { json: { running: true, responding: true, ...snapshot }, exitCode: EXIT_OK2 };
|
|
2072
2447
|
}
|
|
2073
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
|
+
}
|
|
2074
2462
|
if (!service.loaded) {
|
|
2075
2463
|
if (responding) {
|
|
2076
2464
|
return {
|
|
@@ -2198,7 +2586,13 @@ async function detachedDaemonStatus() {
|
|
|
2198
2586
|
}
|
|
2199
2587
|
}
|
|
2200
2588
|
async function macDaemonStatus() {
|
|
2201
|
-
|
|
2589
|
+
let service;
|
|
2590
|
+
let launchdError;
|
|
2591
|
+
try {
|
|
2592
|
+
service = getLaunchdService();
|
|
2593
|
+
} catch (error) {
|
|
2594
|
+
launchdError = error;
|
|
2595
|
+
}
|
|
2202
2596
|
const responding = await daemonIsLive(STATUS_PROBE_TIMEOUT_MS);
|
|
2203
2597
|
const snapshot = responding ? await daemonRequest(
|
|
2204
2598
|
"status_snapshot",
|
|
@@ -2207,7 +2601,13 @@ async function macDaemonStatus() {
|
|
|
2207
2601
|
) : null;
|
|
2208
2602
|
const disabled = daemonExplicitlyDisabled();
|
|
2209
2603
|
const { json, exitCode } = classifyLaunchdStatus(service, responding, snapshot, disabled);
|
|
2210
|
-
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) {
|
|
2211
2611
|
process.stderr.write(
|
|
2212
2612
|
`[prim] \u25CC daemon pid=${snapshot?.pid ?? "?"} is live but not supervised by launchd
|
|
2213
2613
|
`
|
|
@@ -3347,8 +3747,13 @@ async function checkDaemon() {
|
|
|
3347
3747
|
if (process.platform === "darwin") {
|
|
3348
3748
|
try {
|
|
3349
3749
|
service = getLaunchdService();
|
|
3350
|
-
} catch {
|
|
3351
|
-
|
|
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
|
+
};
|
|
3352
3757
|
}
|
|
3353
3758
|
}
|
|
3354
3759
|
const snapshot = await daemonRequest(
|
package/package.json
CHANGED