@danypops/pi-packed 0.21.11 → 0.21.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-packed",
3
- "version": "0.21.11",
3
+ "version": "0.21.12",
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
- extensions.push({ ...loadResult, path: entry });
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
+ }
@@ -106,7 +106,40 @@ describe("HeadlessInstallValidator (real npm pack + tar extraction, no mocking)"
106
106
  const validator = new HeadlessInstallValidator();
107
107
  const result = await validator.validate(`npm:${HEALTHY}`);
108
108
  expect(result.ok).toBe(true);
109
- expect(result.extensions).toEqual([{ path: "extension/index.ts", ok: true }]);
109
+ expect(result.extensions).toHaveLength(1);
110
+ expect(result.extensions[0]).toMatchObject({ path: "extension/index.ts", ok: true });
111
+ }, 20_000);
112
+ });
113
+
114
+ describe("HeadlessInstallValidator (vehicle-client-pi pi-load-harness, non-gating diagnostic)", () => {
115
+ it("attaches all-three-path evidence for a real healthy package without changing its ok:true verdict", async () => {
116
+ const validator = new HeadlessInstallValidator();
117
+ const result = await validator.validate(`npm:${HEALTHY}`);
118
+ expect(result.ok).toBe(true);
119
+ expect(result.extensions).toHaveLength(1);
120
+ const paths = result.extensions[0]?.additionalLoadPaths;
121
+ expect(paths).toBeDefined();
122
+ expect(paths?.map((p) => p.path).sort()).toEqual(["jiti-try-native-false", "jiti-try-native-true", "native-esm"]);
123
+ expect(paths?.every((p) => p.ok)).toBe(true);
124
+ }, 20_000);
125
+
126
+ it("reports the broken fixture's additional paths as ok:true -- documents a real, deliberate difference in what's checked, not a bug", async () => {
127
+ // verifyLoadableUnderPi only checks that *importing* the module succeeds;
128
+ // it never calls the extension's exported factory the way mock-pi-cli's
129
+ // gating check does. BROKEN's factory only throws once invoked, so
130
+ // merely importing it succeeds on every path -- confirmed directly
131
+ // against the real child process, not assumed. The two checks answer
132
+ // genuinely different questions (import-time vs. factory-execution-time
133
+ // failure) and are complementary for exactly that reason; this test
134
+ // exists so a future change that makes them silently agree (e.g. an
135
+ // accidental factory invocation creeping into the load-path check)
136
+ // gets caught as a real behavior change.
137
+ const validator = new HeadlessInstallValidator();
138
+ const result = await validator.validate(`npm:${BROKEN}`);
139
+ expect(result.ok).toBe(false); // the gating check still refuses it
140
+ const paths = result.extensions[0]?.additionalLoadPaths;
141
+ expect(paths).toBeDefined();
142
+ expect(paths?.every((p) => p.ok === true)).toBe(true);
110
143
  }, 20_000);
111
144
  });
112
145
 
@@ -131,7 +164,8 @@ describe("HeadlessInstallValidator (bug repro, packed-headlessinstallvalidator-n
131
164
  const validator = new HeadlessInstallValidator();
132
165
  const result = await validator.validate(`npm:${dir}`);
133
166
  expect(result.ok).toBe(true);
134
- expect(result.extensions).toEqual([{ path: "extension/index.ts", ok: true }]);
167
+ expect(result.extensions).toHaveLength(1);
168
+ expect(result.extensions[0]).toMatchObject({ path: "extension/index.ts", ok: true });
135
169
  }, 30_000);
136
170
  });
137
171