@danypops/pi-packed 0.28.4 → 0.28.6

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/README.md CHANGED
@@ -8,6 +8,12 @@ pi install npm:@danypops/pi-packed
8
8
 
9
9
  The extension connects to Packed's authenticated user daemon and starts the package-local `packed serve` process when no healthy daemon is available. It contains no SQLite, registry, package execution, or daemon implementation code.
10
10
 
11
+ ## Isolated daemon services
12
+
13
+ Packed materializes every managed daemon into a private, versioned npm project under its state directory (`services/<package>/<version>/`). Each root has its own `package.json`, lockfile, and `node_modules`; Pi's shared global project, root-level `overrides`, hoisting, and unrelated package updates therefore cannot rewrite a daemon's dependency closure. Downloads still use npm's shared cache.
14
+
15
+ A complete root is activated only after installation and manifest validation, using an atomic directory rename. Armada switches the persistent service to that root, and Packed retains the previous root for rollback while pruning older versions within a fixed bound. Removing the service removes its private roots. Pi extensions remain installed in Pi's own package project; only their daemon processes run from the isolated closure.
16
+
11
17
  ## Commands
12
18
 
13
19
  - `/packed` -- one floating overlay, four tabs (Packages/Find/Config/Settings), always shown in a persistent tab bar at the top: the focused tab reverses the theme's text color for high contrast in both dark and light modes, unfocused tabs show only a foreground color with their own first letter highlighted as a jump mnemonic (the focused tab needs no mnemonic -- there's nothing to jump to when you're already there). `Tab`/`Shift-Tab` (or `←`/`→`) sweep between them -- recognized under legacy CSI, xterm's modifyOtherKeys, and the Kitty keyboard protocol alike, not just one hardcoded encoding; from any tab other than Packages, `p`/`f`/`c`/`s` also jump straight to the matching one (never stolen while that tab's own filter/query box is capturing text). `Esc` returns to Packages from anywhere else, or closes the panel from Packages itself. Every approval and reload confirmation, on any tab, renders inline on this same panel (`y`/`n` keys), never a separate popup or native dialog. A standing test (`keymap.test.ts`, using Malevich's tree-style `findMnemonicConflicts`) verifies no two actions genuinely reachable at once share a key.
@@ -1,6 +1,7 @@
1
1
  import { type VehicleRegistrar, type VehicleRegistrationOutcome, type VehicleSpec } from "@danypops/armada";
2
2
  import { type ServiceInstallResult, type ServiceSpec } from "@danypops/vehicle-server/service";
3
3
  import type { InstalledPkg } from "../packages/package.ts";
