@elpapi42/pi-fleet 0.1.0-beta.1 → 0.1.0-beta.13
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/CHANGELOG.md +180 -3
- package/README.md +118 -120
- package/bin/pifleet-runtime.mjs +1 -1
- package/dist/cli-meta.json +5555 -175
- package/dist/cli.mjs +4391 -471
- package/dist/cli.mjs.map +4 -4
- package/dist/client/agent-target.d.ts +33 -0
- package/dist/client/contracts.d.ts +81 -0
- package/dist/client/fleet-client.d.ts +118 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/sdk-connector.d.ts +7 -0
- package/dist/client/sdk-facade.d.ts +84 -0
- package/dist/client/sdk-transport.d.ts +24 -0
- package/dist/client/shared-client.d.ts +18 -0
- package/dist/client/socket-fleet-client.d.ts +21 -0
- package/dist/client-meta.json +5736 -0
- package/dist/client.mjs +4434 -0
- package/dist/client.mjs.map +7 -0
- package/dist/installer-meta.json +87 -13
- package/dist/installer.mjs +589 -95
- package/dist/installer.mjs.map +4 -4
- package/dist/journal-sqlite-worker-meta.json +149 -0
- package/dist/journal-sqlite-worker.mjs +1110 -0
- package/dist/journal-sqlite-worker.mjs.map +7 -0
- package/dist/pi/external-installation.d.ts +23 -0
- package/dist/platform/client/start-runtime.d.ts +25 -0
- package/dist/platform/install/runtime-release.d.ts +8 -0
- package/dist/platform/install/tree-integrity.d.ts +7 -0
- package/dist/platform/shared/paths.d.ts +8 -0
- package/dist/platform/shared/runtime-ownership.d.ts +14 -0
- package/dist/protocol/jsonl.d.ts +3 -0
- package/dist/protocol/pi-identity.d.ts +16 -0
- package/dist/protocol/semantic-segmentation.d.ts +22 -0
- package/dist/protocol/version.d.ts +2 -0
- package/dist/runtime/semantic-events.d.ts +43 -0
- package/dist/runtime-manifest.json +167 -48
- package/dist/runtime-meta.json +3241 -2808
- package/dist/runtime.mjs +9290 -5641
- package/dist/runtime.mjs.map +4 -4
- package/dist/shared/result.d.ts +9 -0
- package/package.json +24 -5
- package/dist/sqlite-worker-meta.json +0 -84
- package/dist/sqlite-worker.mjs +0 -304
- package/dist/sqlite-worker.mjs.map +0 -7
package/dist/installer.mjs
CHANGED
|
@@ -1,28 +1,252 @@
|
|
|
1
1
|
// src/entry/installer.ts
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
|
-
import { dirname as
|
|
3
|
+
import { dirname as dirname3, join as join6 } from "node:path";
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
|
|
7
|
-
// src/
|
|
7
|
+
// src/pi/external-installation.ts
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
8
10
|
import { constants } from "node:fs";
|
|
9
|
-
import {
|
|
11
|
+
import { createReadStream } from "node:fs";
|
|
12
|
+
import { access, realpath, stat } from "node:fs/promises";
|
|
13
|
+
import { delimiter, dirname, isAbsolute, join } from "node:path";
|
|
14
|
+
var VERSION_TIMEOUT_MS = 3e3;
|
|
15
|
+
var MAX_VERSION_OUTPUT_BYTES = 4 * 1024;
|
|
16
|
+
var ExternalPiResolutionError = class extends Error {
|
|
17
|
+
constructor(code, message) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.name = "ExternalPiResolutionError";
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
async function resolveExternalPiInstallation(options = {}) {
|
|
24
|
+
const env = options.env ?? process.env;
|
|
25
|
+
const selectedPath = await resolveSelectedPath(env);
|
|
26
|
+
const nodePath = options.nodePath ?? await resolveNodePath(env);
|
|
27
|
+
await inspectNode(nodePath);
|
|
28
|
+
const executionEnv = externalPiExecutionEnvironment(env, selectedPath, nodePath);
|
|
29
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
30
|
+
const observation = await observeInstallation(selectedPath, executionEnv, options);
|
|
31
|
+
if (observation !== null) {
|
|
32
|
+
return {
|
|
33
|
+
selectedPath,
|
|
34
|
+
nodePath,
|
|
35
|
+
...observation
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
throw new ExternalPiResolutionError(
|
|
40
|
+
"pi_installation_changed",
|
|
41
|
+
"Pi changed while its installation was being observed."
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
function externalPiExecutionEnvironment(env, selectedPath, nodePath) {
|
|
45
|
+
return {
|
|
46
|
+
...env,
|
|
47
|
+
PATH: [dirname(nodePath), dirname(selectedPath), env.PATH].filter((value) => value !== void 0 && value.length > 0).join(delimiter)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async function observeInstallation(selectedPath, env, options) {
|
|
51
|
+
const beforeRealPath = await inspectExecutable(selectedPath, "Pi");
|
|
52
|
+
const beforeHash = await hashFile(beforeRealPath);
|
|
53
|
+
const versionOutput = await (options.versionCommand ?? ((executable) => readVersion(executable, env, options.versionTimeoutMs, options.maxVersionOutputBytes)))(selectedPath);
|
|
54
|
+
const version = parseVersion(versionOutput);
|
|
55
|
+
const afterRealPath = await inspectExecutable(selectedPath, "Pi");
|
|
56
|
+
const afterHash = await hashFile(afterRealPath);
|
|
57
|
+
if (beforeRealPath !== afterRealPath || beforeHash !== afterHash) return null;
|
|
58
|
+
return {
|
|
59
|
+
realPath: afterRealPath,
|
|
60
|
+
version,
|
|
61
|
+
fingerprint: createHash("sha256").update(JSON.stringify([selectedPath, afterRealPath, version, afterHash])).digest("hex")
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async function resolveSelectedPath(env) {
|
|
65
|
+
const explicit = env.PIFLEET_PI_EXECUTABLE;
|
|
66
|
+
if (explicit !== void 0) {
|
|
67
|
+
if (!isAbsolute(explicit)) {
|
|
68
|
+
throw new ExternalPiResolutionError(
|
|
69
|
+
"invalid_arguments",
|
|
70
|
+
"PIFLEET_PI_EXECUTABLE must be an absolute path."
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return explicit;
|
|
74
|
+
}
|
|
75
|
+
return resolvePathExecutable(env, "pi", "Pi", "pi_not_found");
|
|
76
|
+
}
|
|
77
|
+
async function resolveNodePath(env) {
|
|
78
|
+
return resolvePathExecutable(env, "node", "Node", "pi_not_executable");
|
|
79
|
+
}
|
|
80
|
+
async function resolvePathExecutable(env, name, label, missingCode) {
|
|
81
|
+
const path = env.PATH;
|
|
82
|
+
if (path === void 0 || path.length === 0) {
|
|
83
|
+
throw new ExternalPiResolutionError(missingCode, `${label} was not found on PATH.`);
|
|
84
|
+
}
|
|
85
|
+
for (const directory of path.split(delimiter)) {
|
|
86
|
+
if (!isAbsolute(directory)) {
|
|
87
|
+
throw new ExternalPiResolutionError(
|
|
88
|
+
"invalid_arguments",
|
|
89
|
+
`PATH entries used to select ${label} must be absolute paths.`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
const candidate = join(directory, name);
|
|
93
|
+
try {
|
|
94
|
+
await inspectExecutable(candidate, label);
|
|
95
|
+
return candidate;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (!(error instanceof ExternalPiResolutionError) || error.code !== "pi_not_executable" && error.code !== "pi_not_found") {
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
throw new ExternalPiResolutionError(missingCode, `${label} was not found on PATH.`);
|
|
103
|
+
}
|
|
104
|
+
async function inspectExecutable(path, label) {
|
|
105
|
+
try {
|
|
106
|
+
const target = await realpath(path);
|
|
107
|
+
const metadata = await stat(target);
|
|
108
|
+
if (!metadata.isFile()) {
|
|
109
|
+
throw new ExternalPiResolutionError("pi_not_executable", `${label} is not a file: ${path}`);
|
|
110
|
+
}
|
|
111
|
+
await access(path, constants.X_OK);
|
|
112
|
+
return target;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (error instanceof ExternalPiResolutionError) throw error;
|
|
115
|
+
if (error.code === "ENOENT") {
|
|
116
|
+
throw new ExternalPiResolutionError("pi_not_found", `${label} was not found: ${path}`);
|
|
117
|
+
}
|
|
118
|
+
throw new ExternalPiResolutionError("pi_not_executable", `${label} is not executable: ${path}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function inspectNode(nodePath) {
|
|
122
|
+
if (!isAbsolute(nodePath)) {
|
|
123
|
+
throw new ExternalPiResolutionError("invalid_arguments", "Node must be an absolute path.");
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
await inspectExecutable(nodePath, "Node");
|
|
127
|
+
} catch {
|
|
128
|
+
throw new ExternalPiResolutionError("pi_not_executable", `Node is not executable: ${nodePath}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function hashFile(path) {
|
|
132
|
+
const hash = createHash("sha256");
|
|
133
|
+
await new Promise((resolveHash, rejectHash) => {
|
|
134
|
+
const stream = createReadStream(path);
|
|
135
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
136
|
+
stream.once("error", rejectHash);
|
|
137
|
+
stream.once("end", resolveHash);
|
|
138
|
+
});
|
|
139
|
+
return hash.digest("hex");
|
|
140
|
+
}
|
|
141
|
+
async function readVersion(executable, env, timeoutMs = VERSION_TIMEOUT_MS, maxOutputBytes = MAX_VERSION_OUTPUT_BYTES) {
|
|
142
|
+
const child = spawn(executable, ["--version"], {
|
|
143
|
+
detached: process.platform !== "win32",
|
|
144
|
+
env,
|
|
145
|
+
shell: false,
|
|
146
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
147
|
+
});
|
|
148
|
+
const chunks = [];
|
|
149
|
+
let outputBytes = 0;
|
|
150
|
+
let terminalError = null;
|
|
151
|
+
let terminated = false;
|
|
152
|
+
const terminate = () => {
|
|
153
|
+
if (terminated) return;
|
|
154
|
+
terminated = true;
|
|
155
|
+
if (process.platform !== "win32" && child.pid !== void 0) {
|
|
156
|
+
try {
|
|
157
|
+
process.kill(-child.pid, "SIGKILL");
|
|
158
|
+
return;
|
|
159
|
+
} catch {
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
child.kill("SIGKILL");
|
|
163
|
+
};
|
|
164
|
+
child.stdout.on("data", (chunk) => {
|
|
165
|
+
if (terminalError !== null) return;
|
|
166
|
+
outputBytes += chunk.byteLength;
|
|
167
|
+
if (outputBytes > maxOutputBytes) {
|
|
168
|
+
terminalError = new ExternalPiResolutionError(
|
|
169
|
+
"pi_version_unavailable",
|
|
170
|
+
"Pi version output exceeded its limit."
|
|
171
|
+
);
|
|
172
|
+
terminate();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
chunks.push(chunk);
|
|
176
|
+
});
|
|
177
|
+
child.stderr.resume();
|
|
178
|
+
return new Promise((resolveVersion, rejectVersion) => {
|
|
179
|
+
const timer = setTimeout(() => {
|
|
180
|
+
terminalError = new ExternalPiResolutionError(
|
|
181
|
+
"pi_version_unavailable",
|
|
182
|
+
"Pi version command timed out."
|
|
183
|
+
);
|
|
184
|
+
terminate();
|
|
185
|
+
}, timeoutMs);
|
|
186
|
+
child.once("error", () => {
|
|
187
|
+
terminalError ??= new ExternalPiResolutionError(
|
|
188
|
+
"pi_version_unavailable",
|
|
189
|
+
"Pi version command failed."
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
child.once("close", (code) => {
|
|
193
|
+
clearTimeout(timer);
|
|
194
|
+
if (terminalError !== null) {
|
|
195
|
+
rejectVersion(terminalError);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (code !== 0) {
|
|
199
|
+
rejectVersion(
|
|
200
|
+
new ExternalPiResolutionError("pi_version_unavailable", "Pi version command failed.")
|
|
201
|
+
);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
resolveVersion(new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks)));
|
|
206
|
+
} catch {
|
|
207
|
+
rejectVersion(
|
|
208
|
+
new ExternalPiResolutionError(
|
|
209
|
+
"pi_version_unavailable",
|
|
210
|
+
"Pi version command returned invalid UTF-8."
|
|
211
|
+
)
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
function parseVersion(output) {
|
|
218
|
+
const match = /^(?:pi\s+)?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\s*$/i.exec(output);
|
|
219
|
+
if (match?.[1] === void 0) {
|
|
220
|
+
throw new ExternalPiResolutionError(
|
|
221
|
+
"pi_version_unavailable",
|
|
222
|
+
"Pi version command returned an invalid version."
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
return match[1];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/platform/install/service-installer.ts
|
|
229
|
+
import { constants as constants2 } from "node:fs";
|
|
230
|
+
import { access as access2, chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
10
231
|
import { homedir } from "node:os";
|
|
11
|
-
import { dirname, join } from "node:path";
|
|
232
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
12
233
|
|
|
13
234
|
// src/platform/install/service-definition.ts
|
|
14
|
-
import { isAbsolute } from "node:path";
|
|
235
|
+
import { isAbsolute as isAbsolute2 } from "node:path";
|
|
15
236
|
function systemdUserUnit(options) {
|
|
16
237
|
validate(options);
|
|
17
|
-
const environment = options.
|
|
238
|
+
const environment = environmentEntries(options).map(([key, value]) => {
|
|
239
|
+
const assignment = `${key}=${value}`;
|
|
240
|
+
return `Environment=${/^[A-Za-z0-9_./:@=-]+$/.test(assignment) ? assignment : systemdQuote(assignment)}
|
|
18
241
|
`;
|
|
242
|
+
}).join("");
|
|
19
243
|
return `[Unit]
|
|
20
|
-
Description=
|
|
244
|
+
Description=pi-fleet user runtime
|
|
21
245
|
After=default.target
|
|
22
246
|
|
|
23
247
|
[Service]
|
|
24
248
|
Type=simple
|
|
25
|
-
ExecStart=${
|
|
249
|
+
ExecStart=${systemdArgument(options.nodePath)} ${systemdArgument(options.runtimePath)}
|
|
26
250
|
${environment}Restart=on-failure
|
|
27
251
|
RestartSec=1
|
|
28
252
|
TimeoutStopSec=10
|
|
@@ -35,8 +259,9 @@ WantedBy=default.target
|
|
|
35
259
|
}
|
|
36
260
|
function launchdAgentPlist(options) {
|
|
37
261
|
validate(options);
|
|
38
|
-
const
|
|
39
|
-
<
|
|
262
|
+
const entries = environmentEntries(options);
|
|
263
|
+
const environment = entries.length === 0 ? "" : ` <key>EnvironmentVariables</key>
|
|
264
|
+
<dict>${entries.map(([key, value]) => `<key>${xmlEscape(key)}</key><string>${xmlEscape(value)}</string>`).join("")}</dict>
|
|
40
265
|
`;
|
|
41
266
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
42
267
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -55,27 +280,62 @@ ${environment} <key>RunAtLoad</key><true/>
|
|
|
55
280
|
</plist>
|
|
56
281
|
`;
|
|
57
282
|
}
|
|
283
|
+
function environmentEntries(options) {
|
|
284
|
+
return [
|
|
285
|
+
...options.stateRoot === void 0 ? [] : [["PIFLEET_STATE_ROOT", options.stateRoot]],
|
|
286
|
+
...options.piExecutablePath === void 0 ? [] : [["PIFLEET_PI_EXECUTABLE", options.piExecutablePath]],
|
|
287
|
+
...options.piNodePath === void 0 ? [] : [["PIFLEET_PI_NODE", options.piNodePath]]
|
|
288
|
+
];
|
|
289
|
+
}
|
|
58
290
|
function validate(options) {
|
|
59
|
-
|
|
291
|
+
const paths = [
|
|
292
|
+
options.nodePath,
|
|
293
|
+
options.runtimePath,
|
|
294
|
+
options.piExecutablePath,
|
|
295
|
+
options.piNodePath
|
|
296
|
+
].filter((value) => value !== void 0);
|
|
297
|
+
if (paths.some((path) => !isAbsolute2(path))) {
|
|
60
298
|
throw new Error("Service executables must use absolute paths");
|
|
61
299
|
}
|
|
300
|
+
if (paths.some((path) => /[\0\n\r]/.test(path))) {
|
|
301
|
+
throw new Error("Service paths cannot contain control characters");
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function systemdArgument(value) {
|
|
305
|
+
return /^[A-Za-z0-9_./:@-]+$/.test(value) ? value : systemdQuote(value);
|
|
62
306
|
}
|
|
63
|
-
function
|
|
64
|
-
if (
|
|
65
|
-
return value
|
|
307
|
+
function systemdQuote(value) {
|
|
308
|
+
if (/[\0\n\r]/.test(value)) throw new Error("Unsafe systemd value");
|
|
309
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
66
310
|
}
|
|
67
311
|
function xmlEscape(value) {
|
|
68
312
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
69
313
|
}
|
|
70
314
|
|
|
71
315
|
// src/platform/install/service-installer.ts
|
|
316
|
+
var ServiceRepairDeferredError = class extends Error {
|
|
317
|
+
code = "runtime_upgrade_deferred";
|
|
318
|
+
constructor() {
|
|
319
|
+
super(
|
|
320
|
+
"The installed pi-fleet service differs from the requested runtime or Pi selection. Automatic replacement is deferred because runtime quiescence cannot be proven; finish active work, stop the existing service explicitly, then install or repair again."
|
|
321
|
+
);
|
|
322
|
+
this.name = "ServiceRepairDeferredError";
|
|
323
|
+
}
|
|
324
|
+
};
|
|
72
325
|
async function installUserService(options) {
|
|
73
326
|
const home = options.home ?? homedir();
|
|
74
327
|
if (options.platform === "linux") {
|
|
75
|
-
const path =
|
|
328
|
+
const path = join2(home, ".config", "systemd", "user", "pi-fleet.service");
|
|
76
329
|
const effectiveDefinition = await preserveInstalledStateRoot(path, options.definition, "linux");
|
|
77
330
|
const definition = systemdUserUnit(effectiveDefinition);
|
|
78
331
|
const needsRepair = await serviceDefinitionNeedsRepair(path, definition, effectiveDefinition);
|
|
332
|
+
const installed = await exists(path);
|
|
333
|
+
await preflightInstalledReplacement({
|
|
334
|
+
installed,
|
|
335
|
+
needsRepair,
|
|
336
|
+
replaceExisting: options.replaceExisting,
|
|
337
|
+
ownershipPreflight: options.ownershipPreflight
|
|
338
|
+
});
|
|
79
339
|
if (needsRepair) {
|
|
80
340
|
await atomicWrite(path, definition);
|
|
81
341
|
await options.executor.run("systemctl", ["--user", "daemon-reload"]);
|
|
@@ -87,38 +347,47 @@ async function installUserService(options) {
|
|
|
87
347
|
return path;
|
|
88
348
|
}
|
|
89
349
|
if (options.platform === "darwin") {
|
|
90
|
-
const path =
|
|
350
|
+
const path = join2(home, "Library", "LaunchAgents", "works.elpapi.pifleet.plist");
|
|
91
351
|
const effectiveDefinition = await preserveInstalledStateRoot(
|
|
92
352
|
path,
|
|
93
353
|
options.definition,
|
|
94
354
|
"darwin"
|
|
95
355
|
);
|
|
96
|
-
|
|
356
|
+
const definition = launchdAgentPlist(effectiveDefinition);
|
|
357
|
+
const needsRepair = await serviceDefinitionNeedsRepair(path, definition, effectiveDefinition);
|
|
358
|
+
const installed = await exists(path);
|
|
359
|
+
await preflightInstalledReplacement({
|
|
360
|
+
installed,
|
|
361
|
+
needsRepair,
|
|
362
|
+
replaceExisting: options.replaceExisting,
|
|
363
|
+
ownershipPreflight: options.ownershipPreflight
|
|
364
|
+
});
|
|
365
|
+
if (needsRepair) await atomicWrite(path, definition);
|
|
97
366
|
const domain = `gui/${options.uid ?? process.getuid?.() ?? 0}`;
|
|
98
367
|
await options.executor.run("launchctl", ["bootout", domain, path]).catch(() => void 0);
|
|
99
368
|
await options.executor.run("launchctl", ["bootstrap", domain, path]);
|
|
100
369
|
await options.executor.run("launchctl", ["kickstart", `${domain}/works.elpapi.pifleet`]);
|
|
101
370
|
return path;
|
|
102
371
|
}
|
|
103
|
-
throw new Error(`Native
|
|
372
|
+
throw new Error(`Native pi-fleet supervision is unsupported on ${options.platform}`);
|
|
104
373
|
}
|
|
105
374
|
async function uninstallUserService(options) {
|
|
106
375
|
const home = options.home ?? homedir();
|
|
107
376
|
if (options.platform === "linux") {
|
|
108
|
-
const path =
|
|
377
|
+
const path = join2(home, ".config", "systemd", "user", "pi-fleet.service");
|
|
109
378
|
await options.executor.run("systemctl", ["--user", "disable", "--now", "pi-fleet.service"]);
|
|
110
379
|
await rm(path, { force: true });
|
|
111
380
|
await options.executor.run("systemctl", ["--user", "daemon-reload"]);
|
|
112
381
|
return;
|
|
113
382
|
}
|
|
114
383
|
if (options.platform === "darwin") {
|
|
115
|
-
const path =
|
|
384
|
+
const path = join2(home, "Library", "LaunchAgents", "works.elpapi.pifleet.plist");
|
|
116
385
|
const domain = `gui/${options.uid ?? process.getuid?.() ?? 0}`;
|
|
117
386
|
await options.executor.run("launchctl", ["bootout", domain, path]).catch(() => void 0);
|
|
118
387
|
await rm(path, { force: true });
|
|
119
388
|
return;
|
|
120
389
|
}
|
|
121
|
-
throw new Error(`Native
|
|
390
|
+
throw new Error(`Native pi-fleet supervision is unsupported on ${options.platform}`);
|
|
122
391
|
}
|
|
123
392
|
async function serviceDefinitionNeedsRepair(path, expectedContents, definition) {
|
|
124
393
|
const current = await readFile(path, "utf8").catch((error) => {
|
|
@@ -127,13 +396,20 @@ async function serviceDefinitionNeedsRepair(path, expectedContents, definition)
|
|
|
127
396
|
});
|
|
128
397
|
if (current !== expectedContents) return true;
|
|
129
398
|
try {
|
|
130
|
-
await
|
|
131
|
-
await
|
|
399
|
+
await access2(definition.nodePath, constants2.X_OK);
|
|
400
|
+
await access2(definition.runtimePath, constants2.R_OK);
|
|
132
401
|
return false;
|
|
133
402
|
} catch {
|
|
134
403
|
return true;
|
|
135
404
|
}
|
|
136
405
|
}
|
|
406
|
+
async function preflightInstalledReplacement(options) {
|
|
407
|
+
if (!options.installed || !options.needsRepair) return;
|
|
408
|
+
if (options.replaceExisting !== true || options.ownershipPreflight === void 0) {
|
|
409
|
+
throw new ServiceRepairDeferredError();
|
|
410
|
+
}
|
|
411
|
+
await options.ownershipPreflight();
|
|
412
|
+
}
|
|
137
413
|
async function preserveInstalledStateRoot(path, definition, platform) {
|
|
138
414
|
if (definition.stateRoot !== void 0) return definition;
|
|
139
415
|
const current = await readFile(path, "utf8").catch((error) => {
|
|
@@ -146,8 +422,16 @@ async function preserveInstalledStateRoot(path, definition, platform) {
|
|
|
146
422
|
const stateRoot = platform === "linux" ? encoded : encoded.replaceAll("'", "'").replaceAll(""", '"').replaceAll(">", ">").replaceAll("<", "<").replaceAll("&", "&");
|
|
147
423
|
return { ...definition, stateRoot };
|
|
148
424
|
}
|
|
425
|
+
async function exists(path) {
|
|
426
|
+
try {
|
|
427
|
+
await access2(path);
|
|
428
|
+
return true;
|
|
429
|
+
} catch {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
149
433
|
async function atomicWrite(path, contents) {
|
|
150
|
-
await mkdir(
|
|
434
|
+
await mkdir(dirname2(path), { recursive: true, mode: 448 });
|
|
151
435
|
const temporary = `${path}.tmp-${process.pid}`;
|
|
152
436
|
await writeFile(temporary, contents, { mode: 384 });
|
|
153
437
|
await chmod(temporary, 384);
|
|
@@ -155,18 +439,18 @@ async function atomicWrite(path, contents) {
|
|
|
155
439
|
}
|
|
156
440
|
|
|
157
441
|
// src/platform/install/runtime-release.ts
|
|
158
|
-
import { createHash as
|
|
159
|
-
import { chmod as chmod2, cp, lstat as lstat2, mkdir as mkdir2, readFile as readFile3, rename as rename2, rm as rm2,
|
|
160
|
-
import { join as
|
|
442
|
+
import { createHash as createHash3, randomUUID } from "node:crypto";
|
|
443
|
+
import { chmod as chmod2, cp, lstat as lstat2, mkdir as mkdir2, readFile as readFile3, rename as rename2, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
|
|
444
|
+
import { join as join4, posix, resolve as resolve2, sep as sep2 } from "node:path";
|
|
161
445
|
|
|
162
446
|
// src/platform/install/tree-integrity.ts
|
|
163
|
-
import { createHash } from "node:crypto";
|
|
164
|
-
import { lstat, readFile as readFile2, readdir, stat } from "node:fs/promises";
|
|
165
|
-
import { join as
|
|
447
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
448
|
+
import { lstat, readFile as readFile2, readdir, realpath as realpath2, stat as stat2 } from "node:fs/promises";
|
|
449
|
+
import { join as join3, relative, resolve, sep } from "node:path";
|
|
166
450
|
async function hashDirectoryTree(root, manifestPath) {
|
|
167
451
|
const resolvedRoot = resolve(root);
|
|
168
452
|
const entries = await collectFiles(resolvedRoot, resolvedRoot);
|
|
169
|
-
const hash =
|
|
453
|
+
const hash = createHash2("sha256");
|
|
170
454
|
let bytes = 0;
|
|
171
455
|
for (const entry of entries) {
|
|
172
456
|
const contents = await readFile2(entry.absolutePath);
|
|
@@ -188,9 +472,9 @@ async function hashDirectoryTree(root, manifestPath) {
|
|
|
188
472
|
async function collectFiles(root, directory) {
|
|
189
473
|
const output = [];
|
|
190
474
|
for (const name of (await readdir(directory)).sort()) {
|
|
191
|
-
const absolutePath =
|
|
475
|
+
const absolutePath = join3(directory, name);
|
|
192
476
|
const linkInfo = await lstat(absolutePath);
|
|
193
|
-
const info = linkInfo.isSymbolicLink() ? await
|
|
477
|
+
const info = linkInfo.isSymbolicLink() ? await stat2(absolutePath) : linkInfo;
|
|
194
478
|
if (info.isDirectory()) {
|
|
195
479
|
if (linkInfo.isSymbolicLink()) {
|
|
196
480
|
throw new Error(`Runtime dependency tree contains a directory symlink: ${absolutePath}`);
|
|
@@ -201,6 +485,14 @@ async function collectFiles(root, directory) {
|
|
|
201
485
|
if (!info.isFile()) {
|
|
202
486
|
throw new Error(`Runtime dependency tree contains an unsupported entry: ${absolutePath}`);
|
|
203
487
|
}
|
|
488
|
+
if (linkInfo.isSymbolicLink()) {
|
|
489
|
+
const target = await realpath2(absolutePath);
|
|
490
|
+
if (target !== root && !target.startsWith(`${root}${sep}`)) {
|
|
491
|
+
throw new Error(
|
|
492
|
+
`Runtime dependency tree contains an external file symlink: ${absolutePath}`
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
204
496
|
const relativePath = relative(root, absolutePath).split(sep).join("/");
|
|
205
497
|
if (relativePath.length === 0 || relativePath.startsWith("../")) {
|
|
206
498
|
throw new Error(`Runtime dependency path escapes its root: ${absolutePath}`);
|
|
@@ -211,75 +503,296 @@ async function collectFiles(root, directory) {
|
|
|
211
503
|
}
|
|
212
504
|
|
|
213
505
|
// src/platform/install/runtime-release.ts
|
|
506
|
+
var requiredRuntimeArtifacts = /* @__PURE__ */ new Set([
|
|
507
|
+
"package.json",
|
|
508
|
+
"bin/pifleet.mjs",
|
|
509
|
+
"bin/pifleet-runtime.mjs",
|
|
510
|
+
"dist/cli.mjs",
|
|
511
|
+
"dist/runtime.mjs",
|
|
512
|
+
"dist/journal-sqlite-worker.mjs",
|
|
513
|
+
"dist/client.mjs",
|
|
514
|
+
"dist/client-meta.json",
|
|
515
|
+
"dist/client/index.d.ts",
|
|
516
|
+
"dist/client/sdk-facade.d.ts",
|
|
517
|
+
"dist/client/contracts.d.ts"
|
|
518
|
+
]);
|
|
519
|
+
var dependencyTreePath = "node_modules";
|
|
214
520
|
async function materializeRuntime(options) {
|
|
215
521
|
const sourceRoot = resolve2(options.sourceRoot);
|
|
522
|
+
const manifestBytes = await readFile3(join4(sourceRoot, "dist", "runtime-manifest.json"));
|
|
523
|
+
const sourceManifest = await parseRuntimeManifest(manifestBytes, sourceRoot);
|
|
524
|
+
if (sourceManifest.closure !== void 0) {
|
|
525
|
+
await verifyRuntime(sourceRoot);
|
|
526
|
+
return sourceRoot;
|
|
527
|
+
}
|
|
528
|
+
await verifyRuntimeFiles(sourceRoot, sourceManifest);
|
|
529
|
+
await verifyDependencyIdentities(sourceRoot, sourceManifest.dependencies);
|
|
530
|
+
const sourceTreeRoot = resolveInside(sourceRoot, dependencyTreePath);
|
|
531
|
+
const sourceTreeBefore = await hashDirectoryTree(sourceTreeRoot, dependencyTreePath);
|
|
532
|
+
const sourceManifestSha256 = createHash3("sha256").update(manifestBytes).digest("hex");
|
|
533
|
+
const materializedManifest = {
|
|
534
|
+
...sourceManifest,
|
|
535
|
+
closure: { sourceManifestSha256, tree: sourceTreeBefore }
|
|
536
|
+
};
|
|
537
|
+
const materializedManifestBytes = serializeManifest(materializedManifest);
|
|
538
|
+
const closureHash = createHash3("sha256").update(sourceManifestSha256).update("\0").update(JSON.stringify(sourceTreeBefore)).digest("hex").slice(0, 16);
|
|
216
539
|
const applicationRoot = resolve2(options.applicationRoot);
|
|
217
540
|
await ensurePrivateDirectory(applicationRoot);
|
|
218
|
-
const
|
|
219
|
-
const manifest = JSON.parse(manifestBytes.toString("utf8"));
|
|
220
|
-
const manifestHash = createHash2("sha256").update(manifestBytes).digest("hex").slice(0, 16);
|
|
221
|
-
const releasesRoot = join3(applicationRoot, "releases");
|
|
541
|
+
const releasesRoot = join4(applicationRoot, "releases");
|
|
222
542
|
await ensurePrivateDirectory(releasesRoot);
|
|
223
|
-
const destination =
|
|
543
|
+
const destination = join4(releasesRoot, `${sourceManifest.package.version}-${closureHash}`);
|
|
224
544
|
if (await pathExists(destination)) {
|
|
225
|
-
await
|
|
545
|
+
await verifyExpectedRuntime(destination, materializedManifest, materializedManifestBytes);
|
|
226
546
|
return destination;
|
|
227
547
|
}
|
|
228
|
-
const staging =
|
|
548
|
+
const staging = join4(releasesRoot, `.staging-${randomUUID()}`);
|
|
229
549
|
await mkdir2(staging, { mode: 448 });
|
|
230
550
|
try {
|
|
231
|
-
await cp(
|
|
551
|
+
await cp(join4(sourceRoot, "dist"), join4(staging, "dist"), {
|
|
232
552
|
recursive: true,
|
|
233
553
|
dereference: true
|
|
234
554
|
});
|
|
235
|
-
await cp(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
555
|
+
await cp(join4(sourceRoot, "bin"), join4(staging, "bin"), {
|
|
556
|
+
recursive: true,
|
|
557
|
+
dereference: true
|
|
558
|
+
});
|
|
559
|
+
await cp(join4(sourceRoot, "package.json"), join4(staging, "package.json"));
|
|
560
|
+
await cp(sourceTreeRoot, join4(staging, dependencyTreePath), {
|
|
561
|
+
recursive: true,
|
|
562
|
+
dereference: true
|
|
563
|
+
});
|
|
564
|
+
await options.hooks?.afterDependencyCopy?.();
|
|
565
|
+
const stagedTree = await hashDirectoryTree(
|
|
566
|
+
join4(staging, dependencyTreePath),
|
|
567
|
+
dependencyTreePath
|
|
568
|
+
);
|
|
569
|
+
assertSameTree(sourceTreeBefore, stagedTree, "copied dependency closure");
|
|
570
|
+
const sourceTreeAfter = await hashDirectoryTree(sourceTreeRoot, dependencyTreePath);
|
|
571
|
+
assertSameTree(sourceTreeBefore, sourceTreeAfter, "source dependency closure");
|
|
572
|
+
await writeFile2(join4(staging, "dist", "runtime-manifest.json"), materializedManifestBytes);
|
|
573
|
+
await verifyExpectedRuntime(staging, materializedManifest, materializedManifestBytes);
|
|
244
574
|
await chmod2(staging, 448);
|
|
245
|
-
|
|
575
|
+
try {
|
|
576
|
+
await rename2(staging, destination);
|
|
577
|
+
} catch (error) {
|
|
578
|
+
if (!isDestinationRace(error) || !await pathExists(destination)) throw error;
|
|
579
|
+
await verifyExpectedRuntime(destination, materializedManifest, materializedManifestBytes);
|
|
580
|
+
}
|
|
246
581
|
return destination;
|
|
247
|
-
}
|
|
582
|
+
} finally {
|
|
248
583
|
await rm2(staging, { recursive: true, force: true });
|
|
249
|
-
throw error;
|
|
250
584
|
}
|
|
251
585
|
}
|
|
252
|
-
async function verifyRuntime(root
|
|
253
|
-
const expected = manifest ?? JSON.parse(
|
|
254
|
-
await readFile3(join3(root, "dist", "runtime-manifest.json"), "utf8")
|
|
255
|
-
);
|
|
586
|
+
async function verifyRuntime(root) {
|
|
256
587
|
const resolvedRoot = resolve2(root);
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
588
|
+
const manifestBytes = await readFile3(join4(resolvedRoot, "dist", "runtime-manifest.json"));
|
|
589
|
+
const manifest = await parseRuntimeManifest(manifestBytes, resolvedRoot);
|
|
590
|
+
if (manifest.closure === void 0) {
|
|
591
|
+
throw new Error("Runtime release manifest is missing its materialized closure");
|
|
592
|
+
}
|
|
593
|
+
await verifyRuntimeFiles(resolvedRoot, manifest);
|
|
594
|
+
await verifyDependencyIdentities(resolvedRoot, manifest.dependencies);
|
|
595
|
+
const actualTree = await hashDirectoryTree(
|
|
596
|
+
resolveInside(resolvedRoot, dependencyTreePath),
|
|
597
|
+
dependencyTreePath
|
|
598
|
+
);
|
|
599
|
+
assertSameTree(manifest.closure.tree, actualTree, "materialized dependency closure");
|
|
600
|
+
}
|
|
601
|
+
async function verifyExpectedRuntime(root, expected, expectedBytes) {
|
|
602
|
+
const manifestPath = join4(root, "dist", "runtime-manifest.json");
|
|
603
|
+
const actualBytes = await readFile3(manifestPath);
|
|
604
|
+
if (!actualBytes.equals(expectedBytes)) {
|
|
605
|
+
throw new Error("Materialized runtime manifest does not match the expected closure");
|
|
606
|
+
}
|
|
607
|
+
const actual = await parseRuntimeManifest(actualBytes, root);
|
|
608
|
+
if (actual.closure === void 0 || expected.closure === void 0) {
|
|
609
|
+
throw new Error("Materialized runtime manifest is missing its closure");
|
|
610
|
+
}
|
|
611
|
+
await verifyRuntimeFiles(root, actual);
|
|
612
|
+
await verifyDependencyIdentities(root, actual.dependencies);
|
|
613
|
+
const actualTree = await hashDirectoryTree(
|
|
614
|
+
resolveInside(root, dependencyTreePath),
|
|
615
|
+
dependencyTreePath
|
|
616
|
+
);
|
|
617
|
+
assertSameTree(expected.closure.tree, actualTree, "materialized dependency closure");
|
|
618
|
+
}
|
|
619
|
+
async function verifyRuntimeFiles(root, manifest) {
|
|
620
|
+
for (const file of manifest.files) {
|
|
621
|
+
const path = resolveInside(root, file.path);
|
|
622
|
+
const info = await lstat2(path);
|
|
623
|
+
if (info.isSymbolicLink()) {
|
|
624
|
+
throw new Error(`Runtime artifact ${file.path} must not be a symbolic link`);
|
|
625
|
+
}
|
|
260
626
|
if (!info.isFile() || info.size !== file.bytes) {
|
|
261
627
|
throw new Error(`Runtime artifact ${file.path} has changed`);
|
|
262
628
|
}
|
|
263
|
-
const hash =
|
|
629
|
+
const hash = createHash3("sha256").update(await readFile3(path)).digest("hex");
|
|
264
630
|
if (hash !== file.sha256) {
|
|
265
631
|
throw new Error(`Runtime artifact ${file.path} failed verification`);
|
|
266
632
|
}
|
|
267
633
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
634
|
+
}
|
|
635
|
+
async function verifyDependencyIdentities(root, dependencies) {
|
|
636
|
+
const nodeModules = resolveInside(root, dependencyTreePath);
|
|
637
|
+
const modulesInfo = await lstat2(nodeModules);
|
|
638
|
+
if (!modulesInfo.isDirectory() || modulesInfo.isSymbolicLink()) {
|
|
639
|
+
throw new Error("Runtime dependency closure must be a regular directory");
|
|
640
|
+
}
|
|
641
|
+
for (const dependency of dependencies) {
|
|
642
|
+
const packageRoot = resolveInside(root, dependency.path);
|
|
643
|
+
const packageInfo = await lstat2(packageRoot);
|
|
644
|
+
if (!packageInfo.isDirectory() || packageInfo.isSymbolicLink()) {
|
|
645
|
+
throw new Error(`Runtime dependency ${dependency.name} must be a regular directory`);
|
|
646
|
+
}
|
|
647
|
+
const packageJsonPath = join4(packageRoot, "package.json");
|
|
648
|
+
const packageJsonInfo = await lstat2(packageJsonPath);
|
|
649
|
+
if (!packageJsonInfo.isFile() || packageJsonInfo.isSymbolicLink()) {
|
|
650
|
+
throw new Error(`Runtime dependency ${dependency.name} has an unsafe package.json`);
|
|
651
|
+
}
|
|
652
|
+
const identity = await readPackageMetadata(packageRoot);
|
|
653
|
+
if (identity.name !== dependency.name || identity.version !== dependency.version) {
|
|
654
|
+
throw new Error(`Runtime dependency ${dependency.name} has an unexpected identity`);
|
|
273
655
|
}
|
|
274
656
|
}
|
|
275
657
|
}
|
|
658
|
+
async function parseRuntimeManifest(bytes, root) {
|
|
659
|
+
let candidate;
|
|
660
|
+
try {
|
|
661
|
+
candidate = JSON.parse(bytes.toString("utf8"));
|
|
662
|
+
} catch {
|
|
663
|
+
throw new Error("Runtime manifest is not valid JSON");
|
|
664
|
+
}
|
|
665
|
+
await validateRuntimeManifest(candidate, root);
|
|
666
|
+
return candidate;
|
|
667
|
+
}
|
|
668
|
+
async function validateRuntimeManifest(candidate, root) {
|
|
669
|
+
if (!isRecord(candidate) || candidate.schemaVersion !== 4) {
|
|
670
|
+
throw new Error("Runtime manifest has an unsupported schema version");
|
|
671
|
+
}
|
|
672
|
+
if (!isRecord(candidate.package) || typeof candidate.package.name !== "string" || typeof candidate.package.version !== "string" || !isRecord(candidate.piRuntime) || candidate.piRuntime.mode !== "external" || !isRuntimeContract(candidate.runtime)) {
|
|
673
|
+
throw new Error("Runtime manifest has an invalid package identity");
|
|
674
|
+
}
|
|
675
|
+
const packageMetadata = await readPackageMetadata(root, true);
|
|
676
|
+
if (candidate.package.name !== packageMetadata.name || candidate.package.version !== packageMetadata.version || packageMetadata.runtime === void 0 || !sameRuntimeContract(candidate.runtime, packageMetadata.runtime) || packageMetadata.clientExport === void 0) {
|
|
677
|
+
throw new Error("Runtime manifest package identity does not match package.json");
|
|
678
|
+
}
|
|
679
|
+
if (!Array.isArray(candidate.files) || !Array.isArray(candidate.dependencies)) {
|
|
680
|
+
throw new Error("Runtime manifest has invalid artifact lists");
|
|
681
|
+
}
|
|
682
|
+
const filePaths = /* @__PURE__ */ new Set();
|
|
683
|
+
for (const file of candidate.files) {
|
|
684
|
+
if (!isRecord(file) || !isManifestPath(file.path) || !isValidSize(file.bytes) || !isSha256(file.sha256)) {
|
|
685
|
+
throw new Error("Runtime manifest contains an invalid artifact");
|
|
686
|
+
}
|
|
687
|
+
if (filePaths.has(file.path)) {
|
|
688
|
+
throw new Error(`Runtime manifest has duplicate path ${file.path}`);
|
|
689
|
+
}
|
|
690
|
+
filePaths.add(file.path);
|
|
691
|
+
}
|
|
692
|
+
for (const required of requiredRuntimeArtifacts) {
|
|
693
|
+
if (!filePaths.has(required)) {
|
|
694
|
+
throw new Error(`Runtime manifest is missing required artifact ${required}`);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
const dependencyPaths = /* @__PURE__ */ new Set();
|
|
698
|
+
const dependencyNames = /* @__PURE__ */ new Set();
|
|
699
|
+
for (const dependency of candidate.dependencies) {
|
|
700
|
+
if (!isRecord(dependency) || !isManifestPath(dependency.path) || typeof dependency.name !== "string" || dependency.name.length === 0 || typeof dependency.version !== "string" || dependency.version.length === 0 || dependency.path !== `node_modules/${dependency.name}`) {
|
|
701
|
+
throw new Error("Runtime manifest contains an invalid dependency declaration");
|
|
702
|
+
}
|
|
703
|
+
if (dependencyPaths.has(dependency.path) || dependencyNames.has(dependency.name)) {
|
|
704
|
+
throw new Error(`Runtime manifest has duplicate dependency ${dependency.name}`);
|
|
705
|
+
}
|
|
706
|
+
dependencyPaths.add(dependency.path);
|
|
707
|
+
dependencyNames.add(dependency.name);
|
|
708
|
+
}
|
|
709
|
+
const expectedDependencies = packageMetadata.dependencies;
|
|
710
|
+
if (dependencyNames.size !== Object.keys(expectedDependencies).length || [...dependencyNames].some(
|
|
711
|
+
(name) => expectedDependencies[name] !== candidate.dependencies.find(
|
|
712
|
+
(dependency) => dependency.name === name
|
|
713
|
+
)?.version
|
|
714
|
+
)) {
|
|
715
|
+
throw new Error("Runtime manifest dependencies do not match package.json");
|
|
716
|
+
}
|
|
717
|
+
for (const filePath of filePaths) {
|
|
718
|
+
for (const dependencyPath of dependencyPaths) {
|
|
719
|
+
if (filePath === dependencyPath || filePath.startsWith(`${dependencyPath}/`) || dependencyPath.startsWith(`${filePath}/`)) {
|
|
720
|
+
throw new Error(`Runtime manifest has overlapping paths ${filePath} and ${dependencyPath}`);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if (candidate.closure !== void 0) {
|
|
725
|
+
if (!isRecord(candidate.closure) || !isSha256(candidate.closure.sourceManifestSha256) || !isTreeIntegrity(candidate.closure.tree) || candidate.closure.tree.path !== dependencyTreePath) {
|
|
726
|
+
throw new Error("Runtime manifest contains an invalid materialized closure");
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
async function readPackageMetadata(root, requireRuntimeContract = false) {
|
|
731
|
+
let candidate;
|
|
732
|
+
try {
|
|
733
|
+
candidate = JSON.parse(await readFile3(join4(root, "package.json"), "utf8"));
|
|
734
|
+
} catch {
|
|
735
|
+
throw new Error("Runtime package.json is not valid JSON");
|
|
736
|
+
}
|
|
737
|
+
if (!isRecord(candidate) || typeof candidate.name !== "string" || typeof candidate.version !== "string" || requireRuntimeContract && !isRuntimeContract(candidate.pifleet) || candidate.dependencies !== void 0 && !isRecord(candidate.dependencies) || isRecord(candidate.dependencies) && Object.values(candidate.dependencies).some((version) => typeof version !== "string")) {
|
|
738
|
+
throw new Error("Runtime package.json has invalid package metadata");
|
|
739
|
+
}
|
|
740
|
+
const clientExport = isRecord(candidate.exports) ? candidate.exports["./client"] : void 0;
|
|
741
|
+
return {
|
|
742
|
+
name: candidate.name,
|
|
743
|
+
version: candidate.version,
|
|
744
|
+
...isRuntimeContract(candidate.pifleet) ? { runtime: candidate.pifleet } : {},
|
|
745
|
+
...isRecord(clientExport) && clientExport.types === "./dist/client/index.d.ts" && clientExport.import === "./dist/client.mjs" ? {
|
|
746
|
+
clientExport: {
|
|
747
|
+
types: "./dist/client/index.d.ts",
|
|
748
|
+
import: "./dist/client.mjs"
|
|
749
|
+
}
|
|
750
|
+
} : {},
|
|
751
|
+
dependencies: isRecord(candidate.dependencies) ? candidate.dependencies : {}
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
function serializeManifest(manifest) {
|
|
755
|
+
return Buffer.from(`${JSON.stringify(manifest, null, 2)}
|
|
756
|
+
`);
|
|
757
|
+
}
|
|
758
|
+
function assertSameTree(expected, actual, label) {
|
|
759
|
+
if (expected.path !== actual.path || expected.files !== actual.files || expected.bytes !== actual.bytes || expected.sha256 !== actual.sha256) {
|
|
760
|
+
throw new Error(`Runtime ${label} changed during materialization`);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
276
763
|
function resolveInside(root, path) {
|
|
764
|
+
if (!isManifestPath(path)) throw new Error(`Runtime manifest contains an unsafe path ${path}`);
|
|
277
765
|
const resolved = resolve2(root, path);
|
|
278
766
|
if (!resolved.startsWith(`${root}${sep2}`)) {
|
|
279
767
|
throw new Error(`Runtime manifest contains an unsafe path ${path}`);
|
|
280
768
|
}
|
|
281
769
|
return resolved;
|
|
282
770
|
}
|
|
771
|
+
function isRecord(value) {
|
|
772
|
+
return typeof value === "object" && value !== null;
|
|
773
|
+
}
|
|
774
|
+
function isRuntimeContract(value) {
|
|
775
|
+
return isRecord(value) && value.protocolVersion === 3 && value.journalSchemaVersion === 3 && value.clientExport === "./client";
|
|
776
|
+
}
|
|
777
|
+
function sameRuntimeContract(left, right) {
|
|
778
|
+
return left.protocolVersion === right.protocolVersion && left.journalSchemaVersion === right.journalSchemaVersion && left.clientExport === right.clientExport;
|
|
779
|
+
}
|
|
780
|
+
function isManifestPath(value) {
|
|
781
|
+
return typeof value === "string" && value.length > 0 && !value.includes("\\") && !value.startsWith("/") && posix.normalize(value) === value && value.split("/").every((part) => part.length > 0 && part !== "." && part !== "..");
|
|
782
|
+
}
|
|
783
|
+
function isValidSize(value) {
|
|
784
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
785
|
+
}
|
|
786
|
+
function isSha256(value) {
|
|
787
|
+
return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
788
|
+
}
|
|
789
|
+
function isTreeIntegrity(value) {
|
|
790
|
+
return isRecord(value) && isManifestPath(value.path) && isValidSize(value.files) && isValidSize(value.bytes) && isSha256(value.sha256);
|
|
791
|
+
}
|
|
792
|
+
function isDestinationRace(error) {
|
|
793
|
+
const code = error.code;
|
|
794
|
+
return code === "EEXIST" || code === "ENOTEMPTY";
|
|
795
|
+
}
|
|
283
796
|
async function ensurePrivateDirectory(path) {
|
|
284
797
|
await mkdir2(path, { recursive: true, mode: 448 });
|
|
285
798
|
const info = await lstat2(path);
|
|
@@ -303,32 +816,9 @@ async function pathExists(path) {
|
|
|
303
816
|
|
|
304
817
|
// src/platform/shared/paths.ts
|
|
305
818
|
import { homedir as homedir2, tmpdir } from "node:os";
|
|
306
|
-
import { join as
|
|
819
|
+
import { join as join5, resolve as resolve3 } from "node:path";
|
|
307
820
|
function resolveApplicationRoot(env = process.env) {
|
|
308
|
-
return env.PIFLEET_APPLICATION_ROOT ?? (process.platform === "darwin" ?
|
|
309
|
-
}
|
|
310
|
-
function resolveFleetPaths(env = process.env) {
|
|
311
|
-
const explicitRoot = env.PIFLEET_STATE_ROOT;
|
|
312
|
-
if (explicitRoot !== void 0) {
|
|
313
|
-
const root = resolve3(explicitRoot);
|
|
314
|
-
return {
|
|
315
|
-
runtimeRoot: root,
|
|
316
|
-
stateRoot: root,
|
|
317
|
-
socketPath: join4(root, "control.sock"),
|
|
318
|
-
databasePath: join4(root, "fleet.sqlite")
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
const runtimeRoot = join4(
|
|
322
|
-
env.XDG_RUNTIME_DIR ?? tmpdir(),
|
|
323
|
-
`pifleet-${process.getuid?.() ?? "user"}`
|
|
324
|
-
);
|
|
325
|
-
const stateRoot = process.platform === "darwin" ? join4(homedir2(), "Library", "Application Support", "Pi Fleet") : join4(env.XDG_STATE_HOME ?? join4(homedir2(), ".local", "state"), "pi-fleet");
|
|
326
|
-
return {
|
|
327
|
-
runtimeRoot,
|
|
328
|
-
stateRoot,
|
|
329
|
-
socketPath: join4(runtimeRoot, "control.sock"),
|
|
330
|
-
databasePath: join4(stateRoot, "fleet.sqlite")
|
|
331
|
-
};
|
|
821
|
+
return env.PIFLEET_APPLICATION_ROOT ?? (process.platform === "darwin" ? join5(homedir2(), "Library", "Application Support", "pi-fleet", "runtime") : join5(env.XDG_DATA_HOME ?? join5(homedir2(), ".local", "share"), "pi-fleet"));
|
|
332
822
|
}
|
|
333
823
|
|
|
334
824
|
// src/entry/installer.ts
|
|
@@ -338,25 +828,29 @@ var executor = {
|
|
|
338
828
|
}
|
|
339
829
|
};
|
|
340
830
|
async function installRuntimeService() {
|
|
341
|
-
return installMaterializedService(
|
|
831
|
+
return installMaterializedService(process.env.PIFLEET_STATE_ROOT);
|
|
342
832
|
}
|
|
343
833
|
async function repairRuntimeService() {
|
|
344
834
|
return installMaterializedService(process.env.PIFLEET_STATE_ROOT);
|
|
345
835
|
}
|
|
346
836
|
async function installMaterializedService(stateRoot) {
|
|
347
|
-
const sourceRoot =
|
|
837
|
+
const sourceRoot = dirname3(dirname3(fileURLToPath(import.meta.url)));
|
|
348
838
|
const release = await materializeRuntime({
|
|
349
839
|
sourceRoot,
|
|
350
840
|
applicationRoot: resolveApplicationRoot()
|
|
351
841
|
});
|
|
842
|
+
const pi = await resolveExternalPiInstallation({ env: process.env });
|
|
352
843
|
return installUserService({
|
|
353
844
|
platform: process.platform,
|
|
354
845
|
definition: {
|
|
355
846
|
nodePath: process.execPath,
|
|
356
|
-
runtimePath:
|
|
847
|
+
runtimePath: join6(release, "bin", "pifleet-runtime.mjs"),
|
|
848
|
+
piExecutablePath: pi.selectedPath,
|
|
849
|
+
piNodePath: pi.nodePath,
|
|
357
850
|
...stateRoot === void 0 ? {} : { stateRoot }
|
|
358
851
|
},
|
|
359
|
-
executor
|
|
852
|
+
executor,
|
|
853
|
+
replaceExisting: false
|
|
360
854
|
});
|
|
361
855
|
}
|
|
362
856
|
async function uninstallRuntimeService() {
|