@danypops/pi-packed 0.28.7 → 0.28.8

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.
@@ -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":"AAWA,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;IAgFrF,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.8",
4
4
  "description": "Pi package lifecycle, validation, daemon, tools, profiles, and TUI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3,7 +3,9 @@ import { join, sep } from "node:path";
3
3
  import { npmPackageName } from "../packages/installed.ts";
4
4
 
5
5
  const MAX_VERSION_DIRECTORIES = 20;
6
+ const MAX_TEMP_DIRECTORY_SCAN = 100;
6
7
  const RETAINED_VERSION_DIRECTORIES = 2;
8
+ const STALE_TEMP_DIRECTORY_MS = 5 * 60 * 1_000;
7
9
  const MAX_INSTALL_OUTPUT_BYTES = 64 * 1024;
8
10
  const VERSION_DIRECTORY_RE = /^[0-9A-Za-z][0-9A-Za-z.+_-]{0,127}$/;
9
11
 
@@ -98,6 +100,8 @@ export class IsolatedDaemonPackageMaterializer implements DaemonPackageMateriali
98
100
  const finalDirectory = join(packageRoot, version);
99
101
  const finalPiHome = isolatedPiHome(finalDirectory);
100
102
  const finalPackageJson = resolvedPackageJson(finalPiHome, packageName);
103
+ mkdirSync(packageRoot, { recursive: true, mode: 0o700 });
104
+ this.pruneTemporaryDirectories(packageRoot);
101
105
  if (readVersion(finalPackageJson) === version) {
102
106
  return {
103
107
  piHome: finalPiHome,
@@ -107,7 +111,6 @@ export class IsolatedDaemonPackageMaterializer implements DaemonPackageMateriali
107
111
  };
108
112
  }
109
113
 
110
- mkdirSync(packageRoot, { recursive: true, mode: 0o700 });
111
114
  if (existsSync(finalDirectory)) rmSync(finalDirectory, { recursive: true, force: true });
112
115
  const temporaryDirectory = join(packageRoot, `.${version}.${process.pid}.${crypto.randomUUID()}.tmp`);
113
116
  const temporaryPiHome = isolatedPiHome(temporaryDirectory);
@@ -176,6 +179,26 @@ export class IsolatedDaemonPackageMaterializer implements DaemonPackageMateriali
176
179
  return path.startsWith(join(this.stateDirectory, "services") + sep);
177
180
  }
178
181
 
182
+ private pruneTemporaryDirectories(packageRoot: string): void {
183
+ let names: string[];
184
+ try {
185
+ names = readdirSync(packageRoot)
186
+ .filter((name) => name.startsWith(".") && name.endsWith(".tmp"))
187
+ .slice(0, MAX_TEMP_DIRECTORY_SCAN);
188
+ } catch {
189
+ return;
190
+ }
191
+ const staleBefore = Date.now() - STALE_TEMP_DIRECTORY_MS;
192
+ for (const name of names) {
193
+ const path = join(packageRoot, name);
194
+ try {
195
+ if (statSync(path).mtimeMs < staleBefore) rmSync(path, { recursive: true, force: true });
196
+ } catch {
197
+ // A concurrent materializer may have renamed or removed the candidate.
198
+ }
199
+ }
200
+ }
201
+
179
202
  private prune(packageRoot: string, activeDirectory: string): void {
180
203
  let directories: string[];
181
204
  try {
@@ -188,7 +211,8 @@ export class IsolatedDaemonPackageMaterializer implements DaemonPackageMateriali
188
211
  return;
189
212
  }
190
213
  directories.sort((left, right) => statSync(right).mtimeMs - statSync(left).mtimeMs);
191
- const retained = new Set([activeDirectory, ...directories.slice(0, RETAINED_VERSION_DIRECTORIES)]);
214
+ const prior = directories.filter((directory) => directory !== activeDirectory).slice(0, RETAINED_VERSION_DIRECTORIES - 1);
215
+ const retained = new Set([activeDirectory, ...prior]);
192
216
  for (const directory of directories) {
193
217
  if (!retained.has(directory)) rmSync(directory, { recursive: true, force: true });
194
218
  }
@@ -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";
@@ -117,6 +117,44 @@ describe("IsolatedDaemonPackageMaterializer", () => {
117
117
  expect(existsSync(versionDirectory)).toBe(false);
118
118
  });
119
119
 
120
+ it("retains only the active and immediately previous committed versions", async () => {
121
+ const root = temporaryRoot();
122
+ const piHome = join(root, "pi");
123
+ const stateDirectory = join(root, "state");
124
+ const materializer = new IsolatedDaemonPackageMaterializer(stateDirectory, writeFakeNpm(root));
125
+ for (const version of ["1.0.0", "1.1.0", "1.2.0"]) {
126
+ writeInstalledPackage(piHome, "daemon", version);
127
+ const installed = await materializer.materialize(piHome, "npm:daemon");
128
+ installed.commit();
129
+ }
130
+
131
+ expect(readdirSync(join(stateDirectory, "services", "daemon")).filter((name) => !name.startsWith(".")).sort()).toEqual([
132
+ "1.1.0",
133
+ "1.2.0",
134
+ ]);
135
+ });
136
+
137
+ it("cleans bounded stale crash candidates without touching a fresh concurrent candidate", async () => {
138
+ const root = temporaryRoot();
139
+ const piHome = join(root, "pi");
140
+ const stateDirectory = join(root, "state");
141
+ const packageRoot = join(stateDirectory, "services", "daemon");
142
+ const stale = join(packageRoot, ".stale.tmp");
143
+ const fresh = join(packageRoot, ".fresh.tmp");
144
+ mkdirSync(stale, { recursive: true });
145
+ mkdirSync(fresh, { recursive: true });
146
+ const old = new Date(Date.now() - 10 * 60 * 1_000);
147
+ utimesSync(stale, old, old);
148
+ writeInstalledPackage(piHome, "daemon", "1.0.0");
149
+ const materializer = new IsolatedDaemonPackageMaterializer(stateDirectory, writeFakeNpm(root));
150
+
151
+ const installed = await materializer.materialize(piHome, "npm:daemon");
152
+
153
+ expect(existsSync(stale)).toBe(false);
154
+ expect(existsSync(fresh)).toBe(true);
155
+ installed.rollback();
156
+ });
157
+
120
158
  it("rejects an unsafe installed version before creating a service directory", async () => {
121
159
  const root = temporaryRoot();
122
160
  const piHome = join(root, "pi");