4
+ import { type DaemonPackageMaterializer } from "./isolated-service-install.ts";
4
5
  export interface DaemonServiceManifest {
5
6
  /** Relative to the installed package's own root directory. */
6
7
  binPath: string;
@@ -234,6 +235,8 @@ export interface DaemonServiceInstaller {
234
235
  * whatever used to produce it.
235
236
  */
236
237
  unregisterVehicleByName(name: string): Promise<VehicleRegistrationOutcome>;
238
+ /** True when an executable lives in this installer's private managed-service roots. */
239
+ ownsExecutable?(path: string): boolean;
237
240
  }
238
241
  /**
239
242
  * Calls Armada's own VehicleRegistrar directly (in-process), rather than
@@ -245,9 +248,11 @@ export interface DaemonServiceInstaller {
245
248
  export declare class RealDaemonServiceInstaller implements DaemonServiceInstaller {
246
249
  private readonly registrar;
247
250
  private readonly selfRestart;
248
- constructor(registrar?: VehicleRegistrar, selfRestart?: SelfRestartScheduler);
251
+ private readonly materializer;
252
+ constructor(registrar?: VehicleRegistrar, selfRestart?: SelfRestartScheduler, materializer?: DaemonPackageMaterializer);
249
253
  listRegisteredVehicles(): Promise<readonly VehicleSpec[]>;
250
254
  unregisterVehicleByName(name: string): Promise<VehicleRegistrationOutcome>;
255
+ ownsExecutable(path: string): boolean;
251
256
  install(piHome: string, source: string): Promise<{
252
257
  ok: true;
253
258
  result: ServiceInstallResult;
@@ -1 +1 @@
1
- {"version":3,"file":"daemon-service.d.ts","sourceRoot":"","sources":["../../service/src/daemon/daemon-service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"daemon-service.d.ts","sourceRoot":"","sources":["../../service/src/daemon/daemon-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ export interface MaterializedDaemonPackage {
2
+ readonly piHome: string;
3
+ readonly source: string;
4
+ commit(): void;
5
+ rollback(): void;
6
+ }
7
+ export interface DaemonPackageMaterializer {
8
+ materialize(piHome: string, source: string): Promise<MaterializedDaemonPackage>;
9
+ remove(source: string): void;
10
+ ownsExecutable(path: string): boolean;
11
+ }
12
+ export declare class PassThroughDaemonPackageMaterializer implements DaemonPackageMaterializer {
13
+ materialize(piHome: string, source: string): Promise<MaterializedDaemonPackage>;
14
+ remove(_source: string): void;
15
+ ownsExecutable(_path: string): boolean;
16
+ }
17
+ /**
18
+ * Installs a daemon source into a private, versioned npm project. The project deliberately has
19
+ * no `overrides` field, so a host-level Pi/Bun override cannot rewrite the daemon's transitive
20
+ * dependency closure. npm's cache remains shared; only resolution metadata and node_modules are
21
+ * private. A completed directory is published with one atomic rename and retained alongside one
22
+ * prior version for rollback.
23
+ */
24
+ export declare class IsolatedDaemonPackageMaterializer implements DaemonPackageMaterializer {
25
+ private readonly stateDirectory;
26
+ private readonly npmBin;
27
+ constructor(stateDirectory: string, npmBin?: string);
28
+ materialize(piHome: string, source: string): Promise<MaterializedDaemonPackage>;
29
+ remove(source: string): void;
30
+ ownsExecutable(path: string): boolean;
31
+ private prune;
32
+ }
33
+ //# sourceMappingURL=isolated-service-install.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"isolated-service-install.d.ts","sourceRoot":"","sources":["../../service/src/daemon/isolated-service-install.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,yBAAyB;IACzC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,MAAM,IAAI,IAAI,CAAC;IACf,QAAQ,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,yBAAyB;IACzC,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,yBAAyB,CAAC,CAAC;IAChF,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;CACtC;AA4CD,qBAAa,oCAAqC,YAAW,yBAAyB;IAC/E,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,yBAAyB,CAAC;IAIrF,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAE7B,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;CAGtC;AAED;;;;;;GAMG;AACH,qBAAa,iCAAkC,YAAW,yBAAyB;IAEjF,OAAO,CAAC,QAAQ,CAAC,cAAc;IAC/B,OAAO,CAAC,QAAQ,CAAC,MAAM;gBADN,cAAc,EAAE,MAAM,EACtB,MAAM,SAAgE;IAGlF,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,yBAAyB,CAAC;IA+ErF,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAM5B,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAIrC,OAAO,CAAC,KAAK;CAiBb"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-packed",
3
- "version": "0.28.4",
3
+ "version": "0.28.6",
4
4
  "description": "Pi package lifecycle, validation, daemon, tools, profiles, and TUI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,9 +30,9 @@
30
30
  "@danypops/armada": "^0.5.0",
31
31
  "@danypops/packed": "^0.7.0",
32
32
  "@danypops/pi-extension-harness": "^0.7.0",
33
- "@danypops/vehicle-client": "^0.10.3",
34
- "@danypops/vehicle-core": "^0.17.1",
35
- "@danypops/vehicle-server": "^0.25.2",
33
+ "@danypops/vehicle-client": "^0.10.5",
34
+ "@danypops/vehicle-core": "^0.18.4",
35
+ "@danypops/vehicle-server": "^0.26.0",
36
36
  "jiti": "^2.7.0",
37
37
  "malevich-tui-components": "^0.32.1",
38
38
  "publint": "0.3.22",
@@ -40,13 +40,13 @@
40
40
  },
41
41
  "devDependencies": {
42
42
  "@danypops/pi-tui-harness": "^0.0.1",
43
- "@danypops/vehicle-client-pi": "^0.44.0",
43
+ "@danypops/vehicle-client-pi": "^0.45.0",
44
44
  "@danypops/vehicle-conformance": "^0.4.0",
45
45
  "@types/semver": "^7.7.1",
46
46
  "follow-redirects": "^1.16.0"
47
47
  },
