@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +180 -3
  2. package/README.md +118 -120
  3. package/bin/pifleet-runtime.mjs +1 -1
  4. package/dist/cli-meta.json +5555 -175
  5. package/dist/cli.mjs +4391 -471
  6. package/dist/cli.mjs.map +4 -4
  7. package/dist/client/agent-target.d.ts +33 -0
  8. package/dist/client/contracts.d.ts +81 -0
  9. package/dist/client/fleet-client.d.ts +118 -0
  10. package/dist/client/index.d.ts +4 -0
  11. package/dist/client/sdk-connector.d.ts +7 -0
  12. package/dist/client/sdk-facade.d.ts +84 -0
  13. package/dist/client/sdk-transport.d.ts +24 -0
  14. package/dist/client/shared-client.d.ts +18 -0
  15. package/dist/client/socket-fleet-client.d.ts +21 -0
  16. package/dist/client-meta.json +5736 -0
  17. package/dist/client.mjs +4434 -0
  18. package/dist/client.mjs.map +7 -0
  19. package/dist/installer-meta.json +87 -13
  20. package/dist/installer.mjs +589 -95
  21. package/dist/installer.mjs.map +4 -4
  22. package/dist/journal-sqlite-worker-meta.json +149 -0
  23. package/dist/journal-sqlite-worker.mjs +1110 -0
  24. package/dist/journal-sqlite-worker.mjs.map +7 -0
  25. package/dist/pi/external-installation.d.ts +23 -0
  26. package/dist/platform/client/start-runtime.d.ts +25 -0
  27. package/dist/platform/install/runtime-release.d.ts +8 -0
  28. package/dist/platform/install/tree-integrity.d.ts +7 -0
  29. package/dist/platform/shared/paths.d.ts +8 -0
  30. package/dist/platform/shared/runtime-ownership.d.ts +14 -0
  31. package/dist/protocol/jsonl.d.ts +3 -0
  32. package/dist/protocol/pi-identity.d.ts +16 -0
  33. package/dist/protocol/semantic-segmentation.d.ts +22 -0
  34. package/dist/protocol/version.d.ts +2 -0
  35. package/dist/runtime/semantic-events.d.ts +43 -0
  36. package/dist/runtime-manifest.json +167 -48
  37. package/dist/runtime-meta.json +3241 -2808
  38. package/dist/runtime.mjs +9290 -5641
  39. package/dist/runtime.mjs.map +4 -4
  40. package/dist/shared/result.d.ts +9 -0
  41. package/package.json +24 -5
  42. package/dist/sqlite-worker-meta.json +0 -84
  43. package/dist/sqlite-worker.mjs +0 -304
  44. package/dist/sqlite-worker.mjs.map +0 -7
@@ -1,28 +1,252 @@
1
1
  // src/entry/installer.ts
2
2
  import { execFile } from "node:child_process";
3
- import { dirname as dirname2, join as join5 } from "node:path";
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/platform/install/service-installer.ts
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 { access, chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
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.stateRoot === void 0 ? "" : `Environment=PIFLEET_STATE_ROOT=${systemdEscape(options.stateRoot)}
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=Pi Fleet user runtime
244
+ Description=pi-fleet user runtime
21
245
  After=default.target
22
246
 
23
247
  [Service]
24
248
  Type=simple
25
- ExecStart=${systemdEscape(options.nodePath)} ${systemdEscape(options.runtimePath)}
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 environment = options.stateRoot === void 0 ? "" : ` <key>EnvironmentVariables</key>
39
- <dict><key>PIFLEET_STATE_ROOT</key><string>${xmlEscape(options.stateRoot)}</string></dict>
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
- if (!isAbsolute(options.nodePath) || !isAbsolute(options.runtimePath)) {
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 systemdEscape(value) {
64
- if (!/^[A-Za-z0-9_./:@-]+$/.test(value)) throw new Error(`Unsafe systemd path ${value}`);
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
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 = join(home, ".config", "systemd", "user", "pi-fleet.service");
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 = join(home, "Library", "LaunchAgents", "works.elpapi.pifleet.plist");
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
- await atomicWrite(path, launchdAgentPlist(effectiveDefinition));
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 Pi Fleet supervision is unsupported on ${options.platform}`);
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 = join(home, ".config", "systemd", "user", "pi-fleet.service");
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 = join(home, "Library", "LaunchAgents", "works.elpapi.pifleet.plist");
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 Pi Fleet supervision is unsupported on ${options.platform}`);
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 access(definition.nodePath, constants.X_OK);
131
- await access(definition.runtimePath, constants.R_OK);
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("&apos;", "'").replaceAll("&quot;", '"').replaceAll("&gt;", ">").replaceAll("&lt;", "<").replaceAll("&amp;", "&");
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(dirname(path), { recursive: true, mode: 448 });
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 createHash2, randomUUID } from "node:crypto";
159
- import { chmod as chmod2, cp, lstat as lstat2, mkdir as mkdir2, readFile as readFile3, rename as rename2, rm as rm2, stat as stat2 } from "node:fs/promises";
160
- import { join as join3, resolve as resolve2, sep as sep2 } from "node:path";
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 join2, relative, resolve, sep } from "node:path";
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 = createHash("sha256");
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 = join2(directory, name);
475
+ const absolutePath = join3(directory, name);
192
476
  const linkInfo = await lstat(absolutePath);
