@danypops/pi-packed 0.21.9 → 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/dist/client.d.ts +7 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1 -1
- package/package.json +1 -1
- package/service/src/adoption/install-validation.ts +19 -1
- package/service/src/public/client.ts +32 -3
- package/service/test/fixtures/install-validation/dep-package/index.js +3 -0
- package/service/test/fixtures/install-validation/dep-package/package.json +7 -0
- package/service/test/install-validation.test.ts +59 -1
- package/service/test/public-client.test.ts +42 -1
package/dist/client.d.ts
CHANGED
|
@@ -105,5 +105,12 @@ export interface EnsureClientDeps {
|
|
|
105
105
|
* creating a competing one.
|
|
106
106
|
*/
|
|
107
107
|
export declare function ensureClient(deps: EnsureClientDeps): Promise<PackedClient>;
|
|
108
|
+
/**
|
|
109
|
+
* An auto-spawned daemon is detached and long-lived, so it keeps whatever
|
|
110
|
+
* PATH its host process had at spawn time forever. Resolve `pi`'s absolute
|
|
111
|
+
* path now and pin it, rather than trust a bare "pi" to keep resolving in
|
|
112
|
+
* that frozen PATH. Leaves an explicit PI_PACKED_PI_BIN/PI_BIN untouched.
|
|
113
|
+
*/
|
|
114
|
+
export declare function resolvePiBinForSpawn(env?: NodeJS.ProcessEnv): string | undefined;
|
|
108
115
|
export declare function ensurePackedClient(paths?: PackedClientPaths, transport?: FetchTransport): Promise<PackedClient>;
|
|
109
116
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../service/src/public/client.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../service/src/public/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{spawn as
|
|
1
|
+
import{spawn as g}from"node:child_process";import{accessSync as A,constants as f,readFileSync as S}from"node:fs";import{delimiter as I,dirname as y,join as O,resolve as k}from"node:path";import{fileURLToPath as P}from"node:url";class j{baseUrl;token;opsPath;transport;label;constructor(z,J,G){this.baseUrl=z;this.token=J;this.label=G.label,this.opsPath=G.opsPath??"/api/v1/ops",this.transport=G.transport??fetch}authedRequest(z,J={}){return this.transport(new Request(`${this.baseUrl}${z}`,{...J,headers:{...J.headers,authorization:`Bearer ${this.token}`}}))}async call(z,J){let G=await this.authedRequest(this.opsPath,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({op:z,input:J})}),Z=await G.json();if(!G.ok)throw Error(Z.error??`${this.label} operation failed with HTTP ${G.status}`);return Z.result}async operations(){let z=await this.authedRequest(this.opsPath),J=await z.json();if(!z.ok)throw Error(J.error??`${this.label} discovery failed with HTTP ${z.status}`);return J.operations??[]}async ready(){let z=await this.authedRequest("/ready");if(z.status===503)return!1;if(!z.ok)throw Error(`${this.label} readiness check failed with HTTP ${z.status}`);return!0}async health(){let z=await this.authedRequest("/health"),J=await z.json();if(!z.ok||J.ok!==!0||typeof J.version!=="string")throw Error(J.error??`${this.label} health check failed`);return{ok:!0,version:J.version}}}import{chmodSync as r,existsSync as p,mkdirSync as o,readFileSync as C,renameSync as n,rmSync as t,writeFileSync as a}from"node:fs";import{homedir as b,tmpdir as N}from"node:os";import{dirname as zz,join as W,win32 as Y}from"node:path";var _="127.0.0.1";function q(z,J={}){let G=J.platform??process.platform,Z=J.home??b();if(G==="darwin")return L(z,Z);if(G==="win32")return M(z,J.env??process.env,Z);return R(z,J,Z)}function R(z,J,G){let Z=J.env??process.env,$=J.uid??process.getuid?.()??0,K=Z.XDG_DATA_HOME??W(G,".local","share"),Q=Z.XDG_STATE_HOME??W(G,".local","state"),x=Z.XDG_RUNTIME_DIR??W("/run","user",String($)),D=Z.XDG_CONFIG_HOME??W(G,".config");return{database:W(K,z.stateDirectoryName,z.databaseFilename),token:W(Q,z.stateDirectoryName,z.tokenFilename),handle:W(x,z.stateDirectoryName,z.handleFilename),serviceDescriptor:W(D,"systemd","user",z.systemdUnitName)}}function L(z,J){let G=W(J,"Library"),Z=W(G,"Application Support",z.stateDirectoryName);return{database:W(Z,z.databaseFilename),token:W(Z,z.tokenFilename),handle:W(N(),z.stateDirectoryName,z.handleFilename),serviceDescriptor:W(Z,z.systemdUnitName)}}function M(z,J,G){let Z=J.LOCALAPPDATA??Y.join(G,"AppData","Local"),$=J.APPDATA??Y.join(G,"AppData","Roaming"),K=Y.join(Z,z.stateDirectoryName,"Data");return{database:Y.join(K,z.databaseFilename),token:Y.join(K,z.tokenFilename),handle:Y.join(Z,"Temp",z.stateDirectoryName,z.handleFilename),serviceDescriptor:Y.join($,z.stateDirectoryName,"Config",z.systemdUnitName)}}function X(z){try{let J=JSON.parse(C(z,"utf8"));if(J.host!==_||!Number.isInteger(J.port)||J.port<1||J.port>65535||!Number.isInteger(J.pid))return null;return J}catch{return null}}import{execFileSync as E}from"node:child_process";import{fileURLToPath as w}from"node:url";function H(z,J){let G=J.runCommand(process.execPath,[J.armadaCliPath,"status","--json"]);if(!G.ok)return!1;try{return JSON.parse(G.output).vehicles?.some(($)=>$.name===z)??!1}catch{return!1}}function B(){return{armadaCliPath:w(import.meta.resolve("@danypops/armada/cli")),runCommand:(z,J,G)=>{try{return{ok:!0,output:E(z,J,{encoding:"utf8",input:G,stdio:[G===void 0?"ignore":"pipe","pipe","pipe"]})}}catch(Z){let $=Z;return{ok:!1,output:($.stdout??"")+($.stderr??$.message)}}}}}class U extends Error{notADaemon;constructor(z,J){super(z);this.notADaemon=J;this.name="InstallServiceError"}}var u="pi-packed",F="pi-packed.service";function T(z={}){let J=z.env??process.env;if(J.PI_PACKED_HOME){let Z=k(J.PI_PACKED_HOME);return{token:`${Z}/token`,handle:`${Z}/handle.json`,serviceDescriptor:`${Z}/${F}`}}let G=q({stateDirectoryName:"pi-packed",databaseFilename:"packages.db",tokenFilename:"token",handleFilename:"handle.json",systemdUnitName:F},z);return{token:G.token,handle:G.handle,serviceDescriptor:G.serviceDescriptor}}function v(){return H(u,B())}function h(z,J=30000){return(G)=>z(new Request(G,{signal:AbortSignal.any([G.signal,AbortSignal.timeout(J)])}))}class V{rpc;constructor(z,J,G=fetch){this.rpc=new j(z,J,{label:"Packed",transport:h(G)})}call(z,J){return this.rpc.call(z,J)}health(){return this.rpc.health()}search(z,J,G=!1){return this.call("package.search",{query:z,limit:J,offline:G})}info(z){return this.call("package.info",{name:z})}installed(){return this.call("package.installed",{})}async updates(){return(await this.call("package.updates",{})).updates}security(){return this.call("package.security.get",{})}setMutationApproval(z,J=!1){return this.call("package.security.set",{mutationApproval:z,approved:J})}async install(z,J=!1){let G=await this.call("package.install",{source:z,approved:J});if(!G.ok)throw Error(G.output);return G.output}async installService(z,J=!1){let G=await this.call("package.install_service",{source:z,approved:J});if(!G.ok)throw new U(G.output,G.notADaemon);return{output:G.output,spec:G.spec}}async remove(z,J=!1){let G=await this.call("package.remove",{name:z,approved:J});if(!G.ok)throw Error(G.output);return G.output}async update(z,J=!1){let G=await this.call("package.update",{source:z,approved:J});if(!G.ok)throw Error(G.output);return{output:G.output,reloadRequired:G.reloadRequired??!0,alreadyUpToDate:G.alreadyUpToDate??!1,pinned:G.pinned??!1,previousVersion:G.previousVersion,currentVersion:G.currentVersion}}setupPlan(z,J=!1){return this.call("setup.plan",{manifestPath:z,prune:J})}setupApply(z,J=!1,G=!1){return this.call("setup.apply",{manifestPath:z,approved:J,prune:G})}listResources(z){return this.call("resources.list",{projectRoot:z})}async toggleResource(z,J,G,Z,$,K=!1){let Q=await this.call("resources.toggle",{source:z,field:J,path:G,enabled:Z,projectRoot:$,approved:K});if(!Q.ok)throw Error(Q.output);return Q.output}piStatus(){return this.call("pi.status",{})}}async function m(z=T(),J=fetch){let G=X(z.handle);if(!G)throw Error("Packed daemon is not running");let Z;try{Z=S(z.token,"utf8").trim()}catch{throw Error("Packed daemon token is unavailable")}if(!/^[a-f0-9]{64}$/.test(Z))throw Error("Packed daemon token is invalid");let $=new V(`http://${G.host}:${G.port}`,Z,J);return await $.health(),$}async function d(z){try{return await z.connect()}catch{}let J=z.retryAttempts??30,G=z.retryDelayMs??100,Z=z.isServiceInstalled();if(!Z)z.spawn();for(let K=0;K<J;K++){await z.sleep(G);try{return await z.connect()}catch{}}let $=J*G/1000;throw Error(Z?`The managed Packed service is registered but unreachable after ${$} seconds. Its supervisor may have stopped it or it may be failing; restart the managed service and inspect its status and logs. Packed will not auto-spawn a competing process.`:`Packed daemon did not become ready within ${$} seconds`)}function c(z=process.env){if(z.PI_PACKED_PI_BIN||z.PI_BIN)return;let J=z.PATH;if(!J)return;let G=process.platform==="win32"?["pi.exe","pi.cmd","pi.bat","pi"]:["pi"];for(let Z of J.split(I)){if(!Z)continue;for(let $ of G){let K=O(Z,$);try{return A(K,f.X_OK),K}catch{}}}return}async function Hz(z=T(),J=fetch){return d({connect:()=>m(z,J),isServiceInstalled:()=>v(),spawn:()=>{let G=process.env.PI_PACKED_BIN,Z=G??process.env.PI_PACKED_BUN??"bun",$=G?["serve"]:[O(y(P(import.meta.url)),"../service/src/cli/cli.ts"),"serve"],K=c(),Q=g(Z,$,{detached:!0,stdio:"ignore",env:{...process.env,DAEMON_KIT_LAUNCH_PROVENANCE:"auto-spawn",...K?{PI_BIN:K}:{}}});Q.once("error",()=>{}),Q.unref()},sleep:(G)=>new Promise((Z)=>setTimeout(Z,G))})}export{c as resolvePiBinForSpawn,T as resolvePackedClientPaths,Hz as ensurePackedClient,d as ensureClient,m as connectPackedClient,V as PackedClient,U as InstallServiceError};
|
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);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
import { dirname, join, resolve } from "node:path";
|
|
2
|
+
import { accessSync, constants as fsConstants, readFileSync } from "node:fs";
|
|
3
|
+
import { delimiter, dirname, join, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { AuthenticatedRpcClient } from "@danypops/vehicle-client/rpc-client";
|
|
6
6
|
import { readDaemonHandle, resolveDaemonPaths } from "@danypops/vehicle-server/paths";
|
|
@@ -244,6 +244,30 @@ export async function ensureClient(deps: EnsureClientDeps): Promise<PackedClient
|
|
|
244
244
|
);
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
/**
|
|
248
|
+
* An auto-spawned daemon is detached and long-lived, so it keeps whatever
|
|
249
|
+
* PATH its host process had at spawn time forever. Resolve `pi`'s absolute
|
|
250
|
+
* path now and pin it, rather than trust a bare "pi" to keep resolving in
|
|
251
|
+
* that frozen PATH. Leaves an explicit PI_PACKED_PI_BIN/PI_BIN untouched.
|
|
252
|
+
*/
|
|
253
|
+
export function resolvePiBinForSpawn(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
254
|
+
if (env.PI_PACKED_PI_BIN || env.PI_BIN) return undefined; // explicit override already present; nothing to pin
|
|
255
|
+
const path = env.PATH;
|
|
256
|
+
if (!path) return undefined;
|
|
257
|
+
const names = process.platform === "win32" ? ["pi.exe", "pi.cmd", "pi.bat", "pi"] : ["pi"];
|
|
258
|
+
for (const dir of path.split(delimiter)) {
|
|
259
|
+
if (!dir) continue;
|
|
260
|
+
for (const name of names) {
|
|
261
|
+
const candidate = join(dir, name);
|
|
262
|
+
try {
|
|
263
|
+
accessSync(candidate, fsConstants.X_OK);
|
|
264
|
+
return candidate;
|
|
265
|
+
} catch {}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
|
|
247
271
|
export async function ensurePackedClient(paths = resolvePackedClientPaths(), transport: FetchTransport = fetch): Promise<PackedClient> {
|
|
248
272
|
return ensureClient({
|
|
249
273
|
connect: () => connectPackedClient(paths, transport),
|
|
@@ -252,10 +276,15 @@ export async function ensurePackedClient(paths = resolvePackedClientPaths(), tra
|
|
|
252
276
|
const override = process.env.PI_PACKED_BIN;
|
|
253
277
|
const command = override ?? process.env.PI_PACKED_BUN ?? "bun";
|
|
254
278
|
const args = override ? ["serve"] : [join(dirname(fileURLToPath(import.meta.url)), "../service/src/cli/cli.ts"), "serve"];
|
|
279
|
+
const resolvedPiBin = resolvePiBinForSpawn();
|
|
255
280
|
const child = spawn(command, args, {
|
|
256
281
|
detached: true,
|
|
257
282
|
stdio: "ignore",
|
|
258
|
-
env: {
|
|
283
|
+
env: {
|
|
284
|
+
...process.env,
|
|
285
|
+
DAEMON_KIT_LAUNCH_PROVENANCE: "auto-spawn",
|
|
286
|
+
...(resolvedPiBin ? { PI_BIN: resolvedPiBin } : {}),
|
|
287
|
+
},
|
|
259
288
|
});
|
|
260
289
|
child.once("error", () => {});
|
|
261
290
|
child.unref();
|
|
@@ -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 };
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { chmodSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
1
4
|
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { ensureClient } from "../src/public/client.ts";
|
|
5
|
+
import { ensureClient, resolvePiBinForSpawn } from "../src/public/client.ts";
|
|
3
6
|
|
|
4
7
|
describe("ensureClient (packed daemon auto-spawn-or-wait decision)", () => {
|
|
5
8
|
it("connects immediately without ever checking for a service or spawning, when already reachable", async () => {
|
|
@@ -132,3 +135,41 @@ describe("ensureClient (packed daemon auto-spawn-or-wait decision)", () => {
|
|
|
132
135
|
expect(serviceChecks).toBe(1);
|
|
133
136
|
});
|
|
134
137
|
});
|
|
138
|
+
|
|
139
|
+
describe("resolvePiBinForSpawn", () => {
|
|
140
|
+
it("leaves an explicit PI_PACKED_PI_BIN untouched", () => {
|
|
141
|
+
expect(resolvePiBinForSpawn({ PI_PACKED_PI_BIN: "/somewhere/pi", PATH: "/usr/bin" })).toBeUndefined();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("leaves an explicit PI_BIN untouched", () => {
|
|
145
|
+
expect(resolvePiBinForSpawn({ PI_BIN: "/somewhere/pi", PATH: "/usr/bin" })).toBeUndefined();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("returns undefined when PATH is missing", () => {
|
|
149
|
+
expect(resolvePiBinForSpawn({})).toBeUndefined();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("returns undefined when no PATH directory has an executable `pi`", () => {
|
|
153
|
+
const dir = mkdtempSync(join(tmpdir(), "packed-resolve-pi-bin-"));
|
|
154
|
+
expect(resolvePiBinForSpawn({ PATH: dir })).toBeUndefined();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("resolves the absolute path to an executable `pi` on PATH", () => {
|
|
158
|
+
const dir = mkdtempSync(join(tmpdir(), "packed-resolve-pi-bin-"));
|
|
159
|
+
const piPath = join(dir, "pi");
|
|
160
|
+
writeFileSync(piPath, "#!/bin/sh\necho pi\n");
|
|
161
|
+
chmodSync(piPath, 0o755);
|
|
162
|
+
expect(resolvePiBinForSpawn({ PATH: dir })).toBe(piPath);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
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-"));
|
|
168
|
+
writeFileSync(join(deadDir, "pi"), "not executable");
|
|
169
|
+
chmodSync(join(deadDir, "pi"), 0o644);
|
|
170
|
+
const livePi = join(liveDir, "pi");
|
|
171
|
+
writeFileSync(livePi, "#!/bin/sh\necho pi\n");
|
|
172
|
+
chmodSync(livePi, 0o755);
|
|
173
|
+
expect(resolvePiBinForSpawn({ PATH: `${deadDir}:${liveDir}` })).toBe(livePi);
|
|
174
|
+
});
|
|
175
|
+
});
|