@elpapi42/pi-fleet 0.1.0-beta.1 → 0.1.0-beta.10
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 +153 -3
- package/README.md +170 -104
- package/bin/pifleet-runtime.mjs +1 -1
- package/dist/cli-meta.json +5464 -82
- package/dist/cli.mjs +3481 -253
- package/dist/cli.mjs.map +4 -4
- package/dist/installer-meta.json +87 -13
- package/dist/installer.mjs +555 -95
- package/dist/installer.mjs.map +4 -4
- package/dist/runtime-manifest.json +37 -43
- package/dist/runtime-meta.json +2976 -2868
- package/dist/runtime.mjs +6316 -5247
- package/dist/runtime.mjs.map +4 -4
- package/dist/sqlite-worker-meta.json +6 -6
- package/dist/sqlite-worker.mjs +95 -40
- package/dist/sqlite-worker.mjs.map +2 -2
- package/package.json +11 -5
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,58 @@ ${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
|
+
}
|
|
62
303
|
}
|
|
63
|
-
function
|
|
64
|
-
|
|
65
|
-
|
|
304
|
+
function systemdArgument(value) {
|
|
305
|
+
return /^[A-Za-z0-9_./:@-]+$/.test(value) ? value : systemdQuote(value);
|
|
306
|
+
}
|
|
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
|
+
if (needsRepair && options.replaceExisting === false && await exists(path)) {
|
|
333
|
+
throw new ServiceRepairDeferredError();
|
|
334
|
+
}
|
|
79
335
|
if (needsRepair) {
|
|
80
336
|
await atomicWrite(path, definition);
|
|
81
337
|
await options.executor.run("systemctl", ["--user", "daemon-reload"]);
|
|
@@ -87,38 +343,43 @@ async function installUserService(options) {
|
|
|
87
343
|
return path;
|
|
88
344
|
}
|
|
89
345
|
if (options.platform === "darwin") {
|
|
90
|
-
const path =
|
|
346
|
+
const path = join2(home, "Library", "LaunchAgents", "works.elpapi.pifleet.plist");
|
|
91
347
|
const effectiveDefinition = await preserveInstalledStateRoot(
|
|
92
348
|
path,
|
|
93
349
|
options.definition,
|
|
94
350
|
"darwin"
|
|
95
351
|
);
|
|
96
|
-
|
|
352
|
+
const definition = launchdAgentPlist(effectiveDefinition);
|
|
353
|
+
const needsRepair = await serviceDefinitionNeedsRepair(path, definition, effectiveDefinition);
|
|
354
|
+
if (needsRepair && options.replaceExisting === false && await exists(path)) {
|
|
355
|
+
throw new ServiceRepairDeferredError();
|
|
356
|
+
}
|
|
357
|
+
await atomicWrite(path, definition);
|
|
97
358
|
const domain = `gui/${options.uid ?? process.getuid?.() ?? 0}`;
|
|
98
359
|
await options.executor.run("launchctl", ["bootout", domain, path]).catch(() => void 0);
|
|
99
360
|
await options.executor.run("launchctl", ["bootstrap", domain, path]);
|
|
100
361
|
await options.executor.run("launchctl", ["kickstart", `${domain}/works.elpapi.pifleet`]);
|
|
101
362
|
return path;
|
|
102
363
|
}
|
|
103
|
-
throw new Error(`Native
|
|
364
|
+
throw new Error(`Native pi-fleet supervision is unsupported on ${options.platform}`);
|
|
104
365
|
}
|
|
105
366
|
async function uninstallUserService(options) {
|
|
106
367
|
const home = options.home ?? homedir();
|
|
107
368
|
if (options.platform === "linux") {
|
|
108
|
-
const path =
|
|
369
|
+
const path = join2(home, ".config", "systemd", "user", "pi-fleet.service");
|
|
109
370
|
await options.executor.run("systemctl", ["--user", "disable", "--now", "pi-fleet.service"]);
|
|
110
371
|
await rm(path, { force: true });
|
|
111
372
|
await options.executor.run("systemctl", ["--user", "daemon-reload"]);
|
|
112
373
|
return;
|
|
113
374
|
}
|
|
114
375
|
if (options.platform === "darwin") {
|
|
115
|
-
const path =
|
|
376
|
+
const path = join2(home, "Library", "LaunchAgents", "works.elpapi.pifleet.plist");
|
|
116
377
|
const domain = `gui/${options.uid ?? process.getuid?.() ?? 0}`;
|
|
117
378
|
await options.executor.run("launchctl", ["bootout", domain, path]).catch(() => void 0);
|
|
118
379
|
await rm(path, { force: true });
|
|
119
380
|
return;
|
|
120
381
|
}
|
|
121
|
-
throw new Error(`Native
|
|
382
|
+
throw new Error(`Native pi-fleet supervision is unsupported on ${options.platform}`);
|
|
122
383
|
}
|
|
123
384
|
async function serviceDefinitionNeedsRepair(path, expectedContents, definition) {
|
|
124
385
|
const current = await readFile(path, "utf8").catch((error) => {
|
|
@@ -127,8 +388,8 @@ async function serviceDefinitionNeedsRepair(path, expectedContents, definition)
|
|
|
127
388
|
});
|
|
128
389
|
if (current !== expectedContents) return true;
|
|
129
390
|
try {
|
|
130
|
-
await
|
|
131
|
-
await
|
|
391
|
+
await access2(definition.nodePath, constants2.X_OK);
|
|
392
|
+
await access2(definition.runtimePath, constants2.R_OK);
|
|
132
393
|
return false;
|
|
133
394
|
} catch {
|
|
134
395
|
return true;
|
|
@@ -146,8 +407,16 @@ async function preserveInstalledStateRoot(path, definition, platform) {
|
|
|
146
407
|
const stateRoot = platform === "linux" ? encoded : encoded.replaceAll("'", "'").replaceAll(""", '"').replaceAll(">", ">").replaceAll("<", "<").replaceAll("&", "&");
|
|
147
408
|
return { ...definition, stateRoot };
|
|
148
409
|
}
|
|
410
|
+
async function exists(path) {
|
|
411
|
+
try {
|
|
412
|
+
await access2(path);
|
|
413
|
+
return true;
|
|
414
|
+
} catch {
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
149
418
|
async function atomicWrite(path, contents) {
|
|
150
|
-
await mkdir(
|
|
419
|
+
await mkdir(dirname2(path), { recursive: true, mode: 448 });
|
|
151
420
|
const temporary = `${path}.tmp-${process.pid}`;
|
|
152
421
|
await writeFile(temporary, contents, { mode: 384 });
|
|
153
422
|
await chmod(temporary, 384);
|
|
@@ -155,18 +424,18 @@ async function atomicWrite(path, contents) {
|
|
|
155
424
|
}
|
|
156
425
|
|
|
157
426
|
// 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
|
|
427
|
+
import { createHash as createHash3, randomUUID } from "node:crypto";
|
|
428
|
+
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";
|
|
429
|
+
import { join as join4, posix, resolve as resolve2, sep as sep2 } from "node:path";
|
|
161
430
|
|
|
162
431
|
// 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
|
|
432
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
433
|
+
import { lstat, readFile as readFile2, readdir, realpath as realpath2, stat as stat2 } from "node:fs/promises";
|
|
434
|
+
import { join as join3, relative, resolve, sep } from "node:path";
|
|
166
435
|
async function hashDirectoryTree(root, manifestPath) {
|
|
167
436
|
const resolvedRoot = resolve(root);
|
|
168
437
|
const entries = await collectFiles(resolvedRoot, resolvedRoot);
|
|
169
|
-
const hash =
|
|
438
|
+
const hash = createHash2("sha256");
|
|
170
439
|
let bytes = 0;
|
|
171
440
|
for (const entry of entries) {
|
|
172
441
|
const contents = await readFile2(entry.absolutePath);
|
|
@@ -188,9 +457,9 @@ async function hashDirectoryTree(root, manifestPath) {
|
|
|
188
457
|
async function collectFiles(root, directory) {
|
|
189
458
|
const output = [];
|
|
190
459
|
for (const name of (await readdir(directory)).sort()) {
|
|
191
|
-
const absolutePath =
|
|
460
|
+
const absolutePath = join3(directory, name);
|
|
192
461
|
const linkInfo = await lstat(absolutePath);
|
|
193
|
-
const info = linkInfo.isSymbolicLink() ? await
|
|
462
|
+
const info = linkInfo.isSymbolicLink() ? await stat2(absolutePath) : linkInfo;
|
|
194
463
|
if (info.isDirectory()) {
|
|
195
464
|
if (linkInfo.isSymbolicLink()) {
|
|
196
465
|
throw new Error(`Runtime dependency tree contains a directory symlink: ${absolutePath}`);
|
|
@@ -201,6 +470,14 @@ async function collectFiles(root, directory) {
|
|
|
201
470
|
if (!info.isFile()) {
|
|
202
471
|
throw new Error(`Runtime dependency tree contains an unsupported entry: ${absolutePath}`);
|
|
203
472
|
}
|
|
473
|
+
if (linkInfo.isSymbolicLink()) {
|
|
474
|
+
const target = await realpath2(absolutePath);
|
|
475
|
+
if (target !== root && !target.startsWith(`${root}${sep}`)) {
|
|
476
|
+
throw new Error(
|
|
477
|
+
`Runtime dependency tree contains an external file symlink: ${absolutePath}`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
204
481
|
const relativePath = relative(root, absolutePath).split(sep).join("/");
|
|
205
482
|
if (relativePath.length === 0 || relativePath.startsWith("../")) {
|
|
206
483
|
throw new Error(`Runtime dependency path escapes its root: ${absolutePath}`);
|
|
@@ -211,75 +488,277 @@ async function collectFiles(root, directory) {
|
|
|
211
488
|
}
|
|
212
489
|
|
|
213
490
|
// src/platform/install/runtime-release.ts
|
|
491
|
+
var requiredRuntimeArtifacts = /* @__PURE__ */ new Set([
|
|
492
|
+
"package.json",
|
|
493
|
+
"bin/pifleet.mjs",
|
|
494
|
+
"bin/pifleet-runtime.mjs",
|
|
495
|
+
"dist/cli.mjs",
|
|
496
|
+
"dist/runtime.mjs",
|
|
497
|
+
"dist/sqlite-worker.mjs"
|
|
498
|
+
]);
|
|
499
|
+
var dependencyTreePath = "node_modules";
|
|
214
500
|
async function materializeRuntime(options) {
|
|
215
501
|
const sourceRoot = resolve2(options.sourceRoot);
|
|
502
|
+
const manifestBytes = await readFile3(join4(sourceRoot, "dist", "runtime-manifest.json"));
|
|
503
|
+
const sourceManifest = await parseRuntimeManifest(manifestBytes, sourceRoot);
|
|
504
|
+
if (sourceManifest.closure !== void 0) {
|
|
505
|
+
await verifyRuntime(sourceRoot);
|
|
506
|
+
return sourceRoot;
|
|
507
|
+
}
|
|
508
|
+
await verifyRuntimeFiles(sourceRoot, sourceManifest);
|
|
509
|
+
await verifyDependencyIdentities(sourceRoot, sourceManifest.dependencies);
|
|
510
|
+
const sourceTreeRoot = resolveInside(sourceRoot, dependencyTreePath);
|
|
511
|
+
const sourceTreeBefore = await hashDirectoryTree(sourceTreeRoot, dependencyTreePath);
|
|
512
|
+
const sourceManifestSha256 = createHash3("sha256").update(manifestBytes).digest("hex");
|
|
513
|
+
const materializedManifest = {
|
|
514
|
+
...sourceManifest,
|
|
515
|
+
closure: { sourceManifestSha256, tree: sourceTreeBefore }
|
|
516
|
+
};
|
|
517
|
+
const materializedManifestBytes = serializeManifest(materializedManifest);
|
|
518
|
+
const closureHash = createHash3("sha256").update(sourceManifestSha256).update("\0").update(JSON.stringify(sourceTreeBefore)).digest("hex").slice(0, 16);
|
|
216
519
|
const applicationRoot = resolve2(options.applicationRoot);
|
|
217
520
|
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");
|
|
521
|
+
const releasesRoot = join4(applicationRoot, "releases");
|
|
222
522
|
await ensurePrivateDirectory(releasesRoot);
|
|
223
|
-
const destination =
|
|
523
|
+
const destination = join4(releasesRoot, `${sourceManifest.package.version}-${closureHash}`);
|
|
224
524
|
if (await pathExists(destination)) {
|
|
225
|
-
await
|
|
525
|
+
await verifyExpectedRuntime(destination, materializedManifest, materializedManifestBytes);
|
|
226
526
|
return destination;
|
|
227
527
|
}
|
|
228
|
-
const staging =
|
|
528
|
+
const staging = join4(releasesRoot, `.staging-${randomUUID()}`);
|
|
229
529
|
await mkdir2(staging, { mode: 448 });
|
|
230
530
|
try {
|
|
231
|
-
await cp(
|
|
531
|
+
await cp(join4(sourceRoot, "dist"), join4(staging, "dist"), {
|
|
232
532
|
recursive: true,
|
|
233
533
|
dereference: true
|
|
234
534
|
});
|
|
235
|
-
await cp(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
535
|
+
await cp(join4(sourceRoot, "bin"), join4(staging, "bin"), {
|
|
536
|
+
recursive: true,
|
|
537
|
+
dereference: true
|
|
538
|
+
});
|
|
539
|
+
await cp(join4(sourceRoot, "package.json"), join4(staging, "package.json"));
|
|
540
|
+
await cp(sourceTreeRoot, join4(staging, dependencyTreePath), {
|
|
541
|
+
recursive: true,
|
|
542
|
+
dereference: true
|
|
543
|
+
});
|
|
544
|
+
await options.hooks?.afterDependencyCopy?.();
|
|
545
|
+
const stagedTree = await hashDirectoryTree(
|
|
546
|
+
join4(staging, dependencyTreePath),
|
|
547
|
+
dependencyTreePath
|
|
548
|
+
);
|
|
549
|
+
assertSameTree(sourceTreeBefore, stagedTree, "copied dependency closure");
|
|
550
|
+
const sourceTreeAfter = await hashDirectoryTree(sourceTreeRoot, dependencyTreePath);
|
|
551
|
+
assertSameTree(sourceTreeBefore, sourceTreeAfter, "source dependency closure");
|
|
552
|
+
await writeFile2(join4(staging, "dist", "runtime-manifest.json"), materializedManifestBytes);
|
|
553
|
+
await verifyExpectedRuntime(staging, materializedManifest, materializedManifestBytes);
|
|
244
554
|
await chmod2(staging, 448);
|
|
245
|
-
|
|
555
|
+
try {
|
|
556
|
+
await rename2(staging, destination);
|
|
557
|
+
} catch (error) {
|
|
558
|
+
if (!isDestinationRace(error) || !await pathExists(destination)) throw error;
|
|
559
|
+
await verifyExpectedRuntime(destination, materializedManifest, materializedManifestBytes);
|
|
560
|
+
}
|
|
246
561
|
return destination;
|
|
247
|
-
}
|
|
562
|
+
} finally {
|
|
248
563
|
await rm2(staging, { recursive: true, force: true });
|
|
249
|
-
throw error;
|
|
250
564
|
}
|
|
251
565
|
}
|
|
252
|
-
async function verifyRuntime(root
|
|
253
|
-
const expected = manifest ?? JSON.parse(
|
|
254
|
-
await readFile3(join3(root, "dist", "runtime-manifest.json"), "utf8")
|
|
255
|
-
);
|
|
566
|
+
async function verifyRuntime(root) {
|
|
256
567
|
const resolvedRoot = resolve2(root);
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
568
|
+
const manifestBytes = await readFile3(join4(resolvedRoot, "dist", "runtime-manifest.json"));
|
|
569
|
+
const manifest = await parseRuntimeManifest(manifestBytes, resolvedRoot);
|
|
570
|
+
if (manifest.closure === void 0) {
|
|
571
|
+
throw new Error("Runtime release manifest is missing its materialized closure");
|
|
572
|
+
}
|
|
573
|
+
await verifyRuntimeFiles(resolvedRoot, manifest);
|
|
574
|
+
await verifyDependencyIdentities(resolvedRoot, manifest.dependencies);
|
|
575
|
+
const actualTree = await hashDirectoryTree(
|
|
576
|
+
resolveInside(resolvedRoot, dependencyTreePath),
|
|
577
|
+
dependencyTreePath
|
|
578
|
+
);
|
|
579
|
+
assertSameTree(manifest.closure.tree, actualTree, "materialized dependency closure");
|
|
580
|
+
}
|
|
581
|
+
async function verifyExpectedRuntime(root, expected, expectedBytes) {
|
|
582
|
+
const manifestPath = join4(root, "dist", "runtime-manifest.json");
|
|
583
|
+
const actualBytes = await readFile3(manifestPath);
|
|
584
|
+
if (!actualBytes.equals(expectedBytes)) {
|
|
585
|
+
throw new Error("Materialized runtime manifest does not match the expected closure");
|
|
586
|
+
}
|
|
587
|
+
const actual = await parseRuntimeManifest(actualBytes, root);
|
|
588
|
+
if (actual.closure === void 0 || expected.closure === void 0) {
|
|
589
|
+
throw new Error("Materialized runtime manifest is missing its closure");
|
|
590
|
+
}
|
|
591
|
+
await verifyRuntimeFiles(root, actual);
|
|
592
|
+
await verifyDependencyIdentities(root, actual.dependencies);
|
|
593
|
+
const actualTree = await hashDirectoryTree(
|
|
594
|
+
resolveInside(root, dependencyTreePath),
|
|
595
|
+
dependencyTreePath
|
|
596
|
+
);
|
|
597
|
+
assertSameTree(expected.closure.tree, actualTree, "materialized dependency closure");
|
|
598
|
+
}
|
|
599
|
+
async function verifyRuntimeFiles(root, manifest) {
|
|
600
|
+
for (const file of manifest.files) {
|
|
601
|
+
const path = resolveInside(root, file.path);
|
|
602
|
+
const info = await lstat2(path);
|
|
603
|
+
if (info.isSymbolicLink()) {
|
|
604
|
+
throw new Error(`Runtime artifact ${file.path} must not be a symbolic link`);
|
|
605
|
+
}
|
|
260
606
|
if (!info.isFile() || info.size !== file.bytes) {
|
|
261
607
|
throw new Error(`Runtime artifact ${file.path} has changed`);
|
|
262
608
|
}
|
|
263
|
-
const hash =
|
|
609
|
+
const hash = createHash3("sha256").update(await readFile3(path)).digest("hex");
|
|
264
610
|
if (hash !== file.sha256) {
|
|
265
611
|
throw new Error(`Runtime artifact ${file.path} failed verification`);
|
|
266
612
|
}
|
|
267
613
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
614
|
+
}
|
|
615
|
+
async function verifyDependencyIdentities(root, dependencies) {
|
|
616
|
+
const nodeModules = resolveInside(root, dependencyTreePath);
|
|
617
|
+
const modulesInfo = await lstat2(nodeModules);
|
|
618
|
+
if (!modulesInfo.isDirectory() || modulesInfo.isSymbolicLink()) {
|
|
619
|
+
throw new Error("Runtime dependency closure must be a regular directory");
|
|
620
|
+
}
|
|
621
|
+
for (const dependency of dependencies) {
|
|
622
|
+
const packageRoot = resolveInside(root, dependency.path);
|
|
623
|
+
const packageInfo = await lstat2(packageRoot);
|
|
624
|
+
if (!packageInfo.isDirectory() || packageInfo.isSymbolicLink()) {
|
|
625
|
+
throw new Error(`Runtime dependency ${dependency.name} must be a regular directory`);
|
|
626
|
+
}
|
|
627
|
+
const packageJsonPath = join4(packageRoot, "package.json");
|
|
628
|
+
const packageJsonInfo = await lstat2(packageJsonPath);
|
|
629
|
+
if (!packageJsonInfo.isFile() || packageJsonInfo.isSymbolicLink()) {
|
|
630
|
+
throw new Error(`Runtime dependency ${dependency.name} has an unsafe package.json`);
|
|
631
|
+
}
|
|
632
|
+
const identity = await readPackageMetadata(packageRoot);
|
|
633
|
+
if (identity.name !== dependency.name || identity.version !== dependency.version) {
|
|
634
|
+
throw new Error(`Runtime dependency ${dependency.name} has an unexpected identity`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
async function parseRuntimeManifest(bytes, root) {
|
|
639
|
+
let candidate;
|
|
640
|
+
try {
|
|
641
|
+
candidate = JSON.parse(bytes.toString("utf8"));
|
|
642
|
+
} catch {
|
|
643
|
+
throw new Error("Runtime manifest is not valid JSON");
|
|
644
|
+
}
|
|
645
|
+
await validateRuntimeManifest(candidate, root);
|
|
646
|
+
return candidate;
|
|
647
|
+
}
|
|
648
|
+
async function validateRuntimeManifest(candidate, root) {
|
|
649
|
+
if (!isRecord(candidate) || candidate.schemaVersion !== 4) {
|
|
650
|
+
throw new Error("Runtime manifest has an unsupported schema version");
|
|
651
|
+
}
|
|
652
|
+
if (!isRecord(candidate.package) || typeof candidate.package.name !== "string" || typeof candidate.package.version !== "string" || !isRecord(candidate.piRuntime) || candidate.piRuntime.mode !== "external") {
|
|
653
|
+
throw new Error("Runtime manifest has an invalid package identity");
|
|
654
|
+
}
|
|
655
|
+
const packageMetadata = await readPackageMetadata(root);
|
|
656
|
+
if (candidate.package.name !== packageMetadata.name || candidate.package.version !== packageMetadata.version) {
|
|
657
|
+
throw new Error("Runtime manifest package identity does not match package.json");
|
|
658
|
+
}
|
|
659
|
+
if (!Array.isArray(candidate.files) || !Array.isArray(candidate.dependencies)) {
|
|
660
|
+
throw new Error("Runtime manifest has invalid artifact lists");
|
|
661
|
+
}
|
|
662
|
+
const filePaths = /* @__PURE__ */ new Set();
|
|
663
|
+
for (const file of candidate.files) {
|
|
664
|
+
if (!isRecord(file) || !isManifestPath(file.path) || !isValidSize(file.bytes) || !isSha256(file.sha256)) {
|
|
665
|
+
throw new Error("Runtime manifest contains an invalid artifact");
|
|
666
|
+
}
|
|
667
|
+
if (filePaths.has(file.path)) {
|
|
668
|
+
throw new Error(`Runtime manifest has duplicate path ${file.path}`);
|
|
669
|
+
}
|
|
670
|
+
filePaths.add(file.path);
|
|
671
|
+
}
|
|
672
|
+
for (const required of requiredRuntimeArtifacts) {
|
|
673
|
+
if (!filePaths.has(required)) {
|
|
674
|
+
throw new Error(`Runtime manifest is missing required artifact ${required}`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
const dependencyPaths = /* @__PURE__ */ new Set();
|
|
678
|
+
const dependencyNames = /* @__PURE__ */ new Set();
|
|
679
|
+
for (const dependency of candidate.dependencies) {
|
|
680
|
+
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}`) {
|
|
681
|
+
throw new Error("Runtime manifest contains an invalid dependency declaration");
|
|
682
|
+
}
|
|
683
|
+
if (dependencyPaths.has(dependency.path) || dependencyNames.has(dependency.name)) {
|
|
684
|
+
throw new Error(`Runtime manifest has duplicate dependency ${dependency.name}`);
|
|
685
|
+
}
|
|
686
|
+
dependencyPaths.add(dependency.path);
|
|
687
|
+
dependencyNames.add(dependency.name);
|
|
688
|
+
}
|
|
689
|
+
const expectedDependencies = packageMetadata.dependencies;
|
|
690
|
+
if (dependencyNames.size !== Object.keys(expectedDependencies).length || [...dependencyNames].some(
|
|
691
|
+
(name) => expectedDependencies[name] !== candidate.dependencies.find(
|
|
692
|
+
(dependency) => dependency.name === name
|
|
693
|
+
)?.version
|
|
694
|
+
)) {
|
|
695
|
+
throw new Error("Runtime manifest dependencies do not match package.json");
|
|
696
|
+
}
|
|
697
|
+
for (const filePath of filePaths) {
|
|
698
|
+
for (const dependencyPath of dependencyPaths) {
|
|
699
|
+
if (filePath === dependencyPath || filePath.startsWith(`${dependencyPath}/`) || dependencyPath.startsWith(`${filePath}/`)) {
|
|
700
|
+
throw new Error(`Runtime manifest has overlapping paths ${filePath} and ${dependencyPath}`);
|
|
701
|
+
}
|
|
273
702
|
}
|
|
274
703
|
}
|
|
704
|
+
if (candidate.closure !== void 0) {
|
|
705
|
+
if (!isRecord(candidate.closure) || !isSha256(candidate.closure.sourceManifestSha256) || !isTreeIntegrity(candidate.closure.tree) || candidate.closure.tree.path !== dependencyTreePath) {
|
|
706
|
+
throw new Error("Runtime manifest contains an invalid materialized closure");
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
async function readPackageMetadata(root) {
|
|
711
|
+
let candidate;
|
|
712
|
+
try {
|
|
713
|
+
candidate = JSON.parse(await readFile3(join4(root, "package.json"), "utf8"));
|
|
714
|
+
} catch {
|
|
715
|
+
throw new Error("Runtime package.json is not valid JSON");
|
|
716
|
+
}
|
|
717
|
+
if (!isRecord(candidate) || typeof candidate.name !== "string" || typeof candidate.version !== "string" || candidate.dependencies !== void 0 && !isRecord(candidate.dependencies) || isRecord(candidate.dependencies) && Object.values(candidate.dependencies).some((version) => typeof version !== "string")) {
|
|
718
|
+
throw new Error("Runtime package.json has invalid package metadata");
|
|
719
|
+
}
|
|
720
|
+
return {
|
|
721
|
+
name: candidate.name,
|
|
722
|
+
version: candidate.version,
|
|
723
|
+
dependencies: isRecord(candidate.dependencies) ? candidate.dependencies : {}
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
function serializeManifest(manifest) {
|
|
727
|
+
return Buffer.from(`${JSON.stringify(manifest, null, 2)}
|
|
728
|
+
`);
|
|
729
|
+
}
|
|
730
|
+
function assertSameTree(expected, actual, label) {
|
|
731
|
+
if (expected.path !== actual.path || expected.files !== actual.files || expected.bytes !== actual.bytes || expected.sha256 !== actual.sha256) {
|
|
732
|
+
throw new Error(`Runtime ${label} changed during materialization`);
|
|
733
|
+
}
|
|
275
734
|
}
|
|
276
735
|
function resolveInside(root, path) {
|
|
736
|
+
if (!isManifestPath(path)) throw new Error(`Runtime manifest contains an unsafe path ${path}`);
|
|
277
737
|
const resolved = resolve2(root, path);
|
|
278
738
|
if (!resolved.startsWith(`${root}${sep2}`)) {
|
|
279
739
|
throw new Error(`Runtime manifest contains an unsafe path ${path}`);
|
|
280
740
|
}
|
|
281
741
|
return resolved;
|
|
282
742
|
}
|
|
743
|
+
function isRecord(value) {
|
|
744
|
+
return typeof value === "object" && value !== null;
|
|
745
|
+
}
|
|
746
|
+
function isManifestPath(value) {
|
|
747
|
+
return typeof value === "string" && value.length > 0 && !value.includes("\\") && !value.startsWith("/") && posix.normalize(value) === value && value.split("/").every((part) => part.length > 0 && part !== "." && part !== "..");
|
|
748
|
+
}
|
|
749
|
+
function isValidSize(value) {
|
|
750
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
751
|
+
}
|
|
752
|
+
function isSha256(value) {
|
|
753
|
+
return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
754
|
+
}
|
|
755
|
+
function isTreeIntegrity(value) {
|
|
756
|
+
return isRecord(value) && isManifestPath(value.path) && isValidSize(value.files) && isValidSize(value.bytes) && isSha256(value.sha256);
|
|
757
|
+
}
|
|
758
|
+
function isDestinationRace(error) {
|
|
759
|
+
const code = error.code;
|
|
760
|
+
return code === "EEXIST" || code === "ENOTEMPTY";
|
|
761
|
+
}
|
|
283
762
|
async function ensurePrivateDirectory(path) {
|
|
284
763
|
await mkdir2(path, { recursive: true, mode: 448 });
|
|
285
764
|
const info = await lstat2(path);
|
|
@@ -303,32 +782,9 @@ async function pathExists(path) {
|
|
|
303
782
|
|
|
304
783
|
// src/platform/shared/paths.ts
|
|
305
784
|
import { homedir as homedir2, tmpdir } from "node:os";
|
|
306
|
-
import { join as
|
|
785
|
+
import { join as join5, resolve as resolve3 } from "node:path";
|
|
307
786
|
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
|
-
};
|
|
787
|
+
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
788
|
}
|
|
333
789
|
|
|
334
790
|
// src/entry/installer.ts
|
|
@@ -338,25 +794,29 @@ var executor = {
|
|
|
338
794
|
}
|
|
339
795
|
};
|
|
340
796
|
async function installRuntimeService() {
|
|
341
|
-
return installMaterializedService(
|
|
797
|
+
return installMaterializedService(process.env.PIFLEET_STATE_ROOT);
|
|
342
798
|
}
|
|
343
799
|
async function repairRuntimeService() {
|
|
344
800
|
return installMaterializedService(process.env.PIFLEET_STATE_ROOT);
|
|
345
801
|
}
|
|
346
802
|
async function installMaterializedService(stateRoot) {
|
|
347
|
-
const sourceRoot =
|
|
803
|
+
const sourceRoot = dirname3(dirname3(fileURLToPath(import.meta.url)));
|
|
348
804
|
const release = await materializeRuntime({
|
|
349
805
|
sourceRoot,
|
|
350
806
|
applicationRoot: resolveApplicationRoot()
|
|
351
807
|
});
|
|
808
|
+
const pi = await resolveExternalPiInstallation({ env: process.env });
|
|
352
809
|
return installUserService({
|
|
353
810
|
platform: process.platform,
|
|
354
811
|
definition: {
|
|
355
812
|
nodePath: process.execPath,
|
|
356
|
-
runtimePath:
|
|
813
|
+
runtimePath: join6(release, "bin", "pifleet-runtime.mjs"),
|
|
814
|
+
piExecutablePath: pi.selectedPath,
|
|
815
|
+
piNodePath: pi.nodePath,
|
|
357
816
|
...stateRoot === void 0 ? {} : { stateRoot }
|
|
358
817
|
},
|
|
359
|
-
executor
|
|
818
|
+
executor,
|
|
819
|
+
replaceExisting: false
|
|
360
820
|
});
|
|
361
821
|
}
|
|
362
822
|
async function uninstallRuntimeService() {
|