@danypops/pi-packed 0.21.10 → 0.21.11
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
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* for ExecInstaller.install(), so a package that crashes at registration
|
|
4
4
|
* time never gets fully wired into ~/.pi/npm and ~/.pi/agent in the first
|
|
5
5
|
* place. Stages the real npm tarball into a throwaway temp dir (never the
|
|
6
|
-
* live piHome),
|
|
6
|
+
* live piHome), installs its own declared dependencies there, then runs
|
|
7
|
+
* @danypops/pi-extension-harness's own mock-pi-cli
|
|
7
8
|
* subprocess -- a real, isolated process exercising the same production
|
|
8
9
|
* jiti load path Pi's own binary uses -- against every declared
|
|
9
10
|
* pi.extensions entry.
|
|
@@ -33,6 +34,7 @@ export interface InstallValidator {
|
|
|
33
34
|
const DEFAULT_LOAD_TIMEOUT_MS = 8_000;
|
|
34
35
|
const MAX_LOAD_TIMEOUT_MS = 30_000;
|
|
35
36
|
const PACK_TIMEOUT_MS = 30_000;
|
|
37
|
+
const INSTALL_TIMEOUT_MS = 60_000;
|
|
36
38
|
const MAX_EXTENSIONS = 20;
|
|
37
39
|
|
|
38
40
|
function bounded(value: number | undefined, fallback: number, maximum: number): number {
|
|
@@ -96,6 +98,19 @@ async function stageNpmTarball(spec: string, stageDir: string): Promise<StageRes
|
|
|
96
98
|
return { ok: true, root };
|
|
97
99
|
}
|
|
98
100
|
|
|
101
|
+
/** Installs the staged tarball's own declared dependencies -- npm pack +
|
|
102
|
+
* tar only extract the package's own files, never fetch what its
|
|
103
|
+
* package.json requires. --ignore-scripts keeps an untrusted candidate's
|
|
104
|
+
* lifecycle scripts from running during a headless check nobody explicitly
|
|
105
|
+
* approved; --omit=dev matches what the entry point actually needs at
|
|
106
|
+
* runtime (jiti loads its TS source directly, no separate build step). */
|
|
107
|
+
async function installStagedDependencies(root: string): Promise<{ ok: true } | { ok: false; message: string }> {
|
|
108
|
+
const install = await runCommand(["npm", "install", "--ignore-scripts", "--omit=dev", "--no-audit", "--no-fund"], root, INSTALL_TIMEOUT_MS);
|
|
109
|
+
if (install.timedOut) return { ok: false, message: `npm install exceeded ${INSTALL_TIMEOUT_MS}ms installing the staged package's own dependencies` };
|
|
110
|
+
if (install.code !== 0) return { ok: false, message: (install.stderr.trim() || `npm install exited ${install.code}`).slice(0, 2_000) };
|
|
111
|
+
return { ok: true };
|
|
112
|
+
}
|
|
113
|
+
|
|
99
114
|
/** Runs pi-extension-harness's mock-pi-cli against one extension entry
|
|
100
115
|
* point, in its own load-only mode (--tool omitted): a real, isolated
|
|
101
116
|
* subprocess exercising Pi's own production jiti load path. */
|
|
@@ -165,6 +180,9 @@ export class HeadlessInstallValidator implements InstallValidator {
|
|
|
165
180
|
: [];
|
|
166
181
|
if (declared.length === 0) return { ok: true, source, extensions: [] };
|
|
167
182
|
|
|
183
|
+
const installed = await installStagedDependencies(staged.root);
|
|
184
|
+
if (!installed.ok) return { ok: false, source, extensions: [], message: installed.message };
|
|
185
|
+
|
|
168
186
|
const extensions: ExtensionLoadResult[] = [];
|
|
169
187
|
for (const entry of declared) {
|
|
170
188
|
const entryPath = resolve(staged.root, entry);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* pass.
|
|
9
9
|
*/
|
|
10
10
|
import { describe, expect, it } from "bun:test";
|
|
11
|
-
import { mkdirSync, mkdtempSync } from "node:fs";
|
|
11
|
+
import { mkdirSync, mkdtempSync, 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";
|
|
@@ -21,6 +21,39 @@ const FIXTURES = join(__dirname, "fixtures/install-validation");
|
|
|
21
21
|
const HEALTHY = join(FIXTURES, "healthy-package");
|
|
22
22
|
const BROKEN = join(FIXTURES, "broken-package");
|
|
23
23
|
const NO_MANIFEST = join(FIXTURES, "no-manifest-package");
|
|
24
|
+
const DEP_PACKAGE = join(FIXTURES, "dep-package");
|
|
25
|
+
|
|
26
|
+
/** Writes a fresh package into `dir` whose extension entry point has a real
|
|
27
|
+
* runtime import (`packed-fixture-dep`, a `file:` dependency resolvable
|
|
28
|
+
* fully offline) -- otherwise healthy, structurally identical to HEALTHY,
|
|
29
|
+
* except its one declared dependency must actually be installed into the
|
|
30
|
+
* staged tarball for its entry point to load at all. The file: target is
|
|
31
|
+
* an absolute path computed at test time (checkout-independent), so this
|
|
32
|
+
* can't be a static checked-in fixture the way HEALTHY/BROKEN are. */
|
|
33
|
+
function writePackageWithRealDependency(dir: string): void {
|
|
34
|
+
mkdirSync(join(dir, "extension"), { recursive: true });
|
|
35
|
+
writeFileSync(
|
|
36
|
+
join(dir, "package.json"),
|
|
37
|
+
JSON.stringify({
|
|
38
|
+
name: "packed-install-validation-fixture-with-dependency",
|
|
39
|
+
version: "1.0.0",
|
|
40
|
+
private: true,
|
|
41
|
+
pi: { extensions: ["extension/index.ts"] },
|
|
42
|
+
dependencies: { "packed-fixture-dep": `file:${DEP_PACKAGE}` },
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
writeFileSync(
|
|
46
|
+
join(dir, "extension/index.ts"),
|
|
47
|
+
[
|
|
48
|
+
'import { greet } from "packed-fixture-dep";',
|
|
49
|
+
"",
|
|
50
|
+
"export default function withDependencyFixtureExtension(pi: { registerCommand: (name: string, def: unknown) => void }) {",
|
|
51
|
+
'\tpi.registerCommand("with-dependency-fixture", { description: greet(), handler: async () => {} });',
|
|
52
|
+
"}",
|
|
53
|
+
"",
|
|
54
|
+
].join("\n"),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
24
57
|
|
|
25
58
|
describe("bareNpmSpec", () => {
|
|
26
59
|
it("strips the npm: scheme", () => {
|
|
@@ -77,6 +110,31 @@ describe("HeadlessInstallValidator (real npm pack + tar extraction, no mocking)"
|
|
|
77
110
|
}, 20_000);
|
|
78
111
|
});
|
|
79
112
|
|
|
113
|
+
describe("HeadlessInstallValidator (bug repro, packed-headlessinstallvalidator-never-installs-the): staged tarball's own declared dependencies are never installed before the load check", () => {
|
|
114
|
+
it("approves an otherwise-healthy package whose entry point needs its one declared file: dependency", async () => {
|
|
115
|
+
const dir = mkdtempSync(join(tmpdir(), "packed-install-validation-dep-fixture-"));
|
|
116
|
+
writePackageWithRealDependency(dir);
|
|
117
|
+
|
|
118
|
+
// Ground truth this isn't a broken fixture: a plain `bun install` in an
|
|
119
|
+
// identical staged copy resolves and loads it fine (mirrors the real
|
|
120
|
+
// @danypops/pi-pipes repro: `npm pack` + `tar -xzf` + `bun install`
|
|
121
|
+
// succeeds outside of HeadlessInstallValidator).
|
|
122
|
+
const install = await Bun.spawn(["bun", "install", "--no-save"], { cwd: dir, stdout: "pipe", stderr: "pipe" }).exited;
|
|
123
|
+
expect(install).toBe(0);
|
|
124
|
+
const directLoad = await validateExtensionLoadsHeadless(join(dir, "extension/index.ts"));
|
|
125
|
+
expect(directLoad.ok).toBe(true);
|
|
126
|
+
|
|
127
|
+
// The real bug: HeadlessInstallValidator stages its own fresh copy via
|
|
128
|
+
// npm pack + tar (never running bun/npm install in that copy), so the
|
|
129
|
+
// same otherwise-healthy package fails the load check purely because
|
|
130
|
+
// its declared dependency was never installed into *that* copy.
|
|
131
|
+
const validator = new HeadlessInstallValidator();
|
|
132
|
+
const result = await validator.validate(`npm:${dir}`);
|
|
133
|
+
expect(result.ok).toBe(true);
|
|
134
|
+
expect(result.extensions).toEqual([{ path: "extension/index.ts", ok: true }]);
|
|
135
|
+
}, 30_000);
|
|
136
|
+
});
|
|
137
|
+
|
|
80
138
|
describe("ExecInstaller.install() -- refuses before ever spawning the real pi binary", () => {
|
|
81
139
|
function fakeValidator(result: InstallValidationResult): InstallValidator {
|
|
82
140
|
return { validate: async () => result };
|