48
48
  "peerDependencies": {
49
- "@danypops/vehicle-client-pi": "^0.44.0",
49
+ "@danypops/vehicle-client-pi": "^0.45.0",
50
50
  "@earendil-works/pi-coding-agent": "*",
51
51
  "@earendil-works/pi-tui": "*",
52
52
  "typebox": "*"
@@ -41,6 +41,10 @@ import {
41
41
  import { npmPackageName, readInstalledPackagesAcrossScopes, readPackageDeclarations } from "../packages/installed.ts";
42
42
  import type { InstalledPkg } from "../packages/package.ts";
43
43
  import { versionAtLeast } from "../publish/publish.ts";
44
+ import {
45
+ type DaemonPackageMaterializer,
46
+ PassThroughDaemonPackageMaterializer,
47
+ } from "./isolated-service-install.ts";
44
48
 
45
49
  export interface DaemonServiceManifest {
46
50
  /** Relative to the installed package's own root directory. */
@@ -612,7 +616,7 @@ export async function reconcileAllDaemonServices(
612
616
  async function pruneStaleVehicles(
613
617
  piHome: string,
614
618
  discovered: ReadonlySet<string>,
615
- installer: Pick<DaemonServiceInstaller, "listRegisteredVehicles" | "unregisterVehicleByName">,
619
+ installer: Pick<DaemonServiceInstaller, "listRegisteredVehicles" | "unregisterVehicleByName" | "ownsExecutable">,
616
620
  ): Promise<Pick<ReconcileAllResult, "pruned" | "pruneFailed">> {
617
621
  const managedRoot = join(piHome, "npm", "node_modules") + "/";
618
622
  const pruned: ReconcileAllResult["pruned"] = [];
@@ -621,7 +625,7 @@ async function pruneStaleVehicles(
621
625
  for (const vehicle of registered) {
622
626
  if (vehicle.name === PACKED_VEHICLE_NAME) continue;
623
627
  if (discovered.has(vehicle.name)) continue;
624
- if (!vehicle.executable.startsWith(managedRoot)) continue;
628
+ if (!vehicle.executable.startsWith(managedRoot) && !installer.ownsExecutable?.(vehicle.executable)) continue;
625
629
  const outcome = await installer.unregisterVehicleByName(vehicle.name);
626
630
  if (outcome.ok) pruned.push({ vehicleName: vehicle.name, executable: vehicle.executable });
627
631
  else pruneFailed.push({ vehicleName: vehicle.name, reason: outcome.diagnostics.map((d) => d.message).join("; ") || "unregister failed" });
@@ -651,6 +655,8 @@ export interface DaemonServiceInstaller {
651
655
  * whatever used to produce it.
652
656
  */
653
657
  unregisterVehicleByName(name: string): Promise<VehicleRegistrationOutcome>;
658
+ /** True when an executable lives in this installer's private managed-service roots. */
659
+ ownsExecutable?(path: string): boolean;
654
660
  }
655
661
 
656
662
  /**
@@ -664,6 +670,7 @@ export class RealDaemonServiceInstaller implements DaemonServiceInstaller {
664
670
  constructor(
665
671
  private readonly registrar: VehicleRegistrar = createVehicleRegistrar(),
666
672
  private readonly selfRestart: SelfRestartScheduler = scheduleSelfRestart,
673
+ private readonly materializer: DaemonPackageMaterializer = new PassThroughDaemonPackageMaterializer(),
667
674
  ) {}
668
675
 
669
676
  async listRegisteredVehicles(): Promise<readonly VehicleSpec[]> {
@@ -674,16 +681,33 @@ export class RealDaemonServiceInstaller implements DaemonServiceInstaller {
674
681
  return this.registrar.unregister(name);
675
682
  }
676
683
 
684
+ ownsExecutable(path: string): boolean {
685
+ return this.materializer.ownsExecutable(path);
686
+ }
687
+
677
688
  async install(
678
689
  piHome: string,
679
690
  source: string,
680
691
  ): Promise<{ ok: true; result: ServiceInstallResult; spec: ServiceSpec } | { ok: false; reason: string; notADaemon?: boolean }> {
681
- const resolved = resolveDaemonServiceSpec(piHome, source);
682
- if (!resolved.ok) return resolved;
683
- if (resolved.spec.name === PACKED_VEHICLE_NAME) {
684
- return { ok: true, result: { installed: false, reason: SELF_REGISTRATION_REASON }, spec: resolved.spec };
692
+ const discovered = resolveDaemonServiceSpec(piHome, source);
693
+ if (!discovered.ok) return discovered;
694
+ if (discovered.spec.name === PACKED_VEHICLE_NAME) {
695
+ return { ok: true, result: { installed: false, reason: SELF_REGISTRATION_REASON }, spec: discovered.spec };
696
+ }
697
+ let materialized;
698
+ try {
699
+ materialized = await this.materializer.materialize(piHome, source);
700
+ } catch (error) {
701
+ return { ok: false, reason: error instanceof Error ? error.message : String(error) };
702
+ }
703
+ const resolved = resolveDaemonServiceSpec(materialized.piHome, materialized.source);
704
+ if (!resolved.ok) {
705
+ materialized.rollback();
706
+ return resolved;
685
707
  }
686
708
  const result = await registerVehicleService(resolved.spec, this.registrar);
709
+ if (result.installed) materialized.commit();
710
+ else materialized.rollback();
687
711
  return { ok: true, result, spec: resolved.spec };
688
712
  }
689
713
 
@@ -697,6 +721,7 @@ export class RealDaemonServiceInstaller implements DaemonServiceInstaller {
697
721
  return { ok: true, result: { installed: false, reason: SELF_REGISTRATION_REASON }, spec: resolved.spec };
698
722
  }
699
723
  const result = await unregisterVehicleService(resolved.spec.name, this.registrar);
724
+ if (result.installed) this.materializer.remove(source);
700
725
  return { ok: true, result, spec: resolved.spec };
701
726
  }
702
727
 
@@ -713,12 +738,25 @@ export class RealDaemonServiceInstaller implements DaemonServiceInstaller {
713
738
  if (!(await isVehicleServiceRegistered(resolved.spec.name, this.registrar))) {
714
739
  return { ok: true, restarted: false, reason: `no persistent service is registered for ${resolved.spec.name}`, spec: resolved.spec };
715
740
  }
716
- const result = await registerVehicleService(resolved.spec, this.registrar);
741
+ let materialized;
742
+ try {
743
+ materialized = await this.materializer.materialize(piHome, source);
744
+ } catch (error) {
745
+ return { ok: false, reason: error instanceof Error ? error.message : String(error) };
746
+ }
747
+ const isolated = resolveDaemonServiceSpec(materialized.piHome, materialized.source);
748
+ if (!isolated.ok) {
749
+ materialized.rollback();
750
+ return isolated;
751
+ }
752
+ const result = await registerVehicleService(isolated.spec, this.registrar);
753
+ if (result.installed) materialized.commit();
754
+ else materialized.rollback();
717
755
  return {
718
756
  ok: true,
719
757
  restarted: result.installed,
720
758
  ...(result.installed ? {} : { reason: result.reason }),
721
- spec: resolved.spec,
759
+ spec: isolated.spec,
722
760
  };
723
761
  }
724
762
  }
@@ -36,6 +36,7 @@ import { createLogger } from "../shared/log.ts";
36
36
  import { legacyPackedStateDirectory, migrateLegacyPackedState, type PackedPaths, resolvePackedPaths } from "../shared/paths.ts";
37
37
  import { envMs } from "../shared/state.ts";
38
38
  import { createApp } from "./service.ts";
39
+ import { IsolatedDaemonPackageMaterializer } from "./isolated-service-install.ts";
39
40
  import { checkUpdates, saveUpdates } from "./watcher.ts";
40
41
 
41
42
  const logger = createLogger("daemon");
@@ -89,7 +90,9 @@ export function daemonOptions(options: StartPackedDaemonOptions): StartDaemonOpt
89
90
  // already uses via checkUpdates() -- one mirror, two consumers, never two
90
91
  // notions of "the real latest version".
91
92
  const inst = options.inst ?? new ExecInstaller(undefined, piHome, undefined, undefined, undefined, (name) => latestVersion(database, name));
92
- const daemonServiceInstaller = options.daemonServiceInstaller ?? new RealDaemonServiceInstaller();
93
+ const daemonServiceInstaller =
94
+ options.daemonServiceInstaller ??
95
+ new RealDaemonServiceInstaller(undefined, undefined, new IsolatedDaemonPackageMaterializer(paths.stateDirectory));
93
96
  const configuredMaintenanceTasks = options.maintenanceTasks ?? [
94
97
  {
95
98
  name: "package-update-check",
@@ -0,0 +1,196 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { join, sep } from "node:path";
3
+ import { npmPackageName } from "../packages/installed.ts";
4
+
5
+ const MAX_VERSION_DIRECTORIES = 20;
6
+ const RETAINED_VERSION_DIRECTORIES = 2;
7
+ const MAX_INSTALL_OUTPUT_BYTES = 64 * 1024;
8
+ const VERSION_DIRECTORY_RE = /^[0-9A-Za-z][0-9A-Za-z.+_-]{0,127}$/;
9
+
10
+ export interface MaterializedDaemonPackage {
11
+ readonly piHome: string;
12
+ readonly source: string;
13
+ commit(): void;
14
+ rollback(): void;
15
+ }
16
+
17
+ export interface DaemonPackageMaterializer {
18
+ materialize(piHome: string, source: string): Promise<MaterializedDaemonPackage>;
19
+ remove(source: string): void;
20
+ ownsExecutable(path: string): boolean;
21
+ }
22
+
23
+ function readVersion(packageJsonPath: string): string | undefined {
24
+ try {
25
+ const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8")) as { version?: unknown };
26
+ return typeof parsed.version === "string" && parsed.version.length > 0 ? parsed.version : undefined;
27
+ } catch {
28
+ return undefined;
29
+ }
30
+ }
31
+
32
+ async function readBounded(stream: ReadableStream<Uint8Array>): Promise<string> {
33
+ const reader = stream.getReader();
34
+ const decoder = new TextDecoder();
35
+ let output = "";
36
+ let retainedBytes = 0;
37
+ for (;;) {
38
+ const { done, value } = await reader.read();
39
+ if (done) break;
40
+ if (retainedBytes >= MAX_INSTALL_OUTPUT_BYTES) continue;
41
+ const remaining = MAX_INSTALL_OUTPUT_BYTES - retainedBytes;
42
+ const retained = value.byteLength <= remaining ? value : value.slice(0, remaining);
43
+ output += decoder.decode(retained, { stream: true });
44
+ retainedBytes += retained.byteLength;
45
+ }
46
+ return output + decoder.decode();
47
+ }
48
+
49
+ function packageDirectory(piHome: string, packageName: string): string {
50
+ return join(piHome, "npm", "node_modules", packageName);
51
+ }
52
+
53
+ function installKey(packageName: string): string {
54
+ return encodeURIComponent(packageName).replaceAll("%", "_");
55
+ }
56
+
57
+ function isolatedPiHome(versionDirectory: string): string {
58
+ return join(versionDirectory, "pi-home");
59
+ }
60
+
61
+ function resolvedPackageJson(piHome: string, packageName: string): string {
62
+ return join(packageDirectory(piHome, packageName), "package.json");
63
+ }
64
+
65
+ export class PassThroughDaemonPackageMaterializer implements DaemonPackageMaterializer {
66
+ async materialize(piHome: string, source: string): Promise<MaterializedDaemonPackage> {
67
+ return { piHome, source, commit() {}, rollback() {} };
68
+ }
69
+
70
+ remove(_source: string): void {}
71
+
72
+ ownsExecutable(_path: string): boolean {
73
+ return false;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Installs a daemon source into a private, versioned npm project. The project deliberately has
79
+ * no `overrides` field, so a host-level Pi/Bun override cannot rewrite the daemon's transitive
80
+ * dependency closure. npm's cache remains shared; only resolution metadata and node_modules are
81
+ * private. A completed directory is published with one atomic rename and retained alongside one
82
+ * prior version for rollback.
83
+ */
84
+ export class IsolatedDaemonPackageMaterializer implements DaemonPackageMaterializer {
85
+ constructor(
86
+ private readonly stateDirectory: string,
87
+ private readonly npmBin = process.env.PI_PACKED_NPM_BIN ?? process.env.NPM_BIN ?? "npm",
88
+ ) {}
89
+
90
+ async materialize(piHome: string, source: string): Promise<MaterializedDaemonPackage> {
91
+ const packageName = npmPackageName(source);
92
+ if (!packageName) throw new Error(`isolated daemon installation only supports npm sources: ${source}`);
93
+ const version = readVersion(resolvedPackageJson(piHome, packageName));
94
+ if (!version) throw new Error(`cannot isolate ${packageName}: installed package version is unreadable`);
95
+ if (!VERSION_DIRECTORY_RE.test(version)) throw new Error(`cannot isolate ${packageName}: installed package version is unsafe`);
96
+
97
+ const packageRoot = join(this.stateDirectory, "services", installKey(packageName));
98
+ const finalDirectory = join(packageRoot, version);
99
+ const finalPiHome = isolatedPiHome(finalDirectory);
100
+ const finalPackageJson = resolvedPackageJson(finalPiHome, packageName);
101
+ if (readVersion(finalPackageJson) === version) {
102
+ return {
103
+ piHome: finalPiHome,
104
+ source: `npm:${packageName}@${version}`,
105
+ commit: () => this.prune(packageRoot, finalDirectory),
106
+ rollback() {},
107
+ };
108
+ }
109
+
110
+ mkdirSync(packageRoot, { recursive: true, mode: 0o700 });
111
+ if (existsSync(finalDirectory)) rmSync(finalDirectory, { recursive: true, force: true });
112
+ const temporaryDirectory = join(packageRoot, `.${version}.${process.pid}.${crypto.randomUUID()}.tmp`);
113
+ const temporaryPiHome = isolatedPiHome(temporaryDirectory);
114
+ const projectDirectory = join(temporaryPiHome, "npm");
115
+ mkdirSync(projectDirectory, { recursive: true, mode: 0o700 });
116
+ writeFileSync(
117
+ join(projectDirectory, "package.json"),
118
+ JSON.stringify({ name: `packed-service-${installKey(packageName)}`, private: true, dependencies: { [packageName]: version } }, null, 2),
119
+ );
120
+
121
+ let publishedByThisCall = false;
122
+ try {
123
+ const proc = Bun.spawn(
124
+ [
125
+ this.npmBin,
126
+ "install",
127
+ "--ignore-scripts",
128
+ "--no-audit",
129
+ "--no-fund",
130
+ "--install-strategy=nested",
131
+ "--loglevel=error",
132
+ ],
133
+ { cwd: projectDirectory, stdout: "pipe", stderr: "pipe", env: process.env },
134
+ );
135
+ const [stdout, stderr, code] = await Promise.all([readBounded(proc.stdout), readBounded(proc.stderr), proc.exited]);
136
+ if (code !== 0) {
137
+ const output = [stdout.trim(), stderr.trim()].filter(Boolean).join("\n");
138
+ throw new Error(`isolated npm install failed for ${packageName}@${version} (exit ${code}): ${output || "no output"}`);
139
+ }
140
+ if (readVersion(resolvedPackageJson(temporaryPiHome, packageName)) !== version) {
141
+ throw new Error(`isolated npm install did not resolve ${packageName}@${version}`);
142
+ }
143
+ try {
144
+ renameSync(temporaryDirectory, finalDirectory);
145
+ publishedByThisCall = true;
146
+ } catch (error) {
147
+ if (readVersion(finalPackageJson) !== version) throw error;
148
+ rmSync(temporaryDirectory, { recursive: true, force: true });
149
+ }
150
+ } catch (error) {
151
+ rmSync(temporaryDirectory, { recursive: true, force: true });
152
+ throw error;
153
+ }
154
+
155
+ let committed = false;
156
+ return {
157
+ piHome: finalPiHome,
158
+ source: `npm:${packageName}@${version}`,
159
+ commit: () => {
160
+ committed = true;
161
+ this.prune(packageRoot, finalDirectory);
162
+ },
163
+ rollback: () => {
164
+ if (!committed && publishedByThisCall) rmSync(finalDirectory, { recursive: true, force: true });
165
+ },
166
+ };
167
+ }
168
+
169
+ remove(source: string): void {
170
+ const packageName = npmPackageName(source);
171
+ if (!packageName) return;
172
+ rmSync(join(this.stateDirectory, "services", installKey(packageName)), { recursive: true, force: true });
173
+ }
174
+
175
+ ownsExecutable(path: string): boolean {
176
+ return path.startsWith(join(this.stateDirectory, "services") + sep);
177
+ }
178
+
179
+ private prune(packageRoot: string, activeDirectory: string): void {
180
+ let directories: string[];
181
+ try {
182
+ directories = readdirSync(packageRoot)
183
+ .filter((name) => !name.startsWith("."))
184
+ .slice(0, MAX_VERSION_DIRECTORIES)
185
+ .map((name) => join(packageRoot, name))
186
+ .filter((path) => statSync(path).isDirectory());
187
+ } catch {
188
+ return;
189
+ }
190
+ directories.sort((left, right) => statSync(right).mtimeMs - statSync(left).mtimeMs);
191
+ const retained = new Set([activeDirectory, ...directories.slice(0, RETAINED_VERSION_DIRECTORIES)]);
192
+ for (const directory of directories) {
193
+ if (!retained.has(directory)) rmSync(directory, { recursive: true, force: true });
194
+ }
195
+ }
196
+ }
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import type { VehicleRegistrar } from "@danypops/armada";
6
6
  import { createArmadaTestHarness } from "@danypops/armada/testing";
7
+ import type { DaemonPackageMaterializer } from "../src/daemon/isolated-service-install.ts";
7
8
  import {
8
9
  classifyUpdateSource,
9
10
  detectVehicleDaemonService,
@@ -490,6 +491,38 @@ describe("RealDaemonServiceInstaller -- Armada registration through the in-proce
490
491
  }
491
492
  });
492
493
 
494
+ it("install() registers the isolated executable and commits that root only after Armada accepts it", async () => {
495
+ const piHome = fakePiHome();
496
+ const isolatedPiHome = fakePiHome();
497
+ writePackage(piHome, "@danypops/lector", { binPath: "src/cli.ts", args: ["serve"] });
498
+ writePackage(isolatedPiHome, "@danypops/lector", { binPath: "src/cli.ts", args: ["serve"] });
499
+ let committed = 0;
500
+ let rolledBack = 0;
501
+ const materializer: DaemonPackageMaterializer = {
502
+ materialize: async () => ({
503
+ piHome: isolatedPiHome,
504
+ source: "npm:@danypops/lector@1.0.0",
505
+ commit: () => committed++,
506
+ rollback: () => rolledBack++,
507
+ }),
508
+ remove() {},
509
+ ownsExecutable: (path) => path.startsWith(isolatedPiHome),
510
+ };
511
+ const harness = await createArmadaTestHarness();
512
+ try {
513
+ const installer = new RealDaemonServiceInstaller(harness.registrar, undefined, materializer);
514
+ const outcome = await installer.install(piHome, "npm:@danypops/lector");
515
+
516
+ expect(outcome.ok).toBe(true);
517
+ if (!outcome.ok) throw new Error("expected ok");
518
+ expect(outcome.spec.binPath).toBe(join(isolatedPiHome, "npm", "node_modules", "@danypops/lector", "src/cli.ts"));
519
+ expect(committed).toBe(1);
520
+ expect(rolledBack).toBe(0);
521
+ } finally {
522
+ await harness.dispose();
523
+ }
524
+ });
525
+
493
526
  it("install() never asks Armada to replace Packed from inside Packed's own process", async () => {
494
527
  const piHome = fakePiHome();
495
528
  writePackage(piHome, "@danypops/pi-packed", { binPath: "service/src/cli/cli.ts", args: ["serve"] });
@@ -0,0 +1,148 @@
1
+ import { afterEach, describe, expect, it } from "bun:test";
2
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { IsolatedDaemonPackageMaterializer } from "../src/daemon/isolated-service-install.ts";
6
+
7
+ const roots: string[] = [];
8
+ afterEach(() => {
9
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
10
+ });
11
+
12
+ function temporaryRoot(): string {
13
+ const root = mkdtempSync(join(tmpdir(), "packed-isolated-service-"));
14
+ roots.push(root);
15
+ return root;
16
+ }
17
+
18
+ function writeInstalledPackage(piHome: string, name: string, version: string): void {
19
+ const directory = join(piHome, "npm", "node_modules", name);
20
+ mkdirSync(directory, { recursive: true });
21
+ writeFileSync(join(directory, "package.json"), JSON.stringify({ name, version }));
22
+ }
23
+
24
+ function writeFakeNpm(root: string, vehicleVersions: string | Record<string, string> = "0.10.3"): string {
25
+ const path = join(root, `fake-npm-${crypto.randomUUID()}`);
26
+ writeFileSync(
27
+ path,
28
+ `#!/usr/bin/env bun
29
+ const fs = require("node:fs");
30
+ const path = require("node:path");
31
+ const project = JSON.parse(fs.readFileSync(path.join(process.cwd(), "package.json"), "utf8"));
32
+ if (project.overrides) throw new Error("host overrides leaked into isolated project");
33
+ const [name, version] = Object.entries(project.dependencies)[0];
34
+ const vehicleVersions = ${JSON.stringify(vehicleVersions)};
35
+ const vehicleVersion = typeof vehicleVersions === "string" ? vehicleVersions : vehicleVersions[name];
36
+ if (!vehicleVersion) throw new Error("missing fake Vehicle version for " + name);
37
+ const packageDir = path.join(process.cwd(), "node_modules", name);
38
+ fs.mkdirSync(packageDir, { recursive: true });
39
+ fs.writeFileSync(path.join(packageDir, "package.json"), JSON.stringify({ name, version, packed: { daemonService: { binPath: "src/cli.ts", args: ["serve"] } } }));
40
+ const vehicleDir = path.join(packageDir, "node_modules", "@danypops", "vehicle-client");
41
+ fs.mkdirSync(vehicleDir, { recursive: true });
42
+ fs.writeFileSync(path.join(vehicleDir, "package.json"), JSON.stringify({ name: "@danypops/vehicle-client", version: vehicleVersion }));
43
+ `,
44
+ );
45
+ chmodSync(path, 0o755);
46
+ return path;
47
+ }
48
+
49
+ describe("IsolatedDaemonPackageMaterializer", () => {
50
+ it("materializes a private dependency closure that cannot inherit the host project's incompatible override", async () => {
51
+ const root = temporaryRoot();
52
+ const piHome = join(root, "pi");
53
+ const stateDirectory = join(root, "state");
54
+ mkdirSync(join(piHome, "npm"), { recursive: true });
55
+ writeFileSync(
56
+ join(piHome, "npm", "package.json"),
57
+ JSON.stringify({
58
+ dependencies: { "@danypops/lector": "0.19.21" },
59
+ overrides: { "@danypops/vehicle-client": "0.6.2" },
60
+ }),
61
+ );
62
+ writeInstalledPackage(piHome, "@danypops/lector", "0.19.21");
63
+ writeInstalledPackage(piHome, "@danypops/vehicle-client", "0.6.2");
64
+ const materializer = new IsolatedDaemonPackageMaterializer(stateDirectory, writeFakeNpm(root));
65
+
66
+ const result = await materializer.materialize(piHome, "npm:@danypops/lector");
67
+ const isolatedLector = join(result.piHome, "npm", "node_modules", "@danypops", "lector");
68
+ const isolatedVehicle = join(isolatedLector, "node_modules", "@danypops", "vehicle-client", "package.json");
69
+
70
+ expect(JSON.parse(readFileSync(isolatedVehicle, "utf8")).version).toBe("0.10.3");
71
+ expect(JSON.parse(readFileSync(join(piHome, "npm", "node_modules", "@danypops", "vehicle-client", "package.json"), "utf8")).version).toBe("0.6.2");
72
+ expect(result.source).toBe("npm:@danypops/lector@0.19.21");
73
+ expect(existsSync(join(isolatedLector, "package.json"))).toBe(true);
74
+ expect(materializer.ownsExecutable(join(isolatedLector, "src", "cli.ts"))).toBe(true);
75
+ expect(materializer.ownsExecutable(join(piHome, "npm", "node_modules", "@danypops", "lector", "src", "cli.ts"))).toBe(false);
76
+ result.commit();
77
+ });
78
+
79
+ it("keeps two managed daemons with incompatible shared dependency requirements in separate roots", async () => {
80
+ const root = temporaryRoot();
81
+ const piHome = join(root, "pi");
82
+ const stateDirectory = join(root, "state");
83
+ writeInstalledPackage(piHome, "daemon-old", "1.0.0");
84
+ writeInstalledPackage(piHome, "daemon-new", "2.0.0");
85
+ const materializer = new IsolatedDaemonPackageMaterializer(
86
+ stateDirectory,
87
+ writeFakeNpm(root, { "daemon-old": "0.6.2", "daemon-new": "0.10.5" }),
88
+ );
89
+
90
+ const oldDaemon = await materializer.materialize(piHome, "npm:daemon-old");
91
+ const newDaemon = await materializer.materialize(piHome, "npm:daemon-new");
92
+ const versionAt = (materialized: { piHome: string }, name: string) =>
93
+ JSON.parse(
94
+ readFileSync(join(materialized.piHome, "npm", "node_modules", name, "node_modules", "@danypops", "vehicle-client", "package.json"), "utf8"),
95
+ ).version;
96
+
97
+ expect(versionAt(oldDaemon, "daemon-old")).toBe("0.6.2");
98
+ expect(versionAt(newDaemon, "daemon-new")).toBe("0.10.5");
99
+ expect(oldDaemon.piHome).not.toBe(newDaemon.piHome);
100
+ oldDaemon.commit();
101
+ newDaemon.commit();
102
+ });
103
+
104
+ it("publishes complete roots atomically and removes an uncommitted candidate on rollback", async () => {
105
+ const root = temporaryRoot();
106
+ const piHome = join(root, "pi");
107
+ const stateDirectory = join(root, "state");
108
+ writeInstalledPackage(piHome, "daemon", "1.0.0");
109
+ const materializer = new IsolatedDaemonPackageMaterializer(stateDirectory, writeFakeNpm(root));
110
+
111
+ const result = await materializer.materialize(piHome, "npm:daemon");
112
+ const versionDirectory = join(result.piHome, "..");
113
+ expect(existsSync(join(result.piHome, "npm", "node_modules", "daemon", "package.json"))).toBe(true);
114
+ expect(readdirSync(join(stateDirectory, "services", "daemon")).some((name) => name.startsWith("."))).toBe(false);
115
+
116
+ result.rollback();
117
+ expect(existsSync(versionDirectory)).toBe(false);
118
+ });
119
+
120
+ it("rejects an unsafe installed version before creating a service directory", async () => {
121
+ const root = temporaryRoot();
122
+ const piHome = join(root, "pi");
123
+ const stateDirectory = join(root, "state");
124
+ writeInstalledPackage(piHome, "daemon", "../../escape");
125
+ const materializer = new IsolatedDaemonPackageMaterializer(stateDirectory, writeFakeNpm(root));
126
+
127
+ await expect(materializer.materialize(piHome, "npm:daemon")).rejects.toThrow(/version is unsafe/);
128
+ expect(existsSync(join(stateDirectory, "services"))).toBe(false);
129
+ });
130
+
131
+ it("reuses a committed version without invoking npm again and removes all roots on uninstall", async () => {
132
+ const root = temporaryRoot();
133
+ const piHome = join(root, "pi");
134
+ const stateDirectory = join(root, "state");
135
+ writeInstalledPackage(piHome, "daemon", "1.0.0");
136
+ const npmBin = writeFakeNpm(root);
137
+ const materializer = new IsolatedDaemonPackageMaterializer(stateDirectory, npmBin);
138
+ const first = await materializer.materialize(piHome, "npm:daemon");
139
+ first.commit();
140
+ rmSync(npmBin);
141
+
142
+ const second = await materializer.materialize(piHome, "npm:daemon");
143
+ expect(second.piHome).toBe(first.piHome);
144
+ second.commit();
145
+ materializer.remove("npm:daemon");
146
+ expect(existsSync(join(stateDirectory, "services", "daemon"))).toBe(false);
147
+ });
148
+ });