@danypops/pi-packed 0.21.8 → 0.21.10
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/packages/install.ts +30 -1
- package/service/src/public/client.ts +32 -3
- package/service/test/install-validation.test.ts +11 -4
- package/service/test/install.test.ts +89 -4
- 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":"AAOA,OAAO,KAAK,EAGX,yBAAyB,EACzB,WAAW,EACX,gBAAgB,EAChB,cAAc,EACd,QAAQ,EACR,aAAa,EACb,gBAAgB,EAChB,kBAAkB,EAClB,gBAAgB,EAChB,SAAS,EACT,WAAW,EACX,aAAa,EACb,MAAM,eAAe,CAAC;AAEvB,6NAA6N;AAC7N,qBAAa,mBAAoB,SAAQ,KAAK;IAG5C,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO;gBAD7B,OAAO,EAAE,MAAM,EACN,UAAU,CAAC,EAAE,OAAO,YAAA;CAK9B;AAED,MAAM,WAAW,iBAAiB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,iBAAiB,EAAE,MAAM,CAAC;CAC1B;AAGD,MAAM,WAAW,iBAAiB;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC3B;AACD,MAAM,WAAW,qBAAqB;IACrC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,cAAc,EAAE,CAAA;KAAE,CAAC,CAAC;IAC9H,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IACzC,SAAS,IAAI,OAAO,CAAC,yBAAyB,CAAC,mBAAmB,CAAC,CAAC,CAAC;IACrE,OAAO,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAClC,QAAQ,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACtC,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,CAAC,kBAAkB,CAAC,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAChH,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7D,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,kBAAkB,CAAA;KAAE,CAAC,CAAC;IAC3G,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1D,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IACnE,SAAS,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IACrE,UAAU,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACjG,aAAa,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;QAAC,OAAO,EAAE,gBAAgB,EAAE,CAAA;KAAE,CAAC,CAAC;IAC1G,cAAc,CACb,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,aAAa,EACpB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,EAChB,WAAW,CAAC,EAAE,MAAM,EACpB,QAAQ,CAAC,EAAE,OAAO,GAChB,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC9B;AAED,wBAAgB,wBAAwB,CAAC,OAAO,GAAE,iBAAsB,GAAG,iBAAiB,CAiB3F;AAOD,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAMrE,qBAAa,YAAa,YAAW,qBAAqB;IACzD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAsG;gBAC9G,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,GAAE,cAAsB;IAG1E,OAAO,CAAC,IAAI;IAMZ,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC;IAG1B,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,UAAQ;;;;;;IAGpD,IAAI,CAAC,IAAI,EAAE,MAAM;IAGjB,SAAS;IAGH,OAAO;IAGb,QAAQ;IAGR,mBAAmB,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,kBAAkB,CAAC,EAAE,QAAQ,UAAQ;IAGtF,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,UAAQ;IAKxC,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,UAAQ;;;;IAK/C,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,UAAQ;IAKrC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,UAAQ,GAAG,OAAO,CAAC,aAAa,CAAC;IAYtE,SAAS,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,UAAQ;IAG7C,UAAU,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,UAAQ,EAAE,KAAK,UAAQ;IAGhE,aAAa,CAAC,WAAW,CAAC,EAAE,MAAM;;;;IAG5B,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,CAAC,EAAE,MAAM,EAAE,QAAQ,UAAQ;IAKjI,QAAQ;CAGR;AAED,wBAAsB,mBAAmB,CAAC,KAAK,oBAA6B,EAAE,SAAS,GAAE,cAAsB,GAAG,OAAO,CAAC,YAAY,CAAC,CAatI;AAED,MAAM,WAAW,gBAAgB;IAChC,OAAO,IAAI,OAAO,CAAC,YAAY,CAAC,CAAC;IACjC;;mCAE+B;IAC/B,kBAAkB,IAAI,OAAO,CAAC;IAC9B;yDACqD;IACrD,KAAK,IAAI,IAAI,CAAC;IACd,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,YAAY,CAAC,CAoBhF;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,GAAG,SAAS,CAgB7F;AAED,wBAAsB,kBAAkB,CAAC,KAAK,oBAA6B,EAAE,SAAS,GAAE,cAAsB,GAAG,OAAO,CAAC,YAAY,CAAC,CAuBrI"}
|
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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** install.ts — driven adapter: pi CLI mutations via Bun.spawn. */
|
|
2
2
|
|
|
3
|
+
import { join } from "node:path";
|
|
3
4
|
import { HeadlessInstallValidator, type InstallValidator } from "../adoption/install-validation.ts";
|
|
4
5
|
import { defaultPiHome, isPinnedNpmSource, readResolvedVersion } from "./installed.ts";
|
|
5
6
|
import type { Installer, UpdateOutcome } from "./package.ts";
|
|
@@ -11,11 +12,16 @@ export function defaultPiBin(): string {
|
|
|
11
12
|
return process.env.PI_PACKED_PI_BIN ?? process.env.PI_BIN ?? "pi";
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
export function defaultNpmBin(): string {
|
|
16
|
+
return process.env.PI_PACKED_NPM_BIN ?? process.env.NPM_BIN ?? "npm";
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
export class ExecInstaller implements Installer {
|
|
15
20
|
constructor(
|
|
16
21
|
private bin = defaultPiBin(),
|
|
17
22
|
private piHome = defaultPiHome(),
|
|
18
23
|
private validator: InstallValidator = new HeadlessInstallValidator(),
|
|
24
|
+
private npmBin = defaultNpmBin(),
|
|
19
25
|
) {}
|
|
20
26
|
|
|
21
27
|
private async run(args: string[]): Promise<string> {
|
|
@@ -27,6 +33,26 @@ export class ExecInstaller implements Installer {
|
|
|
27
33
|
return out;
|
|
28
34
|
}
|
|
29
35
|
|
|
36
|
+
/**
|
|
37
|
+
* `pi install`/`pi update --extension <source>` only resolve the target
|
|
38
|
+
* package's own subtree. npm's own `dedupe` docs (confirmed by
|
|
39
|
+
* npm/cli#5307 and npm/cli#7277) describe rearranging already-resolved
|
|
40
|
+
* versions, never installing a newer one -- so a sibling package's own
|
|
41
|
+
* declared range can go unsatisfied at the shared root and node's
|
|
42
|
+
* resolution silently walks up to a stale copy nothing wanted. A full
|
|
43
|
+
* `npm install` (no args) at piHome/npm is npm's own documented reliable
|
|
44
|
+
* fix: it re-resolves the whole tree, not just the last-touched package.
|
|
45
|
+
*/
|
|
46
|
+
private async reresolveDependencyTree(): Promise<string> {
|
|
47
|
+
const cwd = join(this.piHome, "npm");
|
|
48
|
+
const proc = Bun.spawn([this.npmBin, "install"], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
49
|
+
const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
|
|
50
|
+
const out = [stdout.trim(), stderr.trim()].filter(Boolean).join("\n");
|
|
51
|
+
const code = await proc.exited;
|
|
52
|
+
if (code !== 0) throw new Error(`npm install failed to re-resolve the dependency tree at ${cwd} (exit ${code}): ${out || "no output"}`);
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
30
56
|
async install(source: string, options?: { approved?: boolean; local?: boolean }): Promise<string> {
|
|
31
57
|
const validation = await this.validator.validate(source);
|
|
32
58
|
if (!validation.ok) {
|
|
@@ -36,7 +62,9 @@ export class ExecInstaller implements Installer {
|
|
|
36
62
|
.join("; ");
|
|
37
63
|
throw new Error(`install refused -- ${detail || validation.message || "extension failed a headless load check"}`);
|
|
38
64
|
}
|
|
39
|
-
|
|
65
|
+
const output = await this.run(["install", ...(options?.local ? ["-l"] : []), source]);
|
|
66
|
+
await this.reresolveDependencyTree();
|
|
67
|
+
return output;
|
|
40
68
|
}
|
|
41
69
|
|
|
42
70
|
remove(source: string, options?: { approved?: boolean; local?: boolean }): Promise<string> {
|
|
@@ -47,6 +75,7 @@ export class ExecInstaller implements Installer {
|
|
|
47
75
|
const pinned = isPinnedNpmSource(source);
|
|
48
76
|
const previousVersion = readResolvedVersion(this.piHome, source);
|
|
49
77
|
const output = await this.run(["update", "--extension", source]);
|
|
78
|
+
await this.reresolveDependencyTree();
|
|
50
79
|
const currentVersion = readResolvedVersion(this.piHome, source);
|
|
51
80
|
// Only trust a "nothing changed" conclusion when we actually read a
|
|
52
81
|
// real version both before and after (npm source, resolvable in
|
|
@@ -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,6 +8,8 @@
|
|
|
8
8
|
* pass.
|
|
9
9
|
*/
|
|
10
10
|
import { describe, expect, it } from "bun:test";
|
|
11
|
+
import { mkdirSync, mkdtempSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
11
13
|
import { dirname, join } from "node:path";
|
|
12
14
|
import { fileURLToPath } from "node:url";
|
|
13
15
|
import type { InstallValidationResult, InstallValidator } from "../src/adoption/install-validation.ts";
|
|
@@ -94,13 +96,18 @@ describe("ExecInstaller.install() -- refuses before ever spawning the real pi bi
|
|
|
94
96
|
await expect(installer.install("npm:broken-pkg")).rejects.toThrow(/install refused.*extension\/index\.ts: boom/);
|
|
95
97
|
});
|
|
96
98
|
|
|
97
|
-
it("proceeds to the real install when the validator approves", async () => {
|
|
98
|
-
// /
|
|
99
|
-
//
|
|
99
|
+
it("proceeds to the real install when the validator approves, then re-resolves the whole tree", async () => {
|
|
100
|
+
// A real piHome/npm dir must exist for the post-install re-resolution
|
|
101
|
+
// step to cwd into -- /bin/true always exits 0 regardless, proving both
|
|
102
|
+
// the real install spawn *and* the re-resolution spawn were reached (a
|
|
103
|
+
// refused install never gets this far to find out).
|
|
104
|
+
const piHome = mkdtempSync(join(tmpdir(), "packed-install-validation-pihome-"));
|
|
105
|
+
mkdirSync(join(piHome, "npm"), { recursive: true });
|
|
100
106
|
const installer = new ExecInstaller(
|
|
101
107
|
"/bin/true",
|
|
102
|
-
|
|
108
|
+
piHome,
|
|
103
109
|
fakeValidator({ ok: true, source: "npm:good-pkg", extensions: [] }),
|
|
110
|
+
"/bin/true",
|
|
104
111
|
);
|
|
105
112
|
|
|
106
113
|
await expect(installer.install("npm:good-pkg")).resolves.toBeDefined();
|
|
@@ -40,6 +40,7 @@ function writeFakePi(dir: string, rewrite?: { piHome: string; name: string; newV
|
|
|
40
40
|
|
|
41
41
|
function writePiHome(nodeModules: Record<string, string> = {}): string {
|
|
42
42
|
const dir = mkdtempSync(join(tmpdir(), "packed-exec-pihome-"));
|
|
43
|
+
mkdirSync(join(dir, "npm"), { recursive: true });
|
|
43
44
|
for (const [name, version] of Object.entries(nodeModules)) {
|
|
44
45
|
const pkgDir = join(dir, "npm", "node_modules", name);
|
|
45
46
|
mkdirSync(pkgDir, { recursive: true });
|
|
@@ -48,12 +49,35 @@ function writePiHome(nodeModules: Record<string, string> = {}): string {
|
|
|
48
49
|
return dir;
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
/**
|
|
53
|
+
* A fake `npm` binary standing in for ExecInstaller's post-install/update
|
|
54
|
+
* full-tree re-resolution step. Writes every invocation's cwd to `logFile`
|
|
55
|
+
* (so a test can assert it actually ran against piHome/npm, not some other
|
|
56
|
+
* directory) and, when `rewrite` is given, writes a fresh version for a
|
|
57
|
+
* *different* package than the one the fake `pi` binary touched --
|
|
58
|
+
* reproducing the real defect: only a real `npm install` (no args, whole
|
|
59
|
+
* tree) reaches a stale sibling that a targeted `pi update` never does.
|
|
60
|
+
*/
|
|
61
|
+
function writeFakeNpm(dir: string, logFile: string, rewrite?: { piHome: string; name: string; newVersion: string }): string {
|
|
62
|
+
const script = join(dir, "fake-npm");
|
|
63
|
+
const rewriteLine = rewrite
|
|
64
|
+
? `mkdir -p '${join(rewrite.piHome, "npm", "node_modules", rewrite.name)}' && printf '{"version":"%s"}' '${rewrite.newVersion}' > '${join(rewrite.piHome, "npm", "node_modules", rewrite.name, "package.json")}'`
|
|
65
|
+
: "true";
|
|
66
|
+
writeFileSync(
|
|
67
|
+
script,
|
|
68
|
+
["#!/usr/bin/env bash", "set -euo pipefail", `pwd >> '${logFile}'`, `echo "$@" >> '${logFile}'`, rewriteLine, "exit 0"].join("\n"),
|
|
69
|
+
);
|
|
70
|
+
chmodSync(script, 0o755);
|
|
71
|
+
return script;
|
|
72
|
+
}
|
|
73
|
+
|
|
51
74
|
describe("ExecInstaller.update() — honest reloadRequired despite pi's ambiguous exit-0 text", () => {
|
|
52
75
|
it("pinned source, version genuinely unchanged: alreadyUpToDate, reloadRequired false", async () => {
|
|
53
76
|
const scriptDir = mkdtempSync(join(tmpdir(), "packed-exec-bin-"));
|
|
54
77
|
const bin = writeFakePi(scriptDir);
|
|
78
|
+
const npmBin = writeFakeNpm(scriptDir, join(scriptDir, "npm.log"));
|
|
55
79
|
const piHome = writePiHome({ "@scope/pkg": "1.2.3" });
|
|
56
|
-
const installer = new ExecInstaller(bin, piHome);
|
|
80
|
+
const installer = new ExecInstaller(bin, piHome, undefined, npmBin);
|
|
57
81
|
|
|
58
82
|
const outcome = await installer.update("npm:@scope/pkg@1.2.3");
|
|
59
83
|
|
|
@@ -68,8 +92,9 @@ describe("ExecInstaller.update() — honest reloadRequired despite pi's ambiguou
|
|
|
68
92
|
it('unpinned source, already latest (pi still exits 0 and says "Updated"): alreadyUpToDate', async () => {
|
|
69
93
|
const scriptDir = mkdtempSync(join(tmpdir(), "packed-exec-bin-"));
|
|
70
94
|
const bin = writeFakePi(scriptDir);
|
|
95
|
+
const npmBin = writeFakeNpm(scriptDir, join(scriptDir, "npm.log"));
|
|
71
96
|
const piHome = writePiHome({ plain: "0.5.0" });
|
|
72
|
-
const installer = new ExecInstaller(bin, piHome);
|
|
97
|
+
const installer = new ExecInstaller(bin, piHome, undefined, npmBin);
|
|
73
98
|
|
|
74
99
|
const outcome = await installer.update("npm:plain");
|
|
75
100
|
|
|
@@ -84,7 +109,8 @@ describe("ExecInstaller.update() — honest reloadRequired despite pi's ambiguou
|
|
|
84
109
|
const scriptDir = mkdtempSync(join(tmpdir(), "packed-exec-bin-"));
|
|
85
110
|
const piHome = writePiHome({ plain: "0.5.0" });
|
|
86
111
|
const bin = writeFakePi(scriptDir, { piHome, name: "plain", newVersion: "0.6.0" });
|
|
87
|
-
const
|
|
112
|
+
const npmBin = writeFakeNpm(scriptDir, join(scriptDir, "npm.log"));
|
|
113
|
+
const installer = new ExecInstaller(bin, piHome, undefined, npmBin);
|
|
88
114
|
|
|
89
115
|
const outcome = await installer.update("npm:plain");
|
|
90
116
|
|
|
@@ -98,8 +124,9 @@ describe("ExecInstaller.update() — honest reloadRequired despite pi's ambiguou
|
|
|
98
124
|
it("git: source (no npm resolution possible either side): conservatively assumes it may have changed", async () => {
|
|
99
125
|
const scriptDir = mkdtempSync(join(tmpdir(), "packed-exec-bin-"));
|
|
100
126
|
const bin = writeFakePi(scriptDir);
|
|
127
|
+
const npmBin = writeFakeNpm(scriptDir, join(scriptDir, "npm.log"));
|
|
101
128
|
const piHome = writePiHome();
|
|
102
|
-
const installer = new ExecInstaller(bin, piHome);
|
|
129
|
+
const installer = new ExecInstaller(bin, piHome, undefined, npmBin);
|
|
103
130
|
|
|
104
131
|
const outcome = await installer.update("git:github.com/u/r@main");
|
|
105
132
|
|
|
@@ -111,3 +138,61 @@ describe("ExecInstaller.update() — honest reloadRequired despite pi's ambiguou
|
|
|
111
138
|
expect(outcome.reloadRequired).toBe(true);
|
|
112
139
|
});
|
|
113
140
|
});
|
|
141
|
+
|
|
142
|
+
describe("ExecInstaller — forces full dependency re-resolution, not just the target's own subtree", () => {
|
|
143
|
+
it("update() fixes a stale root-level sibling that the targeted pi update never touched", async () => {
|
|
144
|
+
const scriptDir = mkdtempSync(join(tmpdir(), "packed-exec-bin-"));
|
|
145
|
+
// Simulates the confirmed live defect: after `pi update npm:@scope/leaf`,
|
|
146
|
+
// the leaf's own version bumps, but a root-level sibling
|
|
147
|
+
// (@scope/shared) that the freshly-updated leaf now needs a newer
|
|
148
|
+
// range of stays pinned at its old, now-unsatisfied version --
|
|
149
|
+
// `pi update --extension` only ever resolved the leaf's own subtree.
|
|
150
|
+
const piHome = writePiHome({ "@scope/leaf": "1.0.0", "@scope/shared": "1.0.0" });
|
|
151
|
+
const bin = writeFakePi(scriptDir, { piHome, name: "@scope/leaf", newVersion: "2.0.0" });
|
|
152
|
+
// A real `npm install` (no args, whole-tree) is the only thing that
|
|
153
|
+
// actually reaches @scope/shared -- reproduced here as the fake npm
|
|
154
|
+
// binary rewriting it to the version the leaf's new range needs.
|
|
155
|
+
const npmLog = join(scriptDir, "npm.log");
|
|
156
|
+
const npmBin = writeFakeNpm(scriptDir, npmLog, { piHome, name: "@scope/shared", newVersion: "2.0.0" });
|
|
157
|
+
const installer = new ExecInstaller(bin, piHome, undefined, npmBin);
|
|
158
|
+
|
|
159
|
+
const outcome = await installer.update("npm:@scope/leaf");
|
|
160
|
+
|
|
161
|
+
expect(outcome.currentVersion).toBe("2.0.0");
|
|
162
|
+
// The real assertion: a package `pi update` never named is resolved
|
|
163
|
+
// too, everywhere in the tree -- not just the target's own package.json.
|
|
164
|
+
expect(readFileSync(join(piHome, "npm", "node_modules", "@scope/shared", "package.json"), "utf8")).toContain("2.0.0");
|
|
165
|
+
// And it ran the re-resolution against piHome/npm specifically, with
|
|
166
|
+
// no target argument -- a full-tree resolve, not another targeted op.
|
|
167
|
+
const log = readFileSync(npmLog, "utf8").trim().split("\n");
|
|
168
|
+
expect(log[0]).toBe(join(piHome, "npm"));
|
|
169
|
+
expect(log[1]).toBe("install");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("install() also forces a full re-resolution after a successful pi install", async () => {
|
|
173
|
+
const scriptDir = mkdtempSync(join(tmpdir(), "packed-exec-bin-"));
|
|
174
|
+
const piHome = writePiHome({ "@scope/shared": "1.0.0" });
|
|
175
|
+
const bin = writeFakePi(scriptDir);
|
|
176
|
+
const npmLog = join(scriptDir, "npm.log");
|
|
177
|
+
const npmBin = writeFakeNpm(scriptDir, npmLog, { piHome, name: "@scope/shared", newVersion: "2.0.0" });
|
|
178
|
+
const installer = new ExecInstaller(bin, piHome, { validate: async (source) => ({ ok: true, source, extensions: [] }) }, npmBin);
|
|
179
|
+
|
|
180
|
+
await installer.install("npm:@scope/new-pkg");
|
|
181
|
+
|
|
182
|
+
expect(readFileSync(join(piHome, "npm", "node_modules", "@scope/shared", "package.json"), "utf8")).toContain("2.0.0");
|
|
183
|
+
const log = readFileSync(npmLog, "utf8").trim().split("\n");
|
|
184
|
+
expect(log[0]).toBe(join(piHome, "npm"));
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("surfaces a failed re-resolution instead of silently reporting success", async () => {
|
|
188
|
+
const scriptDir = mkdtempSync(join(tmpdir(), "packed-exec-bin-"));
|
|
189
|
+
const piHome = writePiHome({ plain: "0.5.0" });
|
|
190
|
+
const bin = writeFakePi(scriptDir, { piHome, name: "plain", newVersion: "0.6.0" });
|
|
191
|
+
const failingNpm = join(scriptDir, "fake-npm-fail");
|
|
192
|
+
writeFileSync(failingNpm, ["#!/usr/bin/env bash", "echo 'ERESOLVE unable to resolve dependency tree' >&2", "exit 1"].join("\n"));
|
|
193
|
+
chmodSync(failingNpm, 0o755);
|
|
194
|
+
const installer = new ExecInstaller(bin, piHome, undefined, failingNpm);
|
|
195
|
+
|
|
196
|
+
await expect(installer.update("npm:plain")).rejects.toThrow(/npm install failed to re-resolve/);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -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
|
+
});
|