@danypops/pi-packed 0.21.12 → 0.21.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.
@@ -29,12 +29,13 @@
29
29
  * inline next to the row currently updating, settling into a real ✓/✗
30
30
  * plus a bounded tail of that row's own actual captured stdout/stderr
31
31
  * once it finishes, never a determinate bar (a single subprocess call has
32
- * no knowable percentage). Rows render through Malevich's Table (real
33
- * column-aligned Package/Version/status cells, per-row selection styling
34
- * baked into each cell since Table's own cellStyle is column-wide, not
35
- * row-wide) inside this tab's own scroll-window slice -- Table
36
- * deliberately owns no pagination of its own, so the visible-window-
37
- * around-selectedIndex math stays here. u/x/d and the Enter action menu
32
+ * no knowable percentage). Rows render as a column-major grid of
33
+ * Malevich Cards (computeCardColumns/renderCardGrid, below) -- one card
34
+ * per package, as many side by side as `width` allows above a fixed
35
+ * minimum card width, rather than one full-width card per line -- inside
36
+ * this tab's own scroll-window slice. No pagination baked into the grid
37
+ * helpers themselves, so the visible-window-around-selectedIndex math
38
+ * stays here. u/x/d and the Enter action menu
38
39
  * all run their whole approve+mutate+confirmReload flow inline, via the
39
40
  * shared TabHost's inlineCtx -- every confirm() along the way renders as
40
41
  * a real Malevich Dialog on this SAME overlay, dispatched by literal y/n
@@ -407,6 +408,87 @@ async function showActionMenu(ctx: ExtensionCommandContext, row: Row): Promise<"
407
408
  );
408
409
  }
409
410
 
