@danypops/pi-packed 0.21.5 → 0.21.7

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.js CHANGED
@@ -1 +1 @@
1
- import{spawn as E}from"node:child_process";import{readFileSync as g}from"node:fs";import{dirname as S,join as f,resolve as A}from"node:path";import{fileURLToPath as I}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 d,existsSync as c,mkdirSync as l,readFileSync as C,renameSync as i,rmSync as r,writeFileSync as p}from"node:fs";import{homedir as D,tmpdir as b}from"node:os";import{dirname as t,join as K,win32 as Q}from"node:path";var N="127.0.0.1";function q(z,J={}){let G=J.platform??process.platform,Z=J.home??D();if(G==="darwin")return R(z,Z);if(G==="win32")return L(z,J.env??process.env,Z);return _(z,J,Z)}function _(z,J,G){let Z=J.env??process.env,$=J.uid??process.getuid?.()??0,W=Z.XDG_DATA_HOME??K(G,".local","share"),Y=Z.XDG_STATE_HOME??K(G,".local","state"),V=Z.XDG_RUNTIME_DIR??K("/run","user",String($)),x=Z.XDG_CONFIG_HOME??K(G,".config");return{database:K(W,z.stateDirectoryName,z.databaseFilename),token:K(Y,z.stateDirectoryName,z.tokenFilename),handle:K(V,z.stateDirectoryName,z.handleFilename),serviceDescriptor:K(x,"systemd","user",z.systemdUnitName)}}function R(z,J){let G=K(J,"Library"),Z=K(G,"Application Support",z.stateDirectoryName);return{database:K(Z,z.databaseFilename),token:K(Z,z.tokenFilename),handle:K(b(),z.stateDirectoryName,z.handleFilename),serviceDescriptor:K(Z,z.systemdUnitName)}}function L(z,J,G){let Z=J.LOCALAPPDATA??Q.join(G,"AppData","Local"),$=J.APPDATA??Q.join(G,"AppData","Roaming"),W=Q.join(Z,z.stateDirectoryName,"Data");return{database:Q.join(W,z.databaseFilename),token:Q.join(W,z.tokenFilename),handle:Q.join(Z,"Temp",z.stateDirectoryName,z.handleFilename),serviceDescriptor:Q.join($,z.stateDirectoryName,"Config",z.systemdUnitName)}}function B(z){try{let J=JSON.parse(C(z,"utf8"));if(J.host!==N||!Number.isInteger(J.port)||J.port<1||J.port>65535||!Number.isInteger(J.pid))return null;return J}catch{return null}}import{execFileSync as M}from"node:child_process";import{fileURLToPath as w}from"node:url";function X(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 H(){return{armadaCliPath:w(import.meta.resolve("@danypops/armada/cli")),runCommand:(z,J,G)=>{try{return{ok:!0,output:M(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 O extends Error{notADaemon;constructor(z,J){super(z);this.notADaemon=J;this.name="InstallServiceError"}}var y="pi-packed",F="pi-packed.service";function U(z={}){let J=z.env??process.env;if(J.PI_PACKED_HOME){let Z=A(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 k(){return X(y,H())}function P(z,J=30000){return(G)=>z(new Request(G,{signal:AbortSignal.any([G.signal,AbortSignal.timeout(J)])}))}class T{rpc;constructor(z,J,G=fetch){this.rpc=new j(z,J,{label:"Packed",transport:P(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 O(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,$,W=!1){let Y=await this.call("resources.toggle",{source:z,field:J,path:G,enabled:Z,projectRoot:$,approved:W});if(!Y.ok)throw Error(Y.output);return Y.output}piStatus(){return this.call("pi.status",{})}}async function u(z=U(),J=fetch){let G=B(z.handle);if(!G)throw Error("Packed daemon is not running");let Z;try{Z=g(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 T(`http://${G.host}:${G.port}`,Z,J);return await $.health(),$}async function v(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 W=0;W<J;W++){await z.sleep(G);try{return await z.connect()}catch{}}let $=J*G/1000;throw Error(Z?`Packed daemon did not become ready within ${$} seconds, but a managed service is installed -- run packed doctor rather than auto-spawning a second one`:`Packed daemon did not become ready within ${$} seconds`)}async function Yz(z=U(),J=fetch){return v({connect:()=>u(z,J),isServiceInstalled:()=>k(),spawn:()=>{let G=process.env.PI_PACKED_BIN,Z=G??process.env.PI_PACKED_BUN??"bun",$=G?["serve"]:[f(S(I(import.meta.url)),"../service/src/cli/cli.ts"),"serve"],W=E(Z,$,{detached:!0,stdio:"ignore",env:{...process.env,DAEMON_KIT_LAUNCH_PROVENANCE:"auto-spawn"}});W.once("error",()=>{}),W.unref()},sleep:(G)=>new Promise((Z)=>setTimeout(Z,G))})}export{U as resolvePackedClientPaths,Yz as ensurePackedClient,v as ensureClient,u as connectPackedClient,T as PackedClient,O as InstallServiceError};
1
+ import{spawn as E}from"node:child_process";import{readFileSync as g}from"node:fs";import{dirname as S,join as f,resolve as A}from"node:path";import{fileURLToPath as I}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 d,existsSync as c,mkdirSync as l,readFileSync as C,renameSync as i,rmSync as r,writeFileSync as p}from"node:fs";import{homedir as D,tmpdir as b}from"node:os";import{dirname as t,join as K,win32 as Q}from"node:path";var N="127.0.0.1";function q(z,J={}){let G=J.platform??process.platform,Z=J.home??D();if(G==="darwin")return R(z,Z);if(G==="win32")return L(z,J.env??process.env,Z);return _(z,J,Z)}function _(z,J,G){let Z=J.env??process.env,$=J.uid??process.getuid?.()??0,W=Z.XDG_DATA_HOME??K(G,".local","share"),Y=Z.XDG_STATE_HOME??K(G,".local","state"),V=Z.XDG_RUNTIME_DIR??K("/run","user",String($)),x=Z.XDG_CONFIG_HOME??K(G,".config");return{database:K(W,z.stateDirectoryName,z.databaseFilename),token:K(Y,z.stateDirectoryName,z.tokenFilename),handle:K(V,z.stateDirectoryName,z.handleFilename),serviceDescriptor:K(x,"systemd","user",z.systemdUnitName)}}function R(z,J){let G=K(J,"Library"),Z=K(G,"Application Support",z.stateDirectoryName);return{database:K(Z,z.databaseFilename),token:K(Z,z.tokenFilename),handle:K(b(),z.stateDirectoryName,z.handleFilename),serviceDescriptor:K(Z,z.systemdUnitName)}}function L(z,J,G){let Z=J.LOCALAPPDATA??Q.join(G,"AppData","Local"),$=J.APPDATA??Q.join(G,"AppData","Roaming"),W=Q.join(Z,z.stateDirectoryName,"Data");return{database:Q.join(W,z.databaseFilename),token:Q.join(W,z.tokenFilename),handle:Q.join(Z,"Temp",z.stateDirectoryName,z.handleFilename),serviceDescriptor:Q.join($,z.stateDirectoryName,"Config",z.systemdUnitName)}}function B(z){try{let J=JSON.parse(C(z,"utf8"));if(J.host!==N||!Number.isInteger(J.port)||J.port<1||J.port>65535||!Number.isInteger(J.pid))return null;return J}catch{return null}}import{execFileSync as M}from"node:child_process";import{fileURLToPath as w}from"node:url";function X(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 H(){return{armadaCliPath:w(import.meta.resolve("@danypops/armada/cli")),runCommand:(z,J,G)=>{try{return{ok:!0,output:M(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 O extends Error{notADaemon;constructor(z,J){super(z);this.notADaemon=J;this.name="InstallServiceError"}}var y="pi-packed",F="pi-packed.service";function U(z={}){let J=z.env??process.env;if(J.PI_PACKED_HOME){let Z=A(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 k(){return X(y,H())}function P(z,J=30000){return(G)=>z(new Request(G,{signal:AbortSignal.any([G.signal,AbortSignal.timeout(J)])}))}class T{rpc;constructor(z,J,G=fetch){this.rpc=new j(z,J,{label:"Packed",transport:P(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 O(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,$,W=!1){let Y=await this.call("resources.toggle",{source:z,field:J,path:G,enabled:Z,projectRoot:$,approved:W});if(!Y.ok)throw Error(Y.output);return Y.output}piStatus(){return this.call("pi.status",{})}}async function u(z=U(),J=fetch){let G=B(z.handle);if(!G)throw Error("Packed daemon is not running");let Z;try{Z=g(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 T(`http://${G.host}:${G.port}`,Z,J);return await $.health(),$}async function v(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 W=0;W<J;W++){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`)}async function Yz(z=U(),J=fetch){return v({connect:()=>u(z,J),isServiceInstalled:()=>k(),spawn:()=>{let G=process.env.PI_PACKED_BIN,Z=G??process.env.PI_PACKED_BUN??"bun",$=G?["serve"]:[f(S(I(import.meta.url)),"../service/src/cli/cli.ts"),"serve"],W=E(Z,$,{detached:!0,stdio:"ignore",env:{...process.env,DAEMON_KIT_LAUNCH_PROVENANCE:"auto-spawn"}});W.once("error",()=>{}),W.unref()},sleep:(G)=>new Promise((Z)=>setTimeout(Z,G))})}export{U as resolvePackedClientPaths,Yz as ensurePackedClient,v as ensureClient,u as connectPackedClient,T as PackedClient,O as InstallServiceError};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-packed",
3
- "version": "0.21.5",
3
+ "version": "0.21.7",
4
4
  "description": "Pi package lifecycle, validation, daemon, tools, profiles, and TUI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,7 +18,6 @@ import { HttpRegistry } from "../registry/registry.ts";
18
18
  import {
19
19
  CATALOG_INTERVAL_DEFAULT_MS,
20
20
  ENV,
21
- IDLE_BUDGET_DEFAULT_MS,
22
21
  INDEX_INTERVAL_DEFAULT_MS,
23
22
  RECONCILE_INTERVAL_DEFAULT_MS,
24
23
  WATCH_INTERVAL_DEFAULT_MS,
@@ -41,6 +40,15 @@ export interface StartPackedDaemonOptions {
41
40
  maintenanceTasks?: MaintenanceTask[];
42
41
  idleBudgetMs?: number;
43
42
  migrateLegacy?: boolean;
43
+ env?: Readonly<Record<string, string | undefined>>;
44
+ }
45
+
46
+ function configuredIdleBudgetMs(options: StartPackedDaemonOptions): number | undefined {
47
+ if (options.idleBudgetMs !== undefined) return options.idleBudgetMs;
48
+ const raw = (options.env ?? process.env)[ENV.IDLE_SECS];
49
+ if (raw === undefined) return undefined;
50
+ const seconds = Number(raw);
51
+ return Number.isFinite(seconds) && seconds >= 0 ? seconds * 1_000 : undefined;
44
52
  }
45
53
 
46
54
  export function daemonOptions(options: StartPackedDaemonOptions): StartDaemonOptions {
@@ -99,13 +107,14 @@ export function daemonOptions(options: StartPackedDaemonOptions): StartDaemonOpt
99
107
  },
100
108
  ];
101
109
  const maintenanceTasks = configuredMaintenanceTasks.filter((task) => task.intervalMs > 0);
110
+ const idleBudgetMs = configuredIdleBudgetMs(options);
102
111
 
103
112
  return {
104
113
  daemonLabel: "Packed",
105
114
  handlePath: paths.handle,
106
115
  logger,
107
116
  maintenanceTasks,
108
- idleBudgetMs: options.idleBudgetMs ?? envMs(ENV.IDLE_SECS, IDLE_BUDGET_DEFAULT_MS),
117
+ ...(idleBudgetMs === undefined ? {} : { idleBudgetMs }),
109
118
  idleTickMs: WATCHDOG_TICK_MS,
110
119
  buildApp: () => createApp({ reg, inst, token, stateDir: paths.stateDirectory, dataDir: dirname(paths.database), piHome, daemonServiceInstaller }),
111
120
  onShutdown: () => database.close(),
@@ -239,7 +239,7 @@ export async function ensureClient(deps: EnsureClientDeps): Promise<PackedClient
239
239
  const waitedSeconds = (attempts * delayMs) / 1000;
240
240
  throw new Error(
241
241
  serviceInstalled
242
- ? `Packed daemon did not become ready within ${waitedSeconds} seconds, but a managed service is installed -- run packed doctor rather than auto-spawning a second one`
242
+ ? `The managed Packed service is registered but unreachable after ${waitedSeconds} 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.`
243
243
  : `Packed daemon did not become ready within ${waitedSeconds} seconds`,
244
244
  );
245
245
  }
@@ -15,6 +15,7 @@ import {
15
15
  import { createLogger } from "../shared/log.ts";
16
16
 
17
17
  const log = createLogger("registry");
18
+ const README_MAX_CHARS = 50_000;
18
19
 
19
20
  /** Upstream etiquette: honor Retry-After on 429, exponential backoff
20
21
  * otherwise, give up after RETRY_MAX_ATTEMPTS. */
@@ -125,9 +126,11 @@ export class HttpRegistry implements Registry {
125
126
  pi?: Record<string, unknown>;
126
127
  peerDependencies?: Record<string, string>;
127
128
  scripts?: Record<string, string>;
129
+ readme?: unknown;
128
130
  dist?: { unpackedSize?: number; integrity?: string; attestations?: { url?: string; provenance?: unknown } };
129
131
  };
130
132
  const manifestFields = v.pi ? Object.keys(v.pi).filter((key) => ["extensions", "skills", "prompts", "themes"].includes(key)) : [];
133
+ const readme = boundedString(v.readme, README_MAX_CHARS);
131
134
  return {
132
135
  name: boundedString(v.name, 214) ?? boundedString(name, 214)!,
133
136
  version: boundedString(v.version, 128) ?? "",
@@ -144,7 +147,8 @@ export class HttpRegistry implements Registry {
144
147
  pi: boundedPiManifest(v.pi),
145
148
  peerDependencies: boundedDependencies(v.peerDependencies),
146
149
  scripts: boundedDependencies(v.scripts),
147
- readmeAvailable: false,
150
+ readme,
151
+ readmeAvailable: readme !== undefined && readme.trim().length > 0,
148
152
  unpackedSize: v.dist?.unpackedSize,
149
153
  packageEvidence:
150
154
  manifestFields.length > 0
@@ -69,6 +69,7 @@ describe("HttpRegistry", () => {
69
69
  license: "MIT",
70
70
  keywords: ["pi-package", "lsp"],
71
71
  pi: { extensions: ["./src/index.ts"] },
72
+ readme: `# pi-lsp\n\n${"r".repeat(60_000)}`,
72
73
  dist: {
73
74
  unpackedSize: 12345,
74
75
  integrity: "sha512-test",
@@ -103,8 +104,10 @@ describe("HttpRegistry", () => {
103
104
  repository: "git+https://github.com/x/pi-lsp.git",
104
105
  license: "MIT",
105
106
  unpackedSize: 12345,
106
- readmeAvailable: false,
107
+ readmeAvailable: true,
107
108
  });
109
+ expect(info.readme).toHaveLength(50_000);
110
+ expect(info.readme).toStartWith("# pi-lsp");
108
111
  expect(info.pi?.extensions).toBeDefined();
109
112
  expect(info.packageEvidence).toMatchObject({ shape: "manifest", verified: false });
110
113
  expect(info.publication).toEqual({
@@ -104,6 +104,30 @@ class RecordingDaemonServiceInstaller {
104
104
  }
105
105
 
106
106
  describe("startPackedDaemon's own maintenance-task wiring (self-heals Vehicle drift, not just per-package on demand)", () => {
107
+ it("leaves the idle budget unspecified when no Packed override exists, so Vehicle can keep a service-launched daemon always on", () => {
108
+ const options = daemonOptions({
109
+ paths: fakePaths(),
110
+ reg: registry,
111
+ inst: installer,
112
+ piHome: fakePiHome([]),
113
+ maintenanceTasks: [],
114
+ env: {},
115
+ });
116
+ expect(options.idleBudgetMs).toBeUndefined();
117
+ });
118
+
119
+ it("honors an explicit Packed idle override without replacing Vehicle's provenance default", () => {
120
+ const options = daemonOptions({
121
+ paths: fakePaths(),
122
+ reg: registry,
123
+ inst: installer,
124
+ piHome: fakePiHome([]),
125
+ maintenanceTasks: [],
126
+ env: { PI_PACKED_IDLE_SECS: "17" },
127
+ });
128
+ expect(options.idleBudgetMs).toBe(17_000);
129
+ });
130
+
107
131
  it("includes a vehicle-reconcile task, defaulting to RECONCILE_INTERVAL_DEFAULT_MS", () => {
108
132
  const piHome = fakePiHome([]);
109
133
  const paths = fakePaths();
@@ -0,0 +1,90 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from "bun:test";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import type { Server } from "bun";
6
+ import type { Installer, UpdateOutcome } from "../src/packages/package.ts";
7
+ import { PackedClient } from "../src/public/client.ts";
8
+ import { HttpRegistry } from "../src/registry/registry.ts";
9
+ import { createApp } from "../src/daemon/service.ts";
10
+
11
+ const TOKEN = "e".repeat(64);
12
+ const PUBLISHED_README = `# @danypops/enigma
13
+
14
+ Encrypted credential vault and supervisor daemon. Holds delegated OAuth/API
15
+ credentials for other daemons and injects them as environment variables into
16
+ spawned child processes.
17
+
18
+ ## Why this exists
19
+
20
+ Consumers receive only their scoped credentials.`;
21
+
22
+ class NoopInstaller implements Installer {
23
+ async install(): Promise<string> {
24
+ throw new Error("not exercised");
25
+ }
26
+ async remove(): Promise<string> {
27
+ throw new Error("not exercised");
28
+ }
29
+ async update(): Promise<UpdateOutcome> {
30
+ throw new Error("not exercised");
31
+ }
32
+ }
33
+
34
+ describe("published npm metadata E2E", () => {
35
+ let npmServer: Server<undefined>;
36
+ let daemonServer: Server<undefined>;
37
+ let stateDir: string;
38
+ const npmRequests: Array<{ path: string; accept: string | null }> = [];
39
+
40
+ beforeAll(() => {
41
+ stateDir = mkdtempSync(join(tmpdir(), "packed-npm-metadata-"));
42
+ npmServer = Bun.serve({
43
+ hostname: "127.0.0.1",
44
+ port: 0,
45
+ fetch(request) {
46
+ const url = new URL(request.url);
47
+ npmRequests.push({ path: url.pathname, accept: request.headers.get("accept") });
48
+ if (url.pathname.endsWith("/latest")) {
49
+ return Response.json({
50
+ name: "@danypops/enigma",
51
+ version: "0.22.1",
52
+ description: "Encrypted credential vault daemon",
53
+ license: "MIT",
54
+ repository: { type: "git", url: "git+https://github.com/DanyPops/enigma.git" },
55
+ readme: PUBLISHED_README,
56
+ dist: { integrity: "sha512-published-fixture" },
57
+ });
58
+ }
59
+ return new Response("not found", { status: 404 });
60
+ },
61
+ });
62
+ const app = createApp({
63
+ reg: new HttpRegistry(`http://127.0.0.1:${npmServer.port}`, 20, 0, 1),
64
+ inst: new NoopInstaller(),
65
+ token: TOKEN,
66
+ stateDir,
67
+ });
68
+ daemonServer = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: app.fetch });
69
+ });
70
+
71
+ afterAll(() => {
72
+ daemonServer.stop(true);
73
+ npmServer.stop(true);
74
+ rmSync(stateDir, { recursive: true, force: true });
75
+ });
76
+
77
+ it("preserves an npm-published README through HttpRegistry, daemon RPC, and PackedClient", async () => {
78
+ const client = new PackedClient(`http://127.0.0.1:${daemonServer.port}`, TOKEN);
79
+ const info = await client.info("@danypops/enigma");
80
+
81
+ expect(npmRequests).toEqual([{ path: "/%40danypops/enigma/latest", accept: "application/json" }]);
82
+ expect(info).toMatchObject({
83
+ name: "@danypops/enigma",
84
+ version: "0.22.1",
85
+ license: "MIT",
86
+ readmeAvailable: true,
87
+ readme: PUBLISHED_README,
88
+ });
89
+ });
90
+ });
@@ -67,9 +67,10 @@ describe("ensureClient (packed daemon auto-spawn-or-wait decision)", () => {
67
67
  expect(connectAttempts).toBeGreaterThan(1); // genuinely retried, not a single shot
68
68
  });
69
69
 
70
- it("fails with a message pointing at the service, not a generic timeout, when a service is installed but never becomes reachable", async () => {
71
- await expect(
72
- ensureClient({
70
+ it("explains the supervisor failure and recovery when a managed service never becomes reachable", async () => {
71
+ let message = "";
72
+ try {
73
+ await ensureClient({
73
74
  connect: async () => {
74
75
  throw new Error("never reachable");
75
76
  },
@@ -80,8 +81,14 @@ describe("ensureClient (packed daemon auto-spawn-or-wait decision)", () => {
80
81
  sleep: async () => {},
81
82
  retryAttempts: 2,
82
83
  retryDelayMs: 0,
83
- }),
84
- ).rejects.toThrow(/managed service is installed/);
84
+ });
85
+ } catch (error) {
86
+ message = error instanceof Error ? error.message : String(error);
87
+ }
88
+ expect(message).toContain("managed Packed service is registered but unreachable");
89
+ expect(message).toContain("restart the managed service");
90
+ expect(message).toContain("will not auto-spawn a competing process");
91
+ expect(message).not.toContain("packed doctor");
85
92
  });
86
93
 
87
94
  it("fails with a plain timeout message, and did spawn, when no service is installed and nothing ever becomes reachable", async () => {