@danypops/pi-packed 0.28.0 → 0.28.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"smoke.d.ts","sourceRoot":"","sources":["../../service/src/adoption/smoke.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,WAAW,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,GAAG,mBAAmB,GAAG,cAAc,GAAG,qBAAqB,CAAC;AAEpH,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,kBAAkB,CAAC;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AA2BD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAO7F;AA4ID,wBAAsB,iBAAiB,CACtC,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,MAAM,EACrB,OAAO,GAAE,YAAiB,GACxB,OAAO,CAAC,oBAAoB,CAAC,CAoF/B"}
1
+ {"version":3,"file":"smoke.d.ts","sourceRoot":"","sources":["../../service/src/adoption/smoke.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,WAAW,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,GAAG,mBAAmB,GAAG,cAAc,GAAG,qBAAqB,CAAC;AAEpH,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,kBAAkB,CAAC;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAuDD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAO7F;AAmJD,wBAAsB,iBAAiB,CACtC,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,MAAM,EACrB,OAAO,GAAE,YAAiB,GACxB,OAAO,CAAC,oBAAoB,CAAC,CAoF/B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-packed",
3
- "version": "0.28.0",
3
+ "version": "0.28.2",
4
4
  "description": "Pi package lifecycle, validation, daemon, tools, profiles, and TUI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,7 +42,8 @@
42
42
  "@danypops/pi-tui-harness": "^0.0.1",
43
43
  "@danypops/vehicle-client-pi": "^0.43.1",
44
44
  "@danypops/vehicle-conformance": "^0.4.0",
45
- "@types/semver": "^7.7.1"
45
+ "@types/semver": "^7.7.1",
46
+ "follow-redirects": "^1.16.0"
46
47
  },
47
48
  "peerDependencies": {
48
49
  "@danypops/vehicle-client-pi": "^0.43.1",
@@ -99,7 +99,16 @@ async function main(): Promise<void> {
99
99
  const extensionPath = process.argv[2];
100
100
  if (!extensionPath) throw new Error("extension path is required");
101
101
  try {
102
- const jiti = createJiti(import.meta.url, { interopDefault: true, tryNative: false });
102
+ // tryNative matches loader.ts's own real, non-Bun-binary Pi extension loading convention:
103
+ // prefer Bun's native TS/ESM transform, falling back to jiti's own Babel transform only for
104
+ // what native genuinely can't handle. Confirmed live: tryNative:false forced EVERY
105
+ // transitive dependency (including plain CJS libraries that never needed a Babel transform,
106
+ // e.g. follow-redirects) through jiti's Babel path -- which has its own real, reproducible
107
+ // bug interacting with a library that calls Error.captureStackTrace on a not-yet-initialized
108
+ // Error instance during module-level prototype setup (confirmed outside this sandbox too,
109
+ // given a cold jiti transform cache), surfacing as an opaque "First argument must be an
110
+ // Error object" crash unrelated to the extension's own code.
111
+ const jiti = createJiti(import.meta.url, { interopDefault: true, tryNative: true });
103
112
  const factory = (await jiti.import(extensionPath, { default: true })) as unknown;
104
113
  if (typeof factory !== "function") throw new Error("extension has no default factory export");
105
114
  await factory(api);
@@ -54,6 +54,34 @@ function addReadOnlyBind(args: string[], path: string): void {
54
54
  if (existsSync(path)) args.push("--ro-bind", path, path);
55
55
  }
56
56
 
57
+ const MAX_ANCESTOR_WALK = 32;
58
+
59
+ /**
60
+ * Every ancestor directory's own node_modules from packageRoot's parent up to
61
+ * the filesystem root -- mirrors Node's real module-resolution walk
62
+ * (require.resolve checks <dir>/node_modules at every level up to /).
63
+ * Confirmed live: packed doctor's own widget reported real extensions
64
+ * (pi-lector, pi-pipes, pi-tickets, pi-jittor, pi-web-spider) as "crash" on a
65
+ * plain Cannot-find-module for a sibling dependency npm/bun hoisted to a
66
+ * shared ancestor node_modules, though Pi's own real, unsandboxed process
67
+ * resolves the exact same import fine -- the sandbox only ever bound the
68
+ * package's own root, never the hoisted siblings one or more levels above it.
69
+ * Bounded to MAX_ANCESTOR_WALK levels as a defensive cap; a real filesystem
70
+ * hierarchy never gets remotely close to that before reaching /.
71
+ */
72
+ function collectAncestorNodeModulesDirs(packageRoot: string): string[] {
73
+ const dirs: string[] = [];
74
+ let current = dirname(packageRoot);
75
+ for (let i = 0; i < MAX_ANCESTOR_WALK; i++) {
76
+ const candidate = join(current, "node_modules");
77
+ if (existsSync(candidate)) dirs.push(realpathSync(candidate));
78
+ const parent = dirname(current);
79
+ if (parent === current) break;
80
+ current = parent;
81
+ }
82
+ return dirs;
83
+ }
84
+
57
85
  /**
58
86
  * Resolves an installed dependency's real on-disk package directory via
59
87
  * Node's own module-resolution algorithm (require.resolve against its
@@ -85,7 +113,6 @@ function sandboxCommand(packageRoot: string, extensionPath: string, maxProcesses
85
113
  const jitiModules = resolveDependencyModulesDir(packageDirectory, "jiti");
86
114
  if (!jitiModules) return undefined;
87
115
  const bun = realpathSync(process.execPath);
88
- const relativeExtension = relative(packageRoot, extensionPath).split(sep).join("/");
89
116
  const args = [
90
117
  "/usr/bin/bwrap",
91
118
  "--unshare-user",
@@ -116,9 +143,6 @@ function sandboxCommand(packageRoot: string, extensionPath: string, maxProcesses
116
143
  jitiModules,
117
144
  "/runner/node_modules/jiti",
118
145
  "--ro-bind",
119
- packageRoot,
120
- "/package",
121
- "--ro-bind",
122
146
  bun,
123
147
  "/bun",
124
148
  "--dev",
@@ -131,6 +155,17 @@ function sandboxCommand(packageRoot: string, extensionPath: string, maxProcesses
131
155
  "/tmp",
132
156
  "--dir",
133
157
  "/tmp/home",
158
+ );
159
+ // Package-related identity binds go AFTER the virtual/tmpfs mounts above --
160
+ // bwrap applies mounts in argument order, so a bind whose real path happens
161
+ // to sit under one of those special mounts (e.g. a test fixture under
162
+ // /tmp, which --tmpfs /tmp would otherwise shadow) must come later to win.
163
+ addReadOnlyBind(args, packageRoot);
164
+ // Real-path identity binds so a hoisted sibling dependency resolves exactly
165
+ // as it would in Pi's own real, unsandboxed process -- see
166
+ // collectAncestorNodeModulesDirs's own doc comment for why this is needed.
167
+ for (const dir of collectAncestorNodeModulesDirs(packageRoot)) addReadOnlyBind(args, dir);
168
+ args.push(
134
169
  "--remount-ro",
135
170
  "/",
136
171
  "--clearenv",
@@ -147,7 +182,7 @@ function sandboxCommand(packageRoot: string, extensionPath: string, maxProcesses
147
182
  "NODE_PATH",
148
183
  "/runner/node_modules",
149
184
  "--chdir",
150
- "/package",
185
+ packageRoot,
151
186
  "--",
152
187
  "/usr/bin/prlimit",
153
188
  `--nproc=${maxProcesses}`,
@@ -158,7 +193,7 @@ function sandboxCommand(packageRoot: string, extensionPath: string, maxProcesses
158
193
  "--",
159
194
  "/bun",
160
195
  "/runner/src/smoke-child.ts",
161
- `/package/${relativeExtension}`,
196
+ extensionPath,
162
197
  );
163
198
  return args;
164
199
  }
@@ -1,8 +1,9 @@
1
1
  import { afterEach, describe, expect, it } from "bun:test";
2
2
  import { spawnSync } from "node:child_process";
3
- import { existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
3
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
- import { join } from "node:path";
5
+ import { dirname, join } from "node:path";
6
+ import { createRequire } from "node:module";
6
7
  import { checkPackage } from "../src/adoption/check.ts";
7
8
  import { resolveDependencyModulesDir, runExtensionSmoke } from "../src/adoption/smoke.ts";
8
9
 
@@ -151,6 +152,67 @@ describeIfSandboxed("isolated extension smoke runner", () => {
151
152
  expect((await runExtensionSmoke(output.root, output.path, { maxOutputBytes: 4_096 })).status).toBe("output-limit");
152
153
  });
153
154
 
155
+ it("resolves a dependency hoisted to a shared ancestor node_modules -- confirmed live bug: packed doctor's own widget reported 5/8 real extensions as crashing on a plain 'Cannot find module' for a sibling dependency npm/bun hoisted above the package's own node_modules, though Pi's own real, unsandboxed process resolves the exact same import fine", async () => {
156
+ const workspaceRoot = mkdtempSync(join(tmpdir(), "packed-smoke-hoisted-"));
157
+ roots.push(workspaceRoot);
158
+ const sharedNodeModules = join(workspaceRoot, "node_modules");
159
+ writeFakeDependency(sharedNodeModules, "hoisted-sibling");
160
+ const packageDir = join(sharedNodeModules, "@fixture", "pi-smoke-hoisted");
161
+ mkdirSync(join(packageDir, "extension"), { recursive: true });
162
+ const extensionPath = join(packageDir, "extension", "index.ts");
163
+ writeFileSync(
164
+ extensionPath,
165
+ 'import pkg from "hoisted-sibling/package.json" with { type: "json" };\nexport default function (pi: any) { pi.registerCommand(pkg.name, { handler() {} }); }\n',
166
+ );
167
+ writeFileSync(
168
+ join(packageDir, "package.json"),
169
+ JSON.stringify({
170
+ name: "@fixture/pi-smoke-hoisted",
171
+ version: "1.0.0",
172
+ keywords: ["pi-package"],
173
+ files: ["extension"],
174
+ pi: { extensions: ["extension/index.ts"] },
175
+ }),
176
+ );
177
+
178
+ const result = await runExtensionSmoke(packageDir, extensionPath);
179
+
180
+ expect(result.status).toBe("ok");
181
+ expect(result.registrations.commands).toEqual(["hoisted-sibling"]);
182
+ });
183
+
184
+ it("never routes a plain CJS dependency through jiti's own Babel transform path -- confirmed live: forcing every transitive dependency through Babel (tryNative:false) hit a real, reproducible upstream bug in jiti's own Babel-installed Error.prepareStackTrace hook, triggered by follow-redirects' own module-level Error.captureStackTrace call during its chained error-type prototype setup, surfacing as an opaque 'First argument must be an Error object' crash unrelated to the scanned extension's own code -- reproduces with a cold jiti transform cache regardless of this sandbox specifically, so a real npm package (not a synthetic fixture) is the only faithful way to prove it stays fixed", async () => {
185
+ const require = createRequire(import.meta.url);
186
+ const followRedirectsDir = dirname(require.resolve("follow-redirects/package.json"));
187
+
188
+ const workspaceRoot = mkdtempSync(join(tmpdir(), "packed-smoke-follow-redirects-"));
189
+ roots.push(workspaceRoot);
190
+ const sharedNodeModules = join(workspaceRoot, "node_modules");
191
+ cpSync(followRedirectsDir, join(sharedNodeModules, "follow-redirects"), { recursive: true });
192
+ const packageDir = join(sharedNodeModules, "@fixture", "pi-smoke-follow-redirects");
193
+ mkdirSync(join(packageDir, "extension"), { recursive: true });
194
+ const extensionPath = join(packageDir, "extension", "index.ts");
195
+ writeFileSync(
196
+ extensionPath,
197
+ 'import { http } from "follow-redirects";\nexport default function (pi: any) { pi.registerCommand(String(typeof http), { handler() {} }); }\n',
198
+ );
199
+ writeFileSync(
200
+ join(packageDir, "package.json"),
201
+ JSON.stringify({
202
+ name: "@fixture/pi-smoke-follow-redirects",
203
+ version: "1.0.0",
204
+ keywords: ["pi-package"],
205
+ files: ["extension"],
206
+ pi: { extensions: ["extension/index.ts"] },
207
+ }),
208
+ );
209
+
210
+ const result = await runExtensionSmoke(packageDir, extensionPath);
211
+
212
+ expect(result.status).toBe("ok");
213
+ expect(result.registrations.commands).toEqual(["object"]);
214
+ });
215
+
154
216
  it("keeps default package checks static and adds smoke results only when requested", async () => {
155
217
  const fixture = extension(
156
218
  'import { writeFileSync } from "node:fs"; export default function (pi: any) { writeFileSync("executed", "yes"); pi.registerCommand("x", { handler() {} }); }',