411
+ /** A card narrower than this wastes more space on border/padding than it
412
+ * shows of a real scoped package name ("@scope/pi-longer-name") plus its
413
+ * version/status -- the floor `computeCardColumns` uses when deciding how
414
+ * many side-by-side columns actually fit `width`, rather than guessing a
415
+ * fixed row/page count independent of the panel's real width. */
416
+ const MIN_CARD_WIDTH = 28;
417
+ const CARD_GRID_GAP = 1;
418
+
419
+ /** How many side-by-side card columns fit `width` without any of them
420
+ * dropping below MIN_CARD_WIDTH, capped at `itemCount` -- never more
421
+ * columns than there are real items to fill them (a wide terminal with
422
+ * only 2 packages installed gets 2 columns, not 4 mostly-empty ones). */
423
+ function computeCardColumns(width: number, itemCount: number): number {
424
+ if (itemCount <= 0) return 1;
425
+ const fits = Math.floor((width + CARD_GRID_GAP) / (MIN_CARD_WIDTH + CARD_GRID_GAP));
426
+ return Math.max(1, Math.min(fits, itemCount));
427
+ }
428
+
429
+ /** Exact per-column widths for `columns` side-by-side cards inside `width`,
430
+ * every gap included -- sums (with CARD_GRID_GAP between each) to exactly
431
+ * `width`, distributing any remainder across the leading columns instead
432
+ * of leaving it as slack nobody renders into (the same exact-width
433
+ * contract every other line in this panel already holds, verified by this
434
+ * file's own "every rendered line at the exact same real width" test). */
435
+ function distributeColumnWidths(width: number, columns: number): number[] {
436
+ const available = width - CARD_GRID_GAP * (columns - 1);
437
+ const base = Math.floor(available / columns);
438
+ const remainder = available - base * columns;
439
+ return Array.from({ length: columns }, (_, i) => base + (i < remainder ? 1 : 0));
440
+ }
441
+
442
+ /** Lays `items` into a column-major grid of exactly `columns` columns
443
+ * (column 0 filled top-to-bottom first, then column 1, ...) -- the same
444
+ * fill order `ls -C` uses, so a flat Up/Down cycle through `items` moves
445
+ * down one column at a time instead of snaking left-to-right through a
446
+ * row-major grid, with no new keybinding needed (Left/Right stay reserved
447
+ * for the panel's own tab-cycling -- see this file's own header comment).
448
+ * `columns` is always the caller's own already-computed computeCardColumns
449
+ * result, never re-derived here from `items.length` -- a caller windowing
450
+ * `items` into a page shorter than its full list must still render that
451
+ * page at the same column count the rest of the list uses, or the grid
452
+ * would reflow mid-scroll. `selectedIndex` is an index into `items`
453
+ * itself, or -1 when nothing in this grid is selected. */
454
+ function renderCardGrid<T>(
455
+ items: T[],
456
+ columns: number,
457
+ width: number,
458
+ selectedIndex: number,
459
+ renderCard: (item: T, columnWidth: number, selected: boolean) => string[],
460
+ ): string[] {
461
+ if (items.length === 0) return [];
462
+ const columnWidths = distributeColumnWidths(width, columns);
463
+ const rowsPerColumn = Math.ceil(items.length / columns);
464
+ const rendered: string[][] = [];
465
+ for (let c = 0; c < columns; c++) {
466
+ const columnWidth = columnWidths[c] ?? MIN_CARD_WIDTH;
467
+ const lines: string[] = [];
468
+ for (let r = 0; r < rowsPerColumn; r++) {
469
+ const index = c * rowsPerColumn + r;
470
+ const item = items[index];
471
+ if (item === undefined) break;
472
+ lines.push(...renderCard(item, columnWidth, index === selectedIndex));
473
+ }
474
+ rendered.push(lines);
475
+ }
476
+ const height = Math.max(0, ...rendered.map((lines) => lines.length));
477
+ const out: string[] = [];
478
+ for (let row = 0; row < height; row++) {
479
+ out.push(
480
+ rendered
481
+ .map((lines, c) => {
482
+ const line = lines[row] ?? "";
483
+ const pad = Math.max(0, (columnWidths[c] ?? MIN_CARD_WIDTH) - visibleWidth(line));
484
+ return line + " ".repeat(pad);
485
+ })
486
+ .join(" ".repeat(CARD_GRID_GAP)),
487
+ );
488
+ }
489
+ return out;
490
+ }
491
+
410
492
  /** Packages -- the panel's default/"home" tab. A real Component (not the
411
493
  * panel's own top-level ctx.ui.custom owner anymore); the shared TabHost
412
494
  * gives it inline approval/reload dialogs and a way to signal the overlay
@@ -509,33 +591,42 @@ export class PackagesTab implements Component {
509
591
  lines.push(theme.fg("muted", " No packages"));
510
592
  return lines;
511
593
  }
512
- // No scrolling of its own (Malevich's Table is deliberately
513
- // unopinionated about pagination) -- the visible window around
514
- // selectedIndex stays this tab's own job.
515
- const start = Math.max(0, Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filtered.length - this.maxVisible));
516
- const end = Math.min(start + this.maxVisible, this.filtered.length);
517
- for (const [offset, row] of this.filtered.slice(start, end).entries()) {
518
- const selected = start + offset === this.selectedIndex;
519
- // A settled row shows its real outcome even for the instant before
520
- // the next row's "start" event moves updatingRowName off it.
521
- const rowSettled = this.settled.get(row.name);
522
- const isUpdating = !rowSettled && this.updatingRowName === row.name;
523
- const status = isUpdating
524
- ? theme.fg("accent", `${this.spinner.glyph()} updating…`)
525
- : rowSettled
526
- ? theme.fg(rowSettled.ok ? "success" : "error", `${rowSettled.ok ? "✓" : "✗"}${rowSettled.tail ? ` ${rowSettled.tail}` : ""}`)
527
- : row.hasUpdate
528
- ? theme.fg("warning", `↑${row.latest}`)
529
- : theme.fg("muted", "installed");
530
- const card = new Card({
531
- title: theme.bold(row.name),
532
- content: [`${theme.fg("dim", row.version)} · ${status}`],
533
- selected,
534
- theme: cardTheme(theme),
535
- measure: this.measure,
536
- });
537
- lines.push(...card.render(width));
538
- }
594
+ // Column count is derived from `width` (and the total item count, so a
595
+ // short list never gets more columns than it has items) once per
596
+ // render, then held fixed across the whole page -- otherwise scrolling
597
+ // through a filtered list would reflow the grid mid-scroll. No
598
+ // scrolling of its own beyond that: the visible-window-around-
599
+ // selectedIndex math stays this tab's own job, same as before, just
600
+ // now counting grid rows (maxVisible) times columns instead of a flat
601
+ // one-card-per-line count.
602
+ const columns = computeCardColumns(width, this.filtered.length);
603
+ const pageSize = columns * this.maxVisible;
604
+ const start = Math.max(0, Math.min(this.selectedIndex - Math.floor(pageSize / 2), this.filtered.length - pageSize));
605
+ const end = Math.min(start + pageSize, this.filtered.length);
606
+ const page = this.filtered.slice(start, end);
607
+ lines.push(
608
+ ...renderCardGrid(page, columns, width, this.selectedIndex - start, (row, columnWidth, selected) => {
609
+ // A settled row shows its real outcome even for the instant before
610
+ // the next row's "start" event moves updatingRowName off it.
611
+ const rowSettled = this.settled.get(row.name);
612
+ const isUpdating = !rowSettled && this.updatingRowName === row.name;
613
+ const status = isUpdating
614
+ ? theme.fg("accent", `${this.spinner.glyph()} updating…`)
615
+ : rowSettled
616
+ ? theme.fg(rowSettled.ok ? "success" : "error", `${rowSettled.ok ? "✓" : "✗"}${rowSettled.tail ? ` ${rowSettled.tail}` : ""}`)
617
+ : row.hasUpdate
618
+ ? theme.fg("warning", `↑${row.latest}`)
619
+ : theme.fg("muted", "installed");
620
+ const card = new Card({
621
+ title: theme.bold(row.name),
622
+ content: [`${theme.fg("dim", row.version)} · ${status}`],
623
+ selected,
624
+ theme: cardTheme(theme),
625
+ measure: this.measure,
626
+ });
627
+ return card.render(columnWidth);
628
+ }),
629
+ );
539
630
  const hasScroll = start > 0 || end < this.filtered.length;
540
631
  lines.push(theme.fg("dim", ` ${hasScroll ? `${this.selectedIndex + 1}/${this.filtered.length} ` : ""}${this.mode}`));
541
632
  return lines;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-packed",
3
- "version": "0.21.12",
3
+ "version": "0.21.13",
4
4
  "description": "Pi package lifecycle, validation, daemon, tools, profiles, and TUI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,5 +1,5 @@
1
1
  import { afterEach, describe, expect, it } from "bun:test";
2
- import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { AuthenticatedRpcClient } from "@danypops/vehicle-client/rpc-client";
@@ -36,11 +36,18 @@ class NoopInstaller implements Installer {
36
36
  }
37
37
 
38
38
  let server: Server<undefined> | undefined;
39
+ const roots: string[] = [];
39
40
  afterEach(() => {
40
41
  server?.stop(true);
41
42
  server = undefined;
43
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
42
44
  });
43
45
 
46
+ function track(dir: string): string {
47
+ roots.push(dir);
48
+ return dir;
49
+ }
50
+
44
51
  const daysAgo = (n: number) => new Date(Date.now() - n * 24 * 60 * 60 * 1000).toISOString();
45
52
 
46
53
  describe("fetchBulkAdvisories", () => {
@@ -221,6 +228,7 @@ describe("scanInstalledPackages", () => {
221
228
  describe("advisories.scan operation (real daemon route)", () => {
222
229
  it("resolves real installed npm packages' on-disk versions and routes through the authenticated operation registry", async () => {
223
230
  const piHome = mkdtempSync(join(tmpdir(), "packed-advisories-daemon-"));
231
+ roots.push(piHome);
224
232
  writeFileSync(join(piHome, "settings.json"), JSON.stringify({ packages: ["npm:pi-vuln"] }));
225
233
  const pkgDir = join(piHome, "npm", "node_modules", "pi-vuln");
226
234
  mkdirSync(pkgDir, { recursive: true });
@@ -231,8 +239,8 @@ describe("advisories.scan operation (real daemon route)", () => {
231
239
  reg: new NoopRegistry(),
232
240
  inst: new NoopInstaller(),
233
241
  token: "test-token",
234
- stateDir: mkdtempSync(join(tmpdir(), "packed-advisories-state-")),
235
- dataDir: mkdtempSync(join(tmpdir(), "packed-advisories-data-")),
242
+ stateDir: track(mkdtempSync(join(tmpdir(), "packed-advisories-state-"))),
243
+ dataDir: track(mkdtempSync(join(tmpdir(), "packed-advisories-data-"))),
236
244
  piHome,
237
245
  // injected exactly like pi.status's piVersion seam -- never a real
238
246
  // network call to the live npm registry from an automated test.
@@ -258,9 +266,9 @@ describe("advisories.scan operation (real daemon route)", () => {
258
266
  reg: new NoopRegistry(),
259
267
  inst: new NoopInstaller(),
260
268
  token: "test-token",
261
- stateDir: mkdtempSync(join(tmpdir(), "packed-advisories-state-")),
262
- dataDir: mkdtempSync(join(tmpdir(), "packed-advisories-data-")),
263
- piHome: mkdtempSync(join(tmpdir(), "packed-advisories-pihome-")),
269
+ stateDir: track(mkdtempSync(join(tmpdir(), "packed-advisories-state-"))),
270
+ dataDir: track(mkdtempSync(join(tmpdir(), "packed-advisories-data-"))),
271
+ piHome: track(mkdtempSync(join(tmpdir(), "packed-advisories-pihome-"))),
264
272
  });
265
273
  const client = new AuthenticatedRpcClient<OperationName, OperationInputs, OperationOutputs>("http://packed.test", "test-token", {
266
274
  label: "Packed",
@@ -274,9 +282,9 @@ describe("advisories.scan operation (real daemon route)", () => {
274
282
  reg: new NoopRegistry(),
275
283
  inst: new NoopInstaller(),
276
284
  token: "test-token",
277
- stateDir: mkdtempSync(join(tmpdir(), "packed-advisories-state-")),
278
- dataDir: mkdtempSync(join(tmpdir(), "packed-advisories-data-")),
279
- piHome: mkdtempSync(join(tmpdir(), "packed-advisories-pihome-")),
285
+ stateDir: track(mkdtempSync(join(tmpdir(), "packed-advisories-state-"))),
286
+ dataDir: track(mkdtempSync(join(tmpdir(), "packed-advisories-data-"))),
287
+ piHome: track(mkdtempSync(join(tmpdir(), "packed-advisories-pihome-"))),
280
288
  });
281
289
  const client = new AuthenticatedRpcClient<OperationName, OperationInputs, OperationOutputs>("http://packed.test", "test-token", {
282
290
  label: "Packed",
@@ -12,6 +12,11 @@ afterEach(() => {
12
12
  for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
13
13
  });
14
14
 
15
+ function track(dir: string): string {
16
+ roots.push(dir);
17
+ return dir;
18
+ }
19
+
15
20
  function pkg(manifest: Record<string, unknown>): string {
16
21
  const root = mkdtempSync(join(tmpdir(), "packed-cleanup-"));
17
22
  roots.push(root);
@@ -169,8 +174,8 @@ describe("packed remove applies pi.cleanup before delegating to pi remove (real
169
174
  reg: new NoopRegistry(),
170
175
  inst,
171
176
  token: "test-token",
172
- stateDir: mkdtempSync(join(tmpdir(), "packed-cleanup-state-")),
173
- dataDir: mkdtempSync(join(tmpdir(), "packed-cleanup-data-")),
177
+ stateDir: track(mkdtempSync(join(tmpdir(), "packed-cleanup-state-"))),
178
+ dataDir: track(mkdtempSync(join(tmpdir(), "packed-cleanup-data-"))),
174
179
  piHome,
175
180
  });
176
181
  return new AuthenticatedRpcClient<OperationName, OperationInputs, OperationOutputs>("http://packed.test", "test-token", {
@@ -1,6 +1,6 @@
1
- import { afterAll, beforeAll, describe, expect, it } from "bun:test";
1
+ import { afterAll, afterEach, beforeAll, describe, expect, it } from "bun:test";
2
2
  import { spawnSync } from "node:child_process";
3
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join, resolve } from "node:path";
6
6
  import { writeDaemonHandle } from "@danypops/vehicle-server/paths";
@@ -125,6 +125,16 @@ class FakeDaemonServiceInstaller {
125
125
  }
126
126
  }
127
127
 
128
+ const roots: string[] = [];
129
+ afterEach(() => {
130
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
131
+ });
132
+
133
+ function track(dir: string): string {
134
+ roots.push(dir);
135
+ return dir;
136
+ }
137
+
128
138
  function deps(over: Partial<CliDeps> = {}): CliDeps {
129
139
  return {
130
140
  reg: new FakeRegistry(),
@@ -138,8 +148,8 @@ function deps(over: Partial<CliDeps> = {}): CliDeps {
138
148
  return { mutationApproval };
139
149
  },
140
150
  },
141
- stateDir: mkdtempSync(join(tmpdir(), "packed-")),
142
- piHome: mkdtempSync(join(tmpdir(), "packed-pihome-")),
151
+ stateDir: track(mkdtempSync(join(tmpdir(), "packed-"))),
152
+ piHome: track(mkdtempSync(join(tmpdir(), "packed-pihome-"))),
143
153
  ...over,
144
154
  };
145
155
  }
@@ -389,7 +399,7 @@ describe("CLI", () => {
389
399
  it("updates --project also checks a project's own .pi/settings.json pins -- global-only misses them entirely", async () => {
390
400
  const d = deps();
391
401
  writeFileSync(join(d.piHome, "settings.json"), JSON.stringify({ packages: ["npm:pi-global@1.0.0"] }));
392
- const projectRoot = mkdtempSync(join(tmpdir(), "packed-project-"));
402
+ const projectRoot = track(mkdtempSync(join(tmpdir(), "packed-project-")));
393
403
  const projectHome = join(projectRoot, ".pi");
394
404
  mkdirSync(projectHome, { recursive: true });
395
405
  writeFileSync(join(projectHome, "settings.json"), JSON.stringify({ packages: ["npm:papyrus@0.21.2"] }));
@@ -938,7 +948,7 @@ describe("CLI", () => {
938
948
  });
939
949
 
940
950
  it("advisories runs standalone without a daemon and degrades to zero findings, never a real network call, when nothing is installed", async () => {
941
- const d = deps({ piHome: mkdtempSync(join(tmpdir(), "packed-advisories-cli-")) });
951
+ const d = deps({ piHome: track(mkdtempSync(join(tmpdir(), "packed-advisories-cli-"))) });
942
952
  const result = await cliRun(["advisories", "--json"], d);
943
953
  expect(result.code).toBe(0);
944
954
  expect(JSON.parse(result.out)).toEqual({ scanned: 0, findings: [], diagnostics: [], truncated: false });
@@ -956,7 +966,7 @@ describe("CLI", () => {
956
966
  });
957
967
 
958
968
  it("resources list and toggle run standalone without a daemon (CLI parity for the daemon-only resources.list/toggle operations)", async () => {
959
- const piHome = mkdtempSync(join(tmpdir(), "packed-resources-cli-"));
969
+ const piHome = track(mkdtempSync(join(tmpdir(), "packed-resources-cli-")));
960
970
  writeFileSync(join(piHome, "settings.json"), JSON.stringify({ packages: ["npm:pi-demo"] }));
961
971
  const pkgDir = join(piHome, "npm", "node_modules", "pi-demo");
962
972
  mkdirSync(pkgDir, { recursive: true });
@@ -998,7 +1008,7 @@ describe("CLI", () => {
998
1008
  (bwrapUsable ? it : it.skip)(
999
1009
  "doctor runs standalone without a daemon and reproduces the jittor incident through the real CLI (CLI parity for the daemon-only doctor.run operation)",
1000
1010
  async () => {
1001
- const piHome = mkdtempSync(join(tmpdir(), "packed-doctor-cli-"));
1011
+ const piHome = track(mkdtempSync(join(tmpdir(), "packed-doctor-cli-")));
1002
1012
  writeFileSync(join(piHome, "settings.json"), JSON.stringify({ packages: ["npm:pi-papyrus"] }));
1003
1013
  const globalPkg = join(piHome, "npm", "node_modules", "pi-papyrus");
1004
1014
  mkdirSync(join(globalPkg, "extension"), { recursive: true });
@@ -1007,7 +1017,7 @@ describe("CLI", () => {
1007
1017
  JSON.stringify({ name: "pi-papyrus", version: "1.0.0", pi: { extensions: ["extension/index.ts"] } }),
1008
1018
  );
1009
1019
  writeFileSync(join(globalPkg, "extension", "index.ts"), 'export default function (pi: any) { pi.registerTool({ name: "tasks" }); }');
1010
- const projectRoot = mkdtempSync(join(tmpdir(), "packed-doctor-cli-project-"));
1020
+ const projectRoot = track(mkdtempSync(join(tmpdir(), "packed-doctor-cli-project-")));
1011
1021
  const projectHome = join(projectRoot, ".pi");
1012
1022
  mkdirSync(projectHome, { recursive: true });
1013
1023
  writeFileSync(join(projectHome, "settings.json"), JSON.stringify({ packages: ["npm:papyrus"] }));
@@ -1063,12 +1073,14 @@ describe("CLI", () => {
1063
1073
  describe("daemon client", () => {
1064
1074
  let server: Server<undefined>;
1065
1075
  let daemonDir: string;
1076
+ let daemonPiHome: string;
1066
1077
  let daemonPaths: PackedPaths;
1067
1078
  let daemonInstaller: FakeInstaller;
1068
1079
  const daemonToken = "d".repeat(64);
1069
1080
 
1070
1081
  beforeAll(async () => {
1071
1082
  daemonDir = mkdtempSync(join(tmpdir(), "packed-daemon-"));
1083
+ daemonPiHome = mkdtempSync(join(tmpdir(), "packed-daemon-pi-"));
1072
1084
  daemonPaths = resolvePackedPaths({ env: { PI_PACKED_HOME: daemonDir } });
1073
1085
  writeFileSync(daemonPaths.token, `${daemonToken}\n`);
1074
1086
  daemonInstaller = new FakeInstaller();
@@ -1100,7 +1112,7 @@ describe("daemon client", () => {
1100
1112
  },
1101
1113
  token: daemonToken,
1102
1114
  stateDir: daemonDir,
1103
- piHome: mkdtempSync(join(tmpdir(), "packed-daemon-pi-")),
1115
+ piHome: daemonPiHome,
1104
1116
  packer: {
1105
1117
  async verify(path) {
1106
1118
  return {
@@ -1169,7 +1181,11 @@ describe("daemon client", () => {
1169
1181
  server = Bun.serve({ port: 0, hostname: "127.0.0.1", fetch: (req) => app.fetch(req) });
1170
1182
  writeDaemonHandle(daemonPaths.handle, { host: "127.0.0.1", port: server.port!, pid: process.pid });
1171
1183
  });
1172
- afterAll(() => server.stop(true));
1184
+ afterAll(() => {
1185
+ server.stop(true);
1186
+ rmSync(daemonDir, { recursive: true, force: true });
1187
+ rmSync(daemonPiHome, { recursive: true, force: true });
1188
+ });
1173
1189
 
1174
1190
  it("probe finds a live daemon", async () => {
1175
1191
  const found = await probe(daemonPaths);
@@ -1188,7 +1204,7 @@ describe("daemon client", () => {
1188
1204
  });
1189
1205
 
1190
1206
  it("probe rejects dead state", async () => {
1191
- const directory = mkdtempSync(join(tmpdir(), "packed-"));
1207
+ const directory = track(mkdtempSync(join(tmpdir(), "packed-")));
1192
1208
  expect(await probe(resolvePackedPaths({ env: { PI_PACKED_HOME: directory } }))).toBeUndefined();
1193
1209
  });
1194
1210
 
@@ -1260,7 +1276,7 @@ describe("daemon client", () => {
1260
1276
  it("resolveRegistry prefers daemon, falls back direct", async () => {
1261
1277
  const viaDaemon = await resolveRegistry(daemonPaths, "https://registry.npmjs.org");
1262
1278
  expect(viaDaemon).toBeInstanceOf(DaemonRegistry);
1263
- const directory = mkdtempSync(join(tmpdir(), "packed-"));
1279
+ const directory = track(mkdtempSync(join(tmpdir(), "packed-")));
1264
1280
  const direct = await resolveRegistry(resolvePackedPaths({ env: { PI_PACKED_HOME: directory } }), "https://registry.npmjs.org");
1265
1281
  expect(direct).toBeInstanceOf(HttpRegistry);
1266
1282
  });
@@ -1,5 +1,5 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { existsSync, mkdtempSync } from "node:fs";
1
+ import { afterEach, describe, expect, it } from "bun:test";
2
+ import { existsSync, mkdtempSync, rmSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { syncCatalog } from "../src/packages/catalog.ts";
@@ -127,9 +127,15 @@ class PagedRegistry implements Registry {
127
127
  }
128
128
  }
129
129
 
130
+ const roots: string[] = [];
131
+ afterEach(() => {
132
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
133
+ });
134
+
130
135
  describe("syncCatalog → SQLite", () => {
131
136
  it("accumulates pages into the DB and records sync meta", async () => {
132
137
  const dir = mkdtempSync(join(tmpdir(), "packed-"));
138
+ roots.push(dir);
133
139
  const reg = new PagedRegistry({ 0: PKGS.slice(0, 2), 2: PKGS.slice(2) }, 3);
134
140
  expect(await syncCatalog(reg, dir)).toBe(3);
135
141
  expect(existsSync(dbPath(dir))).toBe(true);
@@ -13,6 +13,11 @@ afterEach(() => {
13
13
  for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
14
14
  });
15
15
 
16
+ function track(dir: string): string {
17
+ roots.push(dir);
18
+ return dir;
19
+ }
20
+
16
21
  // Same sandbox-availability probe as smoke.test.ts: binary presence alone
17
22
  // doesn't prove bwrap actually works under this host's user namespaces.
18
23
  function bwrapUsable(): boolean {
@@ -216,8 +221,8 @@ describeIfSandboxed("doctor.run (daemon RPC wiring)", () => {
216
221
  reg: new NoopRegistry(),
217
222
  inst: new NoopInstaller(),
218
223
  token: "test-token",
219
- stateDir: mkdtempSync(join(tmpdir(), "packed-doctor-state-")),
220
- dataDir: mkdtempSync(join(tmpdir(), "packed-doctor-data-")),
224
+ stateDir: track(mkdtempSync(join(tmpdir(), "packed-doctor-state-"))),
225
+ dataDir: track(mkdtempSync(join(tmpdir(), "packed-doctor-data-"))),
221
226
  piHome: home,
222
227
  });
223
228
  const client = new AuthenticatedRpcClient<OperationName, OperationInputs, OperationOutputs>("http://packed.test", "test-token", {
@@ -1,5 +1,5 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
1
+ import { afterEach, describe, expect, it } from "bun:test";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { AuthenticatedRpcClient } from "@danypops/vehicle-client/rpc-client";
@@ -17,8 +17,18 @@ import {
17
17
  } from "../src/packages/installed.ts";
18
18
  import type { Installer, PkgInfo, Registry, SearchPage, UpdateOutcome, UpdatesSnapshot } from "../src/packages/package.ts";
19
19
 
20
+ const roots: string[] = [];
21
+ afterEach(() => {
22
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
23
+ });
24
+
25
+ function track(dir: string): string {
26
+ roots.push(dir);
27
+ return dir;
28
+ }
29
+
20
30
  function writePiHome(settings: unknown, nodeModules: Record<string, string> = {}): string {
21
- const dir = mkdtempSync(join(tmpdir(), "packed-pihome-"));
31
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-pihome-")));
22
32
  writeFileSync(join(dir, "settings.json"), JSON.stringify(settings));
23
33
  for (const [name, version] of Object.entries(nodeModules)) {
24
34
  const pkgDir = join(dir, "npm", "node_modules", name);
@@ -99,7 +109,7 @@ describe("readInstalledPackages", () => {
99
109
  });
100
110
 
101
111
  it("missing settings → empty", () => {
102
- expect(readInstalledPackages(mkdtempSync(join(tmpdir(), "packed-")))).toEqual([]);
112
+ expect(readInstalledPackages(track(mkdtempSync(join(tmpdir(), "packed-"))))).toEqual([]);
103
113
  });
104
114
  });
105
115
 
@@ -113,7 +123,7 @@ describe("readInstalledPackagesAcrossScopes", () => {
113
123
 
114
124
  it("reproduces the real jittor gap: a stale project-scoped pin is invisible to a global-only read, visible once project scope is included", () => {
115
125
  const home = writePiHome({ packages: ["npm:pi-global@1.0.0"] });
116
- const projectRoot = mkdtempSync(join(tmpdir(), "packed-project-"));
126
+ const projectRoot = track(mkdtempSync(join(tmpdir(), "packed-project-")));
117
127
  const projectHome = join(projectRoot, ".pi");
118
128
  mkdirSync(projectHome, { recursive: true });
119
129
  writeFileSync(join(projectHome, "settings.json"), JSON.stringify({ packages: ["npm:papyrus@0.21.2"] }));
@@ -128,7 +138,7 @@ describe("readInstalledPackagesAcrossScopes", () => {
128
138
 
129
139
  it("is unaffected by a missing project settings file", () => {
130
140
  const home = writePiHome({ packages: ["npm:pi-global@1.0.0"] });
131
- const projectRoot = mkdtempSync(join(tmpdir(), "packed-project-"));
141
+ const projectRoot = track(mkdtempSync(join(tmpdir(), "packed-project-")));
132
142
  expect(readInstalledPackagesAcrossScopes(home, projectRoot)).toEqual([
133
143
  { name: "pi-global", pinned: "1.0.0", installed: undefined, scope: "global" },
134
144
  ]);
@@ -214,13 +224,13 @@ class NoopInstaller implements Installer {
214
224
 
215
225
  describe("package.updates.project (daemon RPC wiring)", () => {
216
226
  it("computes a live, cross-scope drift check on demand, distinct from package.updates' own persisted global-only snapshot", async () => {
217
- const home = mkdtempSync(join(tmpdir(), "packed-updates-project-"));
227
+ const home = track(mkdtempSync(join(tmpdir(), "packed-updates-project-")));
218
228
  writeFileSync(join(home, "settings.json"), JSON.stringify({ packages: ["npm:pi-global@1.0.0"] }));
219
- const projectRoot = mkdtempSync(join(tmpdir(), "packed-updates-project-root-"));
229
+ const projectRoot = track(mkdtempSync(join(tmpdir(), "packed-updates-project-root-")));
220
230
  const projectHome = join(projectRoot, ".pi");
221
231
  mkdirSync(projectHome, { recursive: true });
222
232
  writeFileSync(join(projectHome, "settings.json"), JSON.stringify({ packages: ["npm:papyrus@0.21.2"] }));
223
- const stateDir = mkdtempSync(join(tmpdir(), "packed-updates-project-state-"));
233
+ const stateDir = track(mkdtempSync(join(tmpdir(), "packed-updates-project-state-")));
224
234
  const db = openDb(dbPath(stateDir));
225
235
  replaceAll(
226
236
  db,
@@ -249,7 +259,7 @@ describe("package.updates.project (daemon RPC wiring)", () => {
249
259
 
250
260
  describe("updates store", () => {
251
261
  it("roundtrips", async () => {
252
- const dir = mkdtempSync(join(tmpdir(), "packed-"));
262
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-")));
253
263
  const snap = { checkedAt: new Date().toISOString(), updates: [{ name: "a", installed: "1", latest: "2", detectedAt: "" }] };
254
264
  await saveUpdates(dir, snap);
255
265
  expect(await loadUpdates(dir)).toEqual(snap);
@@ -259,7 +269,7 @@ describe("updates store", () => {
259
269
 
260
270
  describe("watcher producer", () => {
261
271
  it("writes a snapshot on tick", async () => {
262
- const dir = mkdtempSync(join(tmpdir(), "packed-"));
272
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-")));
263
273
  let signalTick: ((snapshot: UpdatesSnapshot) => void) | undefined;
264
274
  const tick = new Promise<UpdatesSnapshot>((resolve) => {
265
275
  signalTick = resolve;
@@ -282,7 +292,7 @@ describe("watcher producer", () => {
282
292
 
283
293
  describe("catalog status", () => {
284
294
  it("stale when unsynced, fresh after sync", () => {
285
- const dir = mkdtempSync(join(tmpdir(), "packed-"));
295
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-")));
286
296
  expect(catalogStatus(dir, 6 * 3_600_000).stale).toBe(true);
287
297
  const db = openDb(`${dir}/packed.db`);
288
298
  replaceAll(db, [{ name: "a", version: "1" }], "test");
@@ -1,5 +1,5 @@
1
- import { afterAll, beforeAll, describe, expect, it } from "bun:test";
2
- import { mkdtempSync, readFileSync } from "node:fs";
1
+ import { afterAll, afterEach, beforeAll, describe, expect, it } from "bun:test";
2
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import type { Server } from "bun";
@@ -7,6 +7,16 @@ import { buildIndex, generateIndex, indexPath, indexStatus, readIndex, writeInde
7
7
  import { dbPath, openDb, replaceAll } from "../src/packages/db.ts";
8
8
  import { HttpRegistry } from "../src/registry/registry.ts";
9
9
 
10
+ const roots: string[] = [];
11
+ afterEach(() => {
12
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
13
+ });
14
+
15
+ function track(dir: string): string {
16
+ roots.push(dir);
17
+ return dir;
18
+ }
19
+
10
20
  // Guards the "never bulk GitHub calls" constraint against regression: a
11
21
  // future edit could easily start threading a GitHub-commit fetcher through
12
22
  // buildIndex the same way scoreTarget does. A source-level check catches
@@ -89,7 +99,10 @@ describe("buildIndex", () => {
89
99
  registry = new HttpRegistry(`http://127.0.0.1:${server.port}`, 250, 0, 1, `http://127.0.0.1:${server.port}`);
90
100
  });
91
101
 
92
- afterAll(() => server.stop(true));
102
+ afterAll(() => {
103
+ server.stop(true);
104
+ rmSync(catalogDir, { recursive: true, force: true });
105
+ });
93
106
 
94
107
  it("builds one entry per cataloged package, skipping a lookup failure rather than failing the whole run", async () => {
95
108
  const index = await buildIndex(registry, catalogDir, { delayMs: 0, currentPiVersion: async () => "0.83.0" });
@@ -123,7 +136,7 @@ describe("buildIndex", () => {
123
136
 
124
137
  describe("buildIndex bounds", () => {
125
138
  it("never calls downloads() -- confirmed live to trigger npm's 429s at real catalog scale -- and truncates past maxPackages, marking the result", async () => {
126
- const dir = mkdtempSync(join(tmpdir(), "packed-index-bounds-"));
139
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-index-bounds-")));
127
140
  const db = openDb(dbPath(dir));
128
141
  replaceAll(
129
142
  db,
@@ -160,7 +173,7 @@ describe("buildIndex bounds", () => {
160
173
  });
161
174
 
162
175
  it("collapses two concurrent callers into one run -- confirmed live to matter: the daemon's own maintenance tick and an on-demand CLI build overlapping compounded into a shutdown the daemon's SIGTERM grace period couldn't outlast", async () => {
163
- const dir = mkdtempSync(join(tmpdir(), "packed-index-concurrent-"));
176
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-index-concurrent-")));
164
177
  const db = openDb(dbPath(dir));
165
178
  replaceAll(db, [{ name: "pi-one", version: "1.0.0" }], "test");
166
179
  db.close();
@@ -203,7 +216,7 @@ describe("buildIndex bounds", () => {
203
216
 
204
217
  describe("buildIndex incremental delta scanning", () => {
205
218
  it("partitions the catalog into New/Changed/Unchanged against the prior index -- Unchanged makes zero live registry calls, New and Changed do", async () => {
206
- const dir = mkdtempSync(join(tmpdir(), "packed-index-delta-"));
219
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-index-delta-")));
207
220
  const db = openDb(dbPath(dir));
208
221
  replaceAll(
209
222
  db,
@@ -275,7 +288,7 @@ describe("buildIndex incremental delta scanning", () => {
275
288
  });
276
289
 
277
290
  it("maxPackages bounds only the New+Changed live-call queue -- Unchanged entries beyond that count still complete", async () => {
278
- const dir = mkdtempSync(join(tmpdir(), "packed-index-delta-bound-"));
291
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-index-delta-bound-")));
279
292
  const db = openDb(dbPath(dir));
280
293
  // 5 unchanged entries alone already exceed maxPackages: 1 below.
281
294
  replaceAll(
@@ -323,7 +336,7 @@ describe("buildIndex incremental delta scanning", () => {
323
336
 
324
337
  describe("index persistence", () => {
325
338
  it("writes, reads, and reports staleness", async () => {
326
- const dir = mkdtempSync(join(tmpdir(), "packed-index-store-"));
339
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-index-store-")));
327
340
  const path = indexPath(dir);
328
341
  expect(readIndex(path)).toBeUndefined();
329
342
  expect(indexStatus(path, 1_000).stale).toBe(true);
@@ -343,7 +356,7 @@ describe("index persistence", () => {
343
356
  });
344
357
 
345
358
  it("generateIndex builds and writes in one call", async () => {
346
- const dir = mkdtempSync(join(tmpdir(), "packed-index-generate-"));
359
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-index-generate-")));
347
360
  const db = openDb(dbPath(dir));
348
361
  replaceAll(db, [{ name: "pi-alpha", version: "1.0.0" }], "npm:keywords:pi-package");
349
362
  db.close();
@@ -7,8 +7,8 @@
7
7
  * a broken fixture must genuinely be refused, a healthy one must genuinely
8
8
  * pass.
9
9
  */
10
- import { describe, expect, it } from "bun:test";
11
- import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
10
+ import { afterEach, describe, expect, it } from "bun:test";
11
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
12
12
  import { tmpdir } from "node:os";
13
13
  import { dirname, join } from "node:path";
14
14
  import { fileURLToPath } from "node:url";
@@ -23,6 +23,16 @@ const BROKEN = join(FIXTURES, "broken-package");
23
23
  const NO_MANIFEST = join(FIXTURES, "no-manifest-package");
24
24
  const DEP_PACKAGE = join(FIXTURES, "dep-package");
25
25
 
26
+ const roots: string[] = [];
27
+ afterEach(() => {
28
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
29
+ });
30
+
31
+ function track(dir: string): string {
32
+ roots.push(dir);
33
+ return dir;
34
+ }
35
+
26
36
  /** Writes a fresh package into `dir` whose extension entry point has a real
27
37
  * runtime import (`packed-fixture-dep`, a `file:` dependency resolvable
28
38
  * fully offline) -- otherwise healthy, structurally identical to HEALTHY,
@@ -145,7 +155,7 @@ describe("HeadlessInstallValidator (vehicle-client-pi pi-load-harness, non-gatin
145
155
 
146
156
  describe("HeadlessInstallValidator (bug repro, packed-headlessinstallvalidator-never-installs-the): staged tarball's own declared dependencies are never installed before the load check", () => {
147
157
  it("approves an otherwise-healthy package whose entry point needs its one declared file: dependency", async () => {
148
- const dir = mkdtempSync(join(tmpdir(), "packed-install-validation-dep-fixture-"));
158
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-install-validation-dep-fixture-")));
149
159
  writePackageWithRealDependency(dir);
150
160
 
151
161
  // Ground truth this isn't a broken fixture: a plain `bun install` in an
@@ -193,7 +203,7 @@ describe("ExecInstaller.install() -- refuses before ever spawning the real pi bi
193
203
  // step to cwd into -- /bin/true always exits 0 regardless, proving both
194
204
  // the real install spawn *and* the re-resolution spawn were reached (a
195
205
  // refused install never gets this far to find out).
196
- const piHome = mkdtempSync(join(tmpdir(), "packed-install-validation-pihome-"));
206
+ const piHome = track(mkdtempSync(join(tmpdir(), "packed-install-validation-pihome-")));
197
207
  mkdirSync(join(piHome, "npm"), { recursive: true });
198
208
  const installer = new ExecInstaller(
199
209
  "/bin/true",
@@ -10,8 +10,8 @@
10
10
  * always-"Updated"-regardless-of-outcome text, and assert the real on-disk
11
11
  * version diff is what actually drives reloadRequired/alreadyUpToDate.
12
12
  */
13
- import { describe, expect, it } from "bun:test";
14
- import { chmodSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { afterEach, describe, expect, it } from "bun:test";
14
+ import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
15
15
  import { tmpdir } from "node:os";
16
16
  import { join } from "node:path";
17
17
  import { ExecInstaller } from "../src/packages/install.ts";
@@ -24,6 +24,16 @@ import { ExecInstaller } from "../src/packages/install.ts";
24
24
  * baked into the script file itself (not an env var) so it is immune to
25
25
  * Bun.spawn's default env snapshot not picking up late process.env writes.
26
26
  */
27
+ const roots: string[] = [];
28
+ afterEach(() => {
29
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
30
+ });
31
+
32
+ function track(dir: string): string {
33
+ roots.push(dir);
34
+ return dir;
35
+ }
36
+
27
37
  function writeFakePi(dir: string, rewrite?: { piHome: string; name: string; newVersion: string }): string {
28
38
  const script = join(dir, "fake-pi");
29
39
  const rewriteLine = rewrite
@@ -39,7 +49,7 @@ function writeFakePi(dir: string, rewrite?: { piHome: string; name: string; newV
39
49
  }
40
50
 
41
51
  function writePiHome(nodeModules: Record<string, string> = {}): string {
42
- const dir = mkdtempSync(join(tmpdir(), "packed-exec-pihome-"));
52
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-exec-pihome-")));
43
53
  mkdirSync(join(dir, "npm"), { recursive: true });
44
54
  for (const [name, version] of Object.entries(nodeModules)) {
45
55
  const pkgDir = join(dir, "npm", "node_modules", name);
@@ -73,7 +83,7 @@ function writeFakeNpm(dir: string, logFile: string, rewrite?: { piHome: string;
73
83
 
74
84
  describe("ExecInstaller.update() — honest reloadRequired despite pi's ambiguous exit-0 text", () => {
75
85
  it("pinned source, version genuinely unchanged: alreadyUpToDate, reloadRequired false", async () => {
76
- const scriptDir = mkdtempSync(join(tmpdir(), "packed-exec-bin-"));
86
+ const scriptDir = track(mkdtempSync(join(tmpdir(), "packed-exec-bin-")));
77
87
  const bin = writeFakePi(scriptDir);
78
88
  const npmBin = writeFakeNpm(scriptDir, join(scriptDir, "npm.log"));
79
89
  const piHome = writePiHome({ "@scope/pkg": "1.2.3" });
@@ -90,7 +100,7 @@ describe("ExecInstaller.update() — honest reloadRequired despite pi's ambiguou
90
100
  });
91
101
 
92
102
  it('unpinned source, already latest (pi still exits 0 and says "Updated"): alreadyUpToDate', async () => {
93
- const scriptDir = mkdtempSync(join(tmpdir(), "packed-exec-bin-"));
103
+ const scriptDir = track(mkdtempSync(join(tmpdir(), "packed-exec-bin-")));
94
104
  const bin = writeFakePi(scriptDir);
95
105
  const npmBin = writeFakeNpm(scriptDir, join(scriptDir, "npm.log"));
96
106
  const piHome = writePiHome({ plain: "0.5.0" });
@@ -106,7 +116,7 @@ describe("ExecInstaller.update() — honest reloadRequired despite pi's ambiguou
106
116
  });
107
117
 
108
118
  it("unpinned source, a real version change happens: reloadRequired true", async () => {
109
- const scriptDir = mkdtempSync(join(tmpdir(), "packed-exec-bin-"));
119
+ const scriptDir = track(mkdtempSync(join(tmpdir(), "packed-exec-bin-")));
110
120
  const piHome = writePiHome({ plain: "0.5.0" });
111
121
  const bin = writeFakePi(scriptDir, { piHome, name: "plain", newVersion: "0.6.0" });
112
122
  const npmBin = writeFakeNpm(scriptDir, join(scriptDir, "npm.log"));
@@ -122,7 +132,7 @@ describe("ExecInstaller.update() — honest reloadRequired despite pi's ambiguou
122
132
  });
123
133
 
124
134
  it("git: source (no npm resolution possible either side): conservatively assumes it may have changed", async () => {
125
- const scriptDir = mkdtempSync(join(tmpdir(), "packed-exec-bin-"));
135
+ const scriptDir = track(mkdtempSync(join(tmpdir(), "packed-exec-bin-")));
126
136
  const bin = writeFakePi(scriptDir);
127
137
  const npmBin = writeFakeNpm(scriptDir, join(scriptDir, "npm.log"));
128
138
  const piHome = writePiHome();
@@ -141,7 +151,7 @@ describe("ExecInstaller.update() — honest reloadRequired despite pi's ambiguou
141
151
 
142
152
  describe("ExecInstaller — forces full dependency re-resolution, not just the target's own subtree", () => {
143
153
  it("update() fixes a stale root-level sibling that the targeted pi update never touched", async () => {
144
- const scriptDir = mkdtempSync(join(tmpdir(), "packed-exec-bin-"));
154
+ const scriptDir = track(mkdtempSync(join(tmpdir(), "packed-exec-bin-")));
145
155
  // Simulates the confirmed live defect: after `pi update npm:@scope/leaf`,
146
156
  // the leaf's own version bumps, but a root-level sibling
147
157
  // (@scope/shared) that the freshly-updated leaf now needs a newer
@@ -170,7 +180,7 @@ describe("ExecInstaller — forces full dependency re-resolution, not just the t
170
180
  });
171
181
 
172
182
  it("install() also forces a full re-resolution after a successful pi install", async () => {
173
- const scriptDir = mkdtempSync(join(tmpdir(), "packed-exec-bin-"));
183
+ const scriptDir = track(mkdtempSync(join(tmpdir(), "packed-exec-bin-")));
174
184
  const piHome = writePiHome({ "@scope/shared": "1.0.0" });
175
185
  const bin = writeFakePi(scriptDir);
176
186
  const npmLog = join(scriptDir, "npm.log");
@@ -185,7 +195,7 @@ describe("ExecInstaller — forces full dependency re-resolution, not just the t
185
195
  });
186
196
 
187
197
  it("surfaces a failed re-resolution instead of silently reporting success", async () => {
188
- const scriptDir = mkdtempSync(join(tmpdir(), "packed-exec-bin-"));
198
+ const scriptDir = track(mkdtempSync(join(tmpdir(), "packed-exec-bin-")));
189
199
  const piHome = writePiHome({ plain: "0.5.0" });
190
200
  const bin = writeFakePi(scriptDir, { piHome, name: "plain", newVersion: "0.6.0" });
191
201
  const failingNpm = join(scriptDir, "fake-npm-fail");
@@ -1,5 +1,5 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { existsSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
1
+ import { afterEach, describe, expect, it } from "bun:test";
2
+ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { createGithubLastCommitAt, type FetchGithubLastCommitAt } from "../src/adoption/commit-freshness.ts";
@@ -30,8 +30,14 @@ class FakeRegistry implements Registry {
30
30
  }
31
31
  }
32
32
 
33
+ const roots: string[] = [];
34
+ afterEach(() => {
35
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
36
+ });
37
+
33
38
  function fixture(manifest: Record<string, unknown>, readme = ""): string {
34
39
  const root = mkdtempSync(join(tmpdir(), "packed-pack-"));
40
+ roots.push(root);
35
41
  writeFileSync(join(root, "package.json"), JSON.stringify(manifest));
36
42
  if (readme) writeFileSync(join(root, "README.md"), readme);
37
43
  return root;
@@ -1,5 +1,5 @@
1
1
  import { afterEach, describe, expect, it } from "bun:test";
2
- import { mkdtempSync } from "node:fs";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { AuthenticatedRpcClient } from "@danypops/vehicle-client/rpc-client";
@@ -17,6 +17,16 @@ import {
17
17
  } from "../src/pi/pi-version.ts";
18
18
  import type { VersionCommand } from "../src/publish/publish.ts";
19
19
 
20
+ const roots: string[] = [];
21
+ afterEach(() => {
22
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
23
+ });
24
+
25
+ function track(dir: string): string {
26
+ roots.push(dir);
27
+ return dir;
28
+ }
29
+
20
30
  class NoopRegistry implements Registry {
21
31
  async search(): Promise<SearchPage> {
22
32
  return { results: [], total: 0 };
@@ -288,8 +298,8 @@ describe("pi.status operation", () => {
288
298
  reg: new NoopRegistry(),
289
299
  inst: new NoopInstaller(),
290
300
  token: "test-token",
291
- stateDir: mkdtempSync(join(tmpdir(), "packed-pi-version-state-")),
292
- dataDir: mkdtempSync(join(tmpdir(), "packed-pi-version-data-")),
301
+ stateDir: track(mkdtempSync(join(tmpdir(), "packed-pi-version-state-"))),
302
+ dataDir: track(mkdtempSync(join(tmpdir(), "packed-pi-version-data-"))),
293
303
  piVersion: { check: async () => ({ current: "0.82.1", latest: "0.83.0", upToDate: false }) },
294
304
  });
295
305
  const client = new AuthenticatedRpcClient<OperationName, OperationInputs, OperationOutputs>("http://packed.test", "test-token", {
@@ -305,8 +315,8 @@ describe("pi.status operation", () => {
305
315
  reg: new NoopRegistry(),
306
316
  inst: new NoopInstaller(),
307
317
  token: "test-token",
308
- stateDir: mkdtempSync(join(tmpdir(), "packed-pi-version-state-")),
309
- dataDir: mkdtempSync(join(tmpdir(), "packed-pi-version-data-")),
318
+ stateDir: track(mkdtempSync(join(tmpdir(), "packed-pi-version-state-"))),
319
+ dataDir: track(mkdtempSync(join(tmpdir(), "packed-pi-version-data-"))),
310
320
  piVersion: { check: async () => ({}) },
311
321
  });
312
322
  const client = new AuthenticatedRpcClient<OperationName, OperationInputs, OperationOutputs>("http://packed.test", "test-token", {
@@ -1,9 +1,19 @@
1
- import { chmodSync, mkdtempSync, writeFileSync } from "node:fs";
1
+ import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
- import { describe, expect, it } from "bun:test";
4
+ import { afterEach, describe, expect, it } from "bun:test";
5
5
  import { ensureClient, resolvePiBinForSpawn } from "../src/public/client.ts";
6
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 track(dir: string): string {
13
+ roots.push(dir);
14
+ return dir;
15
+ }
16
+
7
17
  describe("ensureClient (packed daemon auto-spawn-or-wait decision)", () => {
8
18
  it("connects immediately without ever checking for a service or spawning, when already reachable", async () => {
9
19
  let spawnCalls = 0;
@@ -150,12 +160,12 @@ describe("resolvePiBinForSpawn", () => {
150
160
  });
151
161
 
152
162
  it("returns undefined when no PATH directory has an executable `pi`", () => {
153
- const dir = mkdtempSync(join(tmpdir(), "packed-resolve-pi-bin-"));
163
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-resolve-pi-bin-")));
154
164
  expect(resolvePiBinForSpawn({ PATH: dir })).toBeUndefined();
155
165
  });
156
166
 
157
167
  it("resolves the absolute path to an executable `pi` on PATH", () => {
158
- const dir = mkdtempSync(join(tmpdir(), "packed-resolve-pi-bin-"));
168
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-resolve-pi-bin-")));
159
169
  const piPath = join(dir, "pi");
160
170
  writeFileSync(piPath, "#!/bin/sh\necho pi\n");
161
171
  chmodSync(piPath, 0o755);
@@ -163,8 +173,8 @@ describe("resolvePiBinForSpawn", () => {
163
173
  });
164
174
 
165
175
  it("skips a non-executable `pi` earlier on PATH and resolves a later one", () => {
166
- const deadDir = mkdtempSync(join(tmpdir(), "packed-resolve-pi-bin-dead-"));
167
- const liveDir = mkdtempSync(join(tmpdir(), "packed-resolve-pi-bin-live-"));
176
+ const deadDir = track(mkdtempSync(join(tmpdir(), "packed-resolve-pi-bin-dead-")));
177
+ const liveDir = track(mkdtempSync(join(tmpdir(), "packed-resolve-pi-bin-live-")));
168
178
  writeFileSync(join(deadDir, "pi"), "not executable");
169
179
  chmodSync(join(deadDir, "pi"), 0o644);
170
180
  const livePi = join(liveDir, "pi");
@@ -1,4 +1,4 @@
1
- import { describe, expect, it } from "bun:test";
1
+ import { afterEach, describe, expect, it } from "bun:test";
2
2
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
@@ -32,8 +32,18 @@ class RegistryFixture implements Registry {
32
32
  }
33
33
  }
34
34
 
35
+ const roots: string[] = [];
36
+ afterEach(() => {
37
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
38
+ });
39
+
40
+ function track(dir: string): string {
41
+ roots.push(dir);
42
+ return dir;
43
+ }
44
+
35
45
  function project(overrides: Record<string, unknown> = {}): string {
36
- const root = mkdtempSync(join(tmpdir(), "packed-publish-"));
46
+ const root = track(mkdtempSync(join(tmpdir(), "packed-publish-")));
37
47
  writeFileSync(
38
48
  join(root, "package.json"),
39
49
  JSON.stringify({
@@ -84,7 +94,7 @@ class MultiRegistryFixture implements Registry {
84
94
  /** A two-package Bun workspace: packages/core (published, depended on) and
85
95
  * packages/ext (the one under test, declaring a dependency on core). */
86
96
  function workspace(extDependencyRange = "^1.0.0"): { root: string; corePath: string; extPath: string } {
87
- const root = mkdtempSync(join(tmpdir(), "packed-workspace-"));
97
+ const root = track(mkdtempSync(join(tmpdir(), "packed-workspace-")));
88
98
  writeFileSync(join(root, "package.json"), JSON.stringify({ name: "demo-workspace", private: true, workspaces: ["packages/*"] }));
89
99
  writeFileSync(join(root, "bun.lock"), "{}");
90
100
  const corePath = join(root, "packages", "core");
@@ -1,5 +1,5 @@
1
1
  import { afterAll, beforeAll, describe, expect, it } from "bun:test";
2
- import { mkdtempSync } from "node:fs";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import type { Server } from "bun";
@@ -86,6 +86,14 @@ describe("HttpRegistry vs DaemonRegistry", () => {
86
86
  let httpServer: Server<undefined>;
87
87
  let daemonServer: Server<undefined>;
88
88
  const daemonToken = "c".repeat(64);
89
+ // createApp() is invoked fresh on every request below, so this can grow
90
+ // past one entry -- every one of them still needs cleanup.
91
+ const stateDirs: string[] = [];
92
+ function trackedStateDir(): string {
93
+ const dir = mkdtempSync(join(tmpdir(), "packed-registry-contract-"));
94
+ stateDirs.push(dir);
95
+ return dir;
96
+ }
89
97
 
90
98
  beforeAll(() => {
91
99
  httpServer = Bun.serve({
@@ -110,13 +118,14 @@ describe("HttpRegistry vs DaemonRegistry", () => {
110
118
  reg: new InMemoryRegistry(),
111
119
  inst: new NoopInstaller(),
112
120
  token: daemonToken,
113
- stateDir: mkdtempSync(join(tmpdir(), "packed-registry-contract-")),
121
+ stateDir: trackedStateDir(),
114
122
  }).fetch(req),
115
123
  });
116
124
  });
117
125
  afterAll(() => {
118
126
  httpServer.stop(true);
119
127
  daemonServer.stop(true);
128
+ for (const dir of stateDirs.splice(0)) rmSync(dir, { recursive: true, force: true });
120
129
  });
121
130
 
122
131
  registryContract("HttpRegistry (real Bun.serve npm-mock)", () => new HttpRegistry(`http://127.0.0.1:${httpServer.port}`, 2, 0, 1_000));
@@ -12,6 +12,11 @@ afterEach(() => {
12
12
  for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
13
13
  });
14
14
 
15
+ function track(dir: string): string {
16
+ roots.push(dir);
17
+ return dir;
18
+ }
19
+
15
20
  function piHome(settingsPackages: unknown[]): string {
16
21
  const root = mkdtempSync(join(tmpdir(), "packed-resources-"));
17
22
  roots.push(root);
@@ -149,8 +154,8 @@ function rpcClient(piHome: string) {
149
154
  reg: new NoopRegistry(),
150
155
  inst: new NoopInstaller(),
151
156
  token: "test-token",
152
- stateDir: mkdtempSync(join(tmpdir(), "packed-resources-state-")),
153
- dataDir: mkdtempSync(join(tmpdir(), "packed-resources-data-")),
157
+ stateDir: track(mkdtempSync(join(tmpdir(), "packed-resources-state-"))),
158
+ dataDir: track(mkdtempSync(join(tmpdir(), "packed-resources-data-"))),
154
159
  piHome,
155
160
  });
156
161
  return new AuthenticatedRpcClient<OperationName, OperationInputs, OperationOutputs>("http://packed.test", "test-token", {
@@ -1,12 +1,22 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { mkdtempSync, writeFileSync } from "node:fs";
1
+ import { afterEach, describe, expect, it } from "bun:test";
2
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { PACKAGE_OPERATIONS, packagePermissionDecision, readSecuritySettings, writeSecuritySettings } from "../src/security/security.ts";
6
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 track(dir: string): string {
13
+ roots.push(dir);
14
+ return dir;
15
+ }
16
+
7
17
  describe("package permission policy", () => {
8
18
  it("defaults every arbitrary-code and settings/install-root mutation to approval", () => {
9
- const dir = mkdtempSync(join(tmpdir(), "packed-security-"));
19
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-security-")));
10
20
  const settings = readSecuritySettings(dir);
11
21
  expect(settings).toEqual({ mutationApproval: "always" });
12
22
  expect(PACKAGE_OPERATIONS).toEqual([
@@ -72,11 +82,11 @@ describe("package permission policy", () => {
72
82
  });
73
83
 
74
84
  it("persists an explicit unsafe opt-out and migrates the prior storage key", async () => {
75
- const dir = mkdtempSync(join(tmpdir(), "packed-security-"));
85
+ const dir = track(mkdtempSync(join(tmpdir(), "packed-security-")));
76
86
  expect(await writeSecuritySettings(dir, { mutationApproval: "never" })).toEqual({ mutationApproval: "never" });
77
87
  expect(readSecuritySettings(dir)).toEqual({ mutationApproval: "never" });
78
88
 
79
- const legacyDir = mkdtempSync(join(tmpdir(), "packed-security-legacy-"));
89
+ const legacyDir = track(mkdtempSync(join(tmpdir(), "packed-security-legacy-")));
80
90
  writeFileSync(join(legacyDir, "security.json"), '{"installApproval":"never"}\n');
81
91
  expect(readSecuritySettings(legacyDir)).toEqual({ mutationApproval: "never" });
82
92
  });
@@ -1,5 +1,5 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
1
+ import { afterEach, describe, expect, it } from "bun:test";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import type { ServiceSpec } from "@danypops/vehicle-server/service";
@@ -105,12 +105,22 @@ class FakeDaemonServiceInstaller implements DaemonServiceInstaller {
105
105
  }
106
106
  }
107
107
 
108
+ const roots: string[] = [];
109
+ afterEach(() => {
110
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
111
+ });
112
+
113
+ function track(dir: string): string {
114
+ roots.push(dir);
115
+ return dir;
116
+ }
117
+
108
118
  function deps(over: Partial<Deps> = {}): Deps {
109
119
  return {
110
120
  reg: new FakeRegistry(),
111
121
  inst: new FakeInstaller(),
112
122
  token: "test-token",
113
- stateDir: mkdtempSync(join(tmpdir(), "packed-")),
123
+ stateDir: track(mkdtempSync(join(tmpdir(), "packed-"))),
114
124
  ...over,
115
125
  };
116
126
  }
@@ -272,7 +282,7 @@ describe("service app", () => {
272
282
 
273
283
  it("POST /install configures an npm package's persistent Vehicle under the same approval", async () => {
274
284
  const svc = new FakeDaemonServiceInstaller();
275
- const piHome = mkdtempSync(join(tmpdir(), "packed-install-vehicle-"));
285
+ const piHome = track(mkdtempSync(join(tmpdir(), "packed-install-vehicle-")));
276
286
  const app = createApp(deps({ daemonServiceInstaller: svc, piHome }));
277
287
  const response = await app.fetch(
278
288
  new Request("http://x/install", {
@@ -328,7 +338,7 @@ describe("service app", () => {
328
338
 
329
339
  it("POST /install-service installs a real service once approved, reporting the resolved spec", async () => {
330
340
  const svc = new FakeDaemonServiceInstaller();
331
- const piHome = mkdtempSync(join(tmpdir(), "packed-pi-"));
341
+ const piHome = track(mkdtempSync(join(tmpdir(), "packed-pi-")));
332
342
  const app = createApp(deps({ daemonServiceInstaller: svc, piHome }));
333
343
 
334
344
  const res = await app.fetch(
@@ -406,7 +416,7 @@ describe("service app", () => {
406
416
 
407
417
  it("POST /restart-service restarts a real service once approved, reporting the resolved spec", async () => {
408
418
  const svc = new FakeDaemonServiceInstaller();
409
- const piHome = mkdtempSync(join(tmpdir(), "packed-pi-"));
419
+ const piHome = track(mkdtempSync(join(tmpdir(), "packed-pi-")));
410
420
  const app = createApp(deps({ daemonServiceInstaller: svc, piHome }));
411
421
 
412
422
  const res = await app.fetch(
@@ -492,7 +502,7 @@ describe("service app", () => {
492
502
 
493
503
  it("POST /update reconciles an installed Vehicle after a real package change", async () => {
494
504
  const svc = new FakeDaemonServiceInstaller();
495
- const piHome = mkdtempSync(join(tmpdir(), "packed-update-vehicle-"));
505
+ const piHome = track(mkdtempSync(join(tmpdir(), "packed-update-vehicle-")));
496
506
  const app = createApp(deps({ daemonServiceInstaller: svc, piHome }));
497
507
  const response = await app.fetch(
498
508
  new Request("http://x/update", {
@@ -542,7 +552,7 @@ describe("service app", () => {
542
552
  });
543
553
 
544
554
  it("GET /installed lists packages from pi settings", async () => {
545
- const piHome = mkdtempSync(join(tmpdir(), "packed-pi-"));
555
+ const piHome = track(mkdtempSync(join(tmpdir(), "packed-pi-")));
546
556
  writeFileSync(
547
557
  join(piHome, "settings.json"),
548
558
  JSON.stringify({ packages: ["npm:pi-extension-manager@0.8.2", { source: "npm:obj@2.0.0" }] }),
@@ -582,7 +592,7 @@ describe("service app", () => {
582
592
  });
583
593
 
584
594
  it("POST /remove removes declared Vehicle state before deleting the package", async () => {
585
- const piHome = mkdtempSync(join(tmpdir(), "packed-remove-vehicle-"));
595
+ const piHome = track(mkdtempSync(join(tmpdir(), "packed-remove-vehicle-")));
586
596
  const packageDir = join(piHome, "npm", "node_modules", "probe");
587
597
  mkdirSync(packageDir, { recursive: true });
588
598
  writeFileSync(
@@ -1,5 +1,5 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { afterEach, describe, expect, it } from "bun:test";
2
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import type { Installer, PkgInfo, Registry, SearchPage, UpdateOutcome } from "../src/packages/package.ts";
@@ -54,8 +54,18 @@ class GitFixture implements GitResolutionPort {
54
54
  }
55
55
  }
56
56
 
57
+ const roots: string[] = [];
58
+ afterEach(() => {
59
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
60
+ });
61
+
62
+ function track(dir: string): string {
63
+ roots.push(dir);
64
+ return dir;
65
+ }
66
+
57
67
  function piHome(withPackage = true): string {
58
- const root = mkdtempSync(join(tmpdir(), "packed-setup-home-"));
68
+ const root = track(mkdtempSync(join(tmpdir(), "packed-setup-home-")));
59
69
  const packages = withPackage ? ["npm:pi-demo"] : [];
60
70
  writeFileSync(join(root, "settings.json"), JSON.stringify({ packages }));
61
71
  if (withPackage) {
@@ -73,7 +83,7 @@ function piHome(withPackage = true): string {
73
83
  }
74
84
 
75
85
  function project(): string {
76
- const root = mkdtempSync(join(tmpdir(), "packed-setup-project-"));
86
+ const root = track(mkdtempSync(join(tmpdir(), "packed-setup-project-")));
77
87
  mkdirSync(join(root, ".pi"), { recursive: true });
78
88
  return root;
79
89
  }