193
- const info = linkInfo.isSymbolicLink() ? await stat(absolutePath) : linkInfo;
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 manifestBytes = await readFile3(join3(sourceRoot, "dist", "runtime-manifest.json"));
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 = join3(releasesRoot, `${manifest.package.version}-${manifestHash}`);
543
+ const destination = join4(releasesRoot, `${sourceManifest.package.version}-${closureHash}`);
224
544
  if (await pathExists(destination)) {
225
- await verifyRuntime(destination, manifest);
545
+ await verifyExpectedRuntime(destination, materializedManifest, materializedManifestBytes);
226
546
  return destination;
227
547
  }
228
- const staging = join3(releasesRoot, `.staging-${randomUUID()}`);
548
+ const staging = join4(releasesRoot, `.staging-${randomUUID()}`);
229
549
  await mkdir2(staging, { mode: 448 });
230
550
  try {
231
- await cp(join3(sourceRoot, "dist"), join3(staging, "dist"), {
551
+ await cp(join4(sourceRoot, "dist"), join4(staging, "dist"), {
232
552
  recursive: true,
233
553
  dereference: true
234
554
  });
235
- await cp(join3(sourceRoot, "bin"), join3(staging, "bin"), { recursive: true, dereference: true });
236
- await cp(join3(sourceRoot, "package.json"), join3(staging, "package.json"));
237
- for (const tree of manifest.trees) {
238
- const source = resolveInside(sourceRoot, tree.path);
239
- const destination2 = resolveInside(staging, tree.path);
240
- await mkdir2(resolve2(destination2, ".."), { recursive: true, mode: 448 });
241
- await cp(source, destination2, { recursive: true, dereference: true });
242
- }
243
- await verifyRuntime(staging, manifest);
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
- await rename2(staging, destination);
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
- } catch (error) {
582
+ } finally {
248
583
  await rm2(staging, { recursive: true, force: true });
249
- throw error;
250
584
  }
251
585
  }
252
- async function verifyRuntime(root, manifest) {
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
- for (const file of expected.files) {
258
- const path = resolveInside(resolvedRoot, file.path);
259
- const info = await stat2(path);
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 = createHash2("sha256").update(await readFile3(path)).digest("hex");
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
- for (const expectedTree of expected.trees) {
269
- const treeRoot = resolveInside(resolvedRoot, expectedTree.path);
270
- const actualTree = await hashDirectoryTree(treeRoot, expectedTree.path);
271
- if (actualTree.files !== expectedTree.files || actualTree.bytes !== expectedTree.bytes || actualTree.sha256 !== expectedTree.sha256) {
272
- throw new Error(`Runtime dependency tree ${expectedTree.path} failed verification`);
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 join4, resolve as resolve3 } from "node:path";
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" ? join4(homedir2(), "Library", "Application Support", "Pi Fleet", "runtime") : join4(env.XDG_DATA_HOME ?? join4(homedir2(), ".local", "share"), "pi-fleet"));
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(resolveFleetPaths().stateRoot);
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 = dirname2(dirname2(fileURLToPath(import.meta.url)));
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: join5(release, "bin", "pifleet-runtime.mjs"),
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() {