@danypops/pi-packed 0.21.11 → 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.
- package/extension/src/tui.ts +124 -33
- package/package.json +2 -2
- package/service/src/adoption/install-validation.ts +48 -2
- package/service/src/adoption/verify-load-paths-child.mjs +55 -0
- package/service/test/advisories.test.ts +17 -9
- package/service/test/cleanup.test.ts +7 -2
- package/service/test/cli.test.ts +29 -13
- package/service/test/db.test.ts +8 -2
- package/service/test/doctor.test.ts +7 -2
- package/service/test/domain.test.ts +22 -12
- package/service/test/index.test.ts +22 -9
- package/service/test/install-validation.test.ts +50 -6
- package/service/test/install.test.ts +20 -10
- package/service/test/pack-score.test.ts +8 -2
- package/service/test/pi-version.test.ts +15 -5
- package/service/test/public-client.test.ts +16 -6
- package/service/test/publish.test.ts +13 -3
- package/service/test/registry-contract.test.ts +11 -2
- package/service/test/resources.test.ts +7 -2
- package/service/test/security.test.ts +15 -5
- package/service/test/service.test.ts +19 -9
- package/service/test/setup.test.ts +14 -4
package/extension/src/tui.ts
CHANGED
|
@@ -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
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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
|
-
//
|
|
513
|
-
//
|
|
514
|
-
//
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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.
|
|
3
|
+
"version": "0.21.13",
|
|
4
4
|
"description": "Pi package lifecycle, validation, daemon, tools, profiles, and TUI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"@danypops/packed": "^0.7.0",
|
|
32
32
|
"@danypops/pi-extension-harness": "^0.2.0",
|
|
33
33
|
"@danypops/vehicle-client": "^0.5.2",
|
|
34
|
+
"@danypops/vehicle-client-pi": "^0.16.9",
|
|
34
35
|
"@danypops/vehicle-core": "^0.12.3",
|
|
35
36
|
"@danypops/vehicle-server": "^0.17.1",
|
|
36
37
|
"jiti": "^2.7.0",
|
|
@@ -40,7 +41,6 @@
|
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
43
|
"@danypops/pi-tui-harness": "^0.0.1",
|
|
43
|
-
"@danypops/vehicle-client-pi": "^0.16.2",
|
|
44
44
|
"@types/semver": "^7.7.1"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
* @danypops/pi-extension-harness's own mock-pi-cli
|
|
8
8
|
* subprocess -- a real, isolated process exercising the same production
|
|
9
9
|
* jiti load path Pi's own binary uses -- against every declared
|
|
10
|
-
* pi.extensions entry.
|
|
10
|
+
* pi.extensions entry. Also runs @danypops/vehicle-client-pi's
|
|
11
|
+
* pi-load-harness (native ESM, jiti tryNative:false) in its own isolated
|
|
12
|
+
* subprocess as non-gating, observational evidence alongside that gating
|
|
13
|
+
* check -- see ExtensionLoadResult.additionalLoadPaths.
|
|
11
14
|
*/
|
|
12
15
|
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
13
16
|
import { createRequire } from "node:module";
|
|
@@ -18,6 +21,26 @@ export interface ExtensionLoadResult {
|
|
|
18
21
|
path: string;
|
|
19
22
|
ok: boolean;
|
|
20
23
|
message?: string;
|
|
24
|
+
/** Non-gating: @danypops/vehicle-client-pi's pi-load-harness checks two
|
|
25
|
+
* further Pi extension load paths (native ESM, jiti tryNative:false)
|
|
26
|
+
* beyond the one path above (jiti tryNative:true) already gates install.
|
|
27
|
+
* Surfaced as observational evidence only, for two independent reasons:
|
|
28
|
+
* (1) native-esm can legitimately fail for a perfectly loadable extension
|
|
29
|
+
* on an older Node without TS type-stripping support, with no real-world
|
|
30
|
+
* signal yet on how often that's a false alarm; (2) this check only
|
|
31
|
+
* verifies the module *imports* cleanly -- unlike the gating check above,
|
|
32
|
+
* it never calls the extension's exported factory, so it cannot catch a
|
|
33
|
+
* factory that throws once actually registered (confirmed directly: the
|
|
34
|
+
* BROKEN test fixture, whose factory always throws, reports ok:true on
|
|
35
|
+
* every one of these paths). Complementary evidence for an import-time
|
|
36
|
+
* failure class, not a broader replacement for the gating check. */
|
|
37
|
+
additionalLoadPaths?: PiLoadPathResult[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface PiLoadPathResult {
|
|
41
|
+
path: "native-esm" | "jiti-try-native-false" | "jiti-try-native-true";
|
|
42
|
+
ok: boolean;
|
|
43
|
+
error?: string;
|
|
21
44
|
}
|
|
22
45
|
|
|
23
46
|
export interface InstallValidationResult {
|
|
@@ -151,6 +174,28 @@ export async function validateExtensionLoadsHeadless(entryPath: string, timeoutM
|
|
|
151
174
|
};
|
|
152
175
|
}
|
|
153
176
|
|
|
177
|
+
/** Runs the extra two Pi extension load paths @danypops/vehicle-client-pi's
|
|
178
|
+
* pi-load-harness knows about (native ESM, jiti tryNative:false) against
|
|
179
|
+
* one entry point, in their own isolated subprocess -- same trust boundary
|
|
180
|
+
* as validateExtensionLoadsHeadless, never inside the daemon process.
|
|
181
|
+
* Returns undefined (not a failure) when the probe subprocess itself
|
|
182
|
+
* couldn't run at all -- vehicle-client-pi is an optional enrichment here,
|
|
183
|
+
* not a hard requirement the way pi-extension-harness is. */
|
|
184
|
+
async function verifyAllLoadPathsHeadless(entryPath: string, timeoutMs?: number): Promise<PiLoadPathResult[] | undefined> {
|
|
185
|
+
const bound = bounded(timeoutMs, DEFAULT_LOAD_TIMEOUT_MS, MAX_LOAD_TIMEOUT_MS);
|
|
186
|
+
const childPath = new URL("verify-load-paths-child.mjs", import.meta.url).pathname;
|
|
187
|
+
const result = await runCommand(["node", childPath, "--extension", entryPath], tmpdir(), bound);
|
|
188
|
+
if (result.timedOut) return undefined;
|
|
189
|
+
const lastLine = result.stdout.trim().split("\n").at(-1);
|
|
190
|
+
if (!lastLine) return undefined;
|
|
191
|
+
try {
|
|
192
|
+
const parsed = JSON.parse(lastLine) as { results?: PiLoadPathResult[]; error?: string };
|
|
193
|
+
return Array.isArray(parsed.results) ? parsed.results : undefined;
|
|
194
|
+
} catch {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
154
199
|
/** Stages the real npm tarball in isolation and headlessly load-checks
|
|
155
200
|
* every pi.extensions entry it declares. A package with no pi.extensions
|
|
156
201
|
* (most npm packages) or a non-npm source has nothing to validate and
|
|
@@ -194,7 +239,8 @@ export class HeadlessInstallValidator implements InstallValidator {
|
|
|
194
239
|
// path into a throwaway temp stage dir -- meaningful to a caller,
|
|
195
240
|
// matches what package.json itself says.
|
|
196
241
|
const loadResult = await validateExtensionLoadsHeadless(entryPath, this.timeoutMs);
|
|
197
|
-
|
|
242
|
+
const additionalLoadPaths = await verifyAllLoadPathsHeadless(entryPath, this.timeoutMs);
|
|
243
|
+
extensions.push({ ...loadResult, path: entry, ...(additionalLoadPaths ? { additionalLoadPaths } : {}) });
|
|
198
244
|
}
|
|
199
245
|
const ok = extensions.every((extension) => extension.ok);
|
|
200
246
|
return { ok, source, extensions, ...(ok ? {} : { message: "one or more declared extensions failed a headless load check" }) };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* verify-load-paths-child.mjs — spawned in isolation by install-validation.ts
|
|
5
|
+
* to check a staged candidate's extension entry against every Pi extension
|
|
6
|
+
* load path @danypops/vehicle-client-pi's pi-load-harness knows about
|
|
7
|
+
* (native ESM, jiti tryNative:false, jiti tryNative:true) -- not just the
|
|
8
|
+
* single tryNative:true path mock-pi-cli's own headless check exercises.
|
|
9
|
+
*
|
|
10
|
+
* Runs as its own subprocess, same trust boundary as mock-pi-cli: the
|
|
11
|
+
* candidate's real code executes here, never inside the trusted daemon
|
|
12
|
+
* process. Uses jiti itself (the same technique mock-pi-cli.mjs already
|
|
13
|
+
* relies on) to import pi-load-harness's raw TypeScript source -- that
|
|
14
|
+
* package deliberately ships this subpath uncompiled, since its intended
|
|
15
|
+
* consumers already run through a TS-transforming toolchain.
|
|
16
|
+
*
|
|
17
|
+
* Accepts:
|
|
18
|
+
* --extension <path> candidate extension entry point to check
|
|
19
|
+
*
|
|
20
|
+
* Emits exactly one JSON line on stdout, then exits 0 (a probe failure is
|
|
21
|
+
* data, not a process failure -- the caller decides what a failing path
|
|
22
|
+
* means):
|
|
23
|
+
* { "results": [{ "path": "native-esm", "ok": true }, ...] }
|
|
24
|
+
* or, if the harness itself couldn't even be loaded/invoked:
|
|
25
|
+
* { "error": "..." }
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { createJiti } from "jiti";
|
|
29
|
+
|
|
30
|
+
const args = process.argv.slice(2);
|
|
31
|
+
const get = (flag) => {
|
|
32
|
+
const i = args.indexOf(flag);
|
|
33
|
+
return i !== -1 ? args[i + 1] : null;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const extensionPath = get("--extension");
|
|
37
|
+
if (!extensionPath) {
|
|
38
|
+
process.stderr.write("--extension required\n");
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function emit(obj) {
|
|
43
|
+
process.stdout.write(`${JSON.stringify(obj)}\n`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const jiti = createJiti(import.meta.url, { moduleCache: false });
|
|
48
|
+
const { verifyLoadableUnderPi } = await jiti.import("@danypops/vehicle-client-pi/pi-load-harness");
|
|
49
|
+
const results = await verifyLoadableUnderPi(extensionPath);
|
|
50
|
+
emit({ results });
|
|
51
|
+
process.exit(0);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
emit({ error: err?.message ?? String(err) });
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
@@ -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", {
|
package/service/test/cli.test.ts
CHANGED
|
@@ -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:
|
|
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(() =>
|
|
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
|
});
|
package/service/test/db.test.ts
CHANGED
|
@@ -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", {
|