@danypops/pi-packed 0.28.7 → 0.28.9

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
@@ -10,7 +10,7 @@ The extension connects to Packed's authenticated user daemon and starts the pack
10
10
 
11
11
  ## Isolated daemon services
12
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.
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. npm may hoist dependencies inside that one private root for ordinary package compatibility, but never across service roots. Downloads still use npm's shared cache.
14
14
 
15
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
16
 
@@ -28,6 +28,7 @@ export declare class IsolatedDaemonPackageMaterializer implements DaemonPackageM
28
28
  materialize(piHome: string, source: string): Promise<MaterializedDaemonPackage>;
29
29
  remove(source: string): void;
30
30
  ownsExecutable(path: string): boolean;
31
+ private pruneTemporaryDirectories;
31
32
  private prune;
32
33
  }
33
34
  //# sourceMappingURL=isolated-service-install.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"isolated-service-install.d.ts","sourceRoot":"","sources":["../../service/src/daemon/isolated-service-install.ts"],"names":[],"mappings":"AAYA,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;AAuDD,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;IAwFrF,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAM5B,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAIrC,OAAO,CAAC,yBAAyB;IAoBjC,OAAO,CAAC,KAAK;CAkBb"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-packed",
3
- "version": "0.28.7",
3
+ "version": "0.28.9",
4
4
  "description": "Pi package lifecycle, validation, daemon, tools, profiles, and TUI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2,8 +2,11 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, s
2
2
  import { join, sep } from "node:path";
3
3
  import { npmPackageName } from "../packages/installed.ts";
4
4
 
5
+ const INSTALL_FORMAT_VERSION = 1;
5
6
  const MAX_VERSION_DIRECTORIES = 20;
7
+ const MAX_TEMP_DIRECTORY_SCAN = 100;
6
8
  const RETAINED_VERSION_DIRECTORIES = 2;
9
+ const STALE_TEMP_DIRECTORY_MS = 5 * 60 * 1_000;
7
10
  const MAX_INSTALL_OUTPUT_BYTES = 64 * 1024;
8
11
  const VERSION_DIRECTORY_RE = /^[0-9A-Za-z][0-9A-Za-z.+_-]{0,127}$/;
9
12
 
@@ -62,6 +65,17 @@ function resolvedPackageJson(piHome: string, packageName: string): string {
62
65
  return join(packageDirectory(piHome, packageName), "package.json");
63
66
  }
64
67
 
68
+ function hasCurrentInstallFormat(piHome: string): boolean {
69
+ try {
70
+ const project = JSON.parse(readFileSync(join(piHome, "npm", "package.json"), "utf8")) as {
71
+ packedServiceInstallFormat?: unknown;
72
+ };
73
+ return project.packedServiceInstallFormat === INSTALL_FORMAT_VERSION;
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+
65
79
  export class PassThroughDaemonPackageMaterializer implements DaemonPackageMaterializer {
66
80
  async materialize(piHome: string, source: string): Promise<MaterializedDaemonPackage> {
67
81
  return { piHome, source, commit() {}, rollback() {} };
@@ -98,7 +112,9 @@ export class IsolatedDaemonPackageMaterializer implements DaemonPackageMateriali
98
112
  const finalDirectory = join(packageRoot, version);
99
113
  const finalPiHome = isolatedPiHome(finalDirectory);
100
114
  const finalPackageJson = resolvedPackageJson(finalPiHome, packageName);
101
- if (readVersion(finalPackageJson) === version) {
115
+ mkdirSync(packageRoot, { recursive: true, mode: 0o700 });
116
+ this.pruneTemporaryDirectories(packageRoot);
117
+ if (readVersion(finalPackageJson) === version && hasCurrentInstallFormat(finalPiHome)) {
102
118
  return {
103
119
  piHome: finalPiHome,
104
120
  source: `npm:${packageName}@${version}`,
@@ -107,7 +123,6 @@ export class IsolatedDaemonPackageMaterializer implements DaemonPackageMateriali
107
123
  };
108
124
  }
109
125
 
110
- mkdirSync(packageRoot, { recursive: true, mode: 0o700 });
111
126
  if (existsSync(finalDirectory)) rmSync(finalDirectory, { recursive: true, force: true });
112
127
  const temporaryDirectory = join(packageRoot, `.${version}.${process.pid}.${crypto.randomUUID()}.tmp`);
113
128
  const temporaryPiHome = isolatedPiHome(temporaryDirectory);
@@ -115,7 +130,16 @@ export class IsolatedDaemonPackageMaterializer implements DaemonPackageMateriali
115
130
  mkdirSync(projectDirectory, { recursive: true, mode: 0o700 });
116
131
  writeFileSync(
117
132
  join(projectDirectory, "package.json"),
118
- JSON.stringify({ name: `packed-service-${installKey(packageName)}`, private: true, dependencies: { [packageName]: version } }, null, 2),
133
+ JSON.stringify(
134
+ {
135
+ name: `packed-service-${installKey(packageName)}`,
136
+ private: true,
137
+ packedServiceInstallFormat: INSTALL_FORMAT_VERSION,
138
+ dependencies: { [packageName]: version },
139
+ },
140
+ null,
141
+ 2,
142
+ ),
119
143
  );
120
144
 
121
145
  let publishedByThisCall = false;
@@ -127,7 +151,6 @@ export class IsolatedDaemonPackageMaterializer implements DaemonPackageMateriali
127
151
  "--ignore-scripts",
128
152
  "--no-audit",
129
153
  "--no-fund",
130
- "--install-strategy=nested",
131
154
  "--loglevel=error",
132
155
  ],
133
156
  { cwd: projectDirectory, stdout: "pipe", stderr: "pipe", env: process.env },
@@ -176,6 +199,26 @@ export class IsolatedDaemonPackageMaterializer implements DaemonPackageMateriali
176
199
  return path.startsWith(join(this.stateDirectory, "services") + sep);
177
200
  }
178
201
 
202
+ private pruneTemporaryDirectories(packageRoot: string): void {
203
+ let names: string[];
204
+ try {
205
+ names = readdirSync(packageRoot)
206
+ .filter((name) => name.startsWith(".") && name.endsWith(".tmp"))
207
+ .slice(0, MAX_TEMP_DIRECTORY_SCAN);
208
+ } catch {
209
+ return;
210
+ }
211
+ const staleBefore = Date.now() - STALE_TEMP_DIRECTORY_MS;
212
+ for (const name of names) {
213
+ const path = join(packageRoot, name);
214
+ try {
215
+ if (statSync(path).mtimeMs < staleBefore) rmSync(path, { recursive: true, force: true });
216
+ } catch {
217
+ // A concurrent materializer may have renamed or removed the candidate.
218
+ }
219
+ }
220
+ }
221
+
179
222
  private prune(packageRoot: string, activeDirectory: string): void {
180
223
  let directories: string[];
181
224
  try {
@@ -188,7 +231,8 @@ export class IsolatedDaemonPackageMaterializer implements DaemonPackageMateriali
188
231
  return;
189
232
  }
190
233
  directories.sort((left, right) => statSync(right).mtimeMs - statSync(left).mtimeMs);
191
- const retained = new Set([activeDirectory, ...directories.slice(0, RETAINED_VERSION_DIRECTORIES)]);
234
+ const prior = directories.filter((directory) => directory !== activeDirectory).slice(0, RETAINED_VERSION_DIRECTORIES - 1);
235
+ const retained = new Set([activeDirectory, ...prior]);
192
236
  for (const directory of directories) {
193
237
  if (!retained.has(directory)) rmSync(directory, { recursive: true, force: true });
194
238
  }
@@ -1,5 +1,5 @@
1
1
  import { afterEach, describe, expect, it } from "bun:test";
2
- import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, utimesSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { IsolatedDaemonPackageMaterializer } from "../src/daemon/isolated-service-install.ts";
@@ -30,6 +30,7 @@ const fs = require("node:fs");
30
30
  const path = require("node:path");
31
31
  const project = JSON.parse(fs.readFileSync(path.join(process.cwd(), "package.json"), "utf8"));
32
32
  if (project.overrides) throw new Error("host overrides leaked into isolated project");
33
+ if (project.packedServiceInstallFormat !== 1) throw new Error("missing isolated install format marker");
33
34
  const [name, version] = Object.entries(project.dependencies)[0];
34
35
  const vehicleVersions = ${JSON.stringify(vehicleVersions)};
35
36
  const vehicleVersion = typeof vehicleVersions === "string" ? vehicleVersions : vehicleVersions[name];
@@ -117,6 +118,44 @@ describe("IsolatedDaemonPackageMaterializer", () => {
117
118
  expect(existsSync(versionDirectory)).toBe(false);
118
119
  });
119
120
 
121
+ it("retains only the active and immediately previous committed versions", async () => {
122
+ const root = temporaryRoot();
123
+ const piHome = join(root, "pi");
124
+ const stateDirectory = join(root, "state");
125
+ const materializer = new IsolatedDaemonPackageMaterializer(stateDirectory, writeFakeNpm(root));
126
+ for (const version of ["1.0.0", "1.1.0", "1.2.0"]) {
127
+ writeInstalledPackage(piHome, "daemon", version);
128
+ const installed = await materializer.materialize(piHome, "npm:daemon");
129
+ installed.commit();
130
+ }
131
+
132
+ expect(readdirSync(join(stateDirectory, "services", "daemon")).filter((name) => !name.startsWith(".")).sort()).toEqual([
133
+ "1.1.0",
134
+ "1.2.0",
135
+ ]);
136
+ });
137
+
138
+ it("cleans bounded stale crash candidates without touching a fresh concurrent candidate", async () => {
139
+ const root = temporaryRoot();
140
+ const piHome = join(root, "pi");
141
+ const stateDirectory = join(root, "state");
142
+ const packageRoot = join(stateDirectory, "services", "daemon");
143
+ const stale = join(packageRoot, ".stale.tmp");
144
+ const fresh = join(packageRoot, ".fresh.tmp");
145
+ mkdirSync(stale, { recursive: true });
146
+ mkdirSync(fresh, { recursive: true });
147
+ const old = new Date(Date.now() - 10 * 60 * 1_000);
148
+ utimesSync(stale, old, old);
149
+ writeInstalledPackage(piHome, "daemon", "1.0.0");
150
+ const materializer = new IsolatedDaemonPackageMaterializer(stateDirectory, writeFakeNpm(root));
151
+
152
+ const installed = await materializer.materialize(piHome, "npm:daemon");
153
+
154
+ expect(existsSync(stale)).toBe(false);
155
+ expect(existsSync(fresh)).toBe(true);
156
+ installed.rollback();
157
+ });
158
+
120
159
  it("rejects an unsafe installed version before creating a service directory", async () => {
121
160
  const root = temporaryRoot();
122
161
  const piHome = join(root, "pi");