@danypops/pi-packed 0.27.8 → 0.27.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.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{spawn as
|
|
1
|
+
import{spawn as f}from"node:child_process";import{accessSync as I,constants as S,readFileSync as U}from"node:fs";import{delimiter as y,dirname as k,join as V,resolve as P}from"node:path";import{fileURLToPath as u}from"node:url";class q{baseUrl;token;opsPath;transport;label;constructor(z,G,J){this.baseUrl=z;this.token=G;this.label=J.label,this.opsPath=J.opsPath??"/api/v1/ops",this.transport=J.transport??fetch}authedRequest(z,G={}){return this.transport(new Request(`${this.baseUrl}${z}`,{...G,headers:{...G.headers,authorization:`Bearer ${this.token}`}}))}async call(z,G){let J=await this.authedRequest(this.opsPath,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({op:z,input:G})}),Z=await J.json();if(!J.ok)throw Error(Z.error??`${this.label} operation failed with HTTP ${J.status}`);return Z.result}async operations(){let z=await this.authedRequest(this.opsPath),G=await z.json();if(!z.ok)throw Error(G.error??`${this.label} discovery failed with HTTP ${z.status}`);return G.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"),G=await z.json();if(!z.ok||G.ok!==!0||typeof G.version!=="string")throw Error(G.error??`${this.label} health check failed`);return{ok:!0,version:G.version}}}import{chmodSync as p,existsSync as o,mkdirSync as n,readFileSync as R,renameSync as t,rmSync as a,writeFileSync as s}from"node:fs";import{homedir as b,tmpdir as L}from"node:os";import{dirname as Gz,join as W,win32 as Y}from"node:path";var M="127.0.0.1",X="metrics.sqlite";function O(z,G={}){let J=G.platform??process.platform,Z=G.home??b();if(J==="darwin")return g(z,Z);if(J==="win32")return A(z,G.env??process.env,Z);return E(z,G,Z)}function E(z,G,J){let Z=G.env??process.env,$=G.uid??process.getuid?.()??0,K=Z.XDG_DATA_HOME??W(J,".local","share"),Q=Z.XDG_STATE_HOME??W(J,".local","state"),_=Z.XDG_RUNTIME_DIR??W("/run","user",String($)),D=Z.XDG_CONFIG_HOME??W(J,".config");return{database:W(K,z.stateDirectoryName,z.databaseFilename),metrics:W(K,z.stateDirectoryName,z.metricsFilename??X),token:W(Q,z.stateDirectoryName,z.tokenFilename),handle:W(_,z.stateDirectoryName,z.handleFilename),serviceDescriptor:W(D,"systemd","user",z.systemdUnitName)}}function g(z,G){let J=W(G,"Library"),Z=W(J,"Application Support",z.stateDirectoryName);return{database:W(Z,z.databaseFilename),metrics:W(Z,z.metricsFilename??X),token:W(Z,z.tokenFilename),handle:W(L(),z.stateDirectoryName,z.handleFilename),serviceDescriptor:W(Z,z.systemdUnitName)}}function A(z,G,J){let Z=G.LOCALAPPDATA??Y.join(J,"AppData","Local"),$=G.APPDATA??Y.join(J,"AppData","Roaming"),K=Y.join(Z,z.stateDirectoryName,"Data");return{database:Y.join(K,z.databaseFilename),metrics:Y.join(K,z.metricsFilename??X),token:Y.join(K,z.tokenFilename),handle:Y.join(Z,"Temp",z.stateDirectoryName,z.handleFilename),serviceDescriptor:Y.join($,z.stateDirectoryName,"Config",z.systemdUnitName)}}function j(z){try{let G=JSON.parse(R(z,"utf8"));if(G.host!==M||!Number.isInteger(G.port)||G.port<1||G.port>65535||!Number.isInteger(G.pid)||G.tokenPath!==void 0&&typeof G.tokenPath!=="string")return null;return G}catch{return null}}import{execFileSync as N}from"node:child_process";import{fileURLToPath as w}from"node:url";function H(z,G){let J=G.runCommand(process.execPath,[G.armadaCliPath,"status","--json"]);if(!J.ok)return!1;try{return JSON.parse(J.output).vehicles?.some(($)=>$.name===z)??!1}catch{return!1}}function F(){return{armadaCliPath:w(import.meta.resolve("@danypops/armada/cli")),runCommand:(z,G,J)=>{try{return{ok:!0,output:N(z,G,{encoding:"utf8",input:J,stdio:[J===void 0?"ignore":"pipe","pipe","pipe"]})}}catch(Z){let $=Z;return{ok:!1,output:($.stdout??"")+($.stderr??$.message)}}}}}class x extends Error{notADaemon;constructor(z,G){super(z);this.notADaemon=G;this.name="InstallServiceError"}}var v="pi-packed",T="pi-packed.service";function B(z={}){let G=z.env??process.env;if(G.PI_PACKED_HOME){let Z=P(G.PI_PACKED_HOME);return{token:`${Z}/token`,handle:`${Z}/handle.json`,serviceDescriptor:`${Z}/${T}`}}let J=O({stateDirectoryName:"pi-packed",databaseFilename:"packages.db",tokenFilename:"token",handleFilename:"handle.json",systemdUnitName:T},z);return{token:J.token,handle:J.handle,serviceDescriptor:J.serviceDescriptor}}function Oz(z=B()){let G=j(z.handle);if(!G)return;let J;try{J=U(z.token,"utf8").trim()}catch{return}if(!/^[a-f0-9]{64}$/.test(J))return;return{baseUrl:`http://${G.host}:${G.port}`,token:J}}function h(){return H(v,F())}function m(z,G=30000){return(J)=>z(new Request(J,{signal:AbortSignal.any([J.signal,AbortSignal.timeout(G)])}))}class C{rpc;constructor(z,G,J=fetch){this.rpc=new q(z,G,{label:"Packed",transport:m(J)})}call(z,G){return this.rpc.call(z,G)}health(){return this.rpc.health()}search(z,G,J=!1){return this.call("package.search",{query:z,limit:G,offline:J})}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,G=!1){return this.call("package.security.set",{mutationApproval:z,approved:G})}async install(z,G=!1){let J=await this.call("package.install",{source:z,approved:G});if(!J.ok)throw Error(J.output);return J.output}async installService(z,G=!1){let J=await this.call("package.install_service",{source:z,approved:G});if(!J.ok)throw new x(J.output,J.notADaemon);return{output:J.output,spec:J.spec}}async remove(z,G=!1){let J=await this.call("package.remove",{name:z,approved:G});if(!J.ok)throw Error(J.output);return J.output}async update(z,G=!1,J){let Z=await this.call("package.update",{source:z,approved:G,target:J});if(!Z.ok)throw Error(Z.output);return{output:Z.output,reloadRequired:Z.reloadRequired??!0,alreadyUpToDate:Z.alreadyUpToDate??!1,pinned:Z.pinned??!1,previousVersion:Z.previousVersion,currentVersion:Z.currentVersion,outOfRangeUpdateAvailable:Z.outOfRangeUpdateAvailable,pinnedSourceRequiresTarget:Z.pinnedSourceRequiresTarget,replaced:Z.replaced,before:Z.before,after:Z.after,rollback:Z.rollback}}setupPlan(z,G=!1){return this.call("setup.plan",{manifestPath:z,prune:G})}setupApply(z,G=!1,J=!1){return this.call("setup.apply",{manifestPath:z,approved:G,prune:J})}listResources(z){return this.call("resources.list",{projectRoot:z})}async toggleResource(z,G,J,Z,$,K=!1){let Q=await this.call("resources.toggle",{source:z,field:G,path:J,enabled:Z,projectRoot:$,approved:K});if(!Q.ok)throw Error(Q.output);return Q.output}piStatus(){return this.call("pi.status",{})}}async function d(z=B(),G=fetch){let J=j(z.handle);if(!J)throw Error("Packed daemon is not running");let Z;try{Z=U(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 C(`http://${J.host}:${J.port}`,Z,G);return await $.health(),$}async function c(z){try{return await z.connect()}catch{}let G=z.retryAttempts??30,J=z.retryDelayMs??100,Z=z.isServiceInstalled();if(!Z)z.spawn();for(let K=0;K<G;K++){await z.sleep(J);try{return await z.connect()}catch{}}let $=G*J/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 l(z=process.env){if(z.PI_PACKED_PI_BIN||z.PI_BIN)return;let G=z.PATH;if(!G)return;let J=process.platform==="win32"?["pi.exe","pi.cmd","pi.bat","pi"]:["pi"];for(let Z of G.split(y)){if(!Z)continue;for(let $ of J){let K=V(Z,$);try{return I(K,S.X_OK),K}catch{}}}return}async function Hz(z=B(),G=fetch){return c({connect:()=>d(z,G),isServiceInstalled:()=>h(),spawn:()=>{let J=process.env.PI_PACKED_BIN,Z=J??process.env.PI_PACKED_BUN??"bun",$=J?["serve"]:[V(k(u(import.meta.url)),"../service/src/cli/cli.ts"),"serve"],K=l(),Q=f(Z,$,{detached:!0,stdio:"ignore",env:{...process.env,DAEMON_KIT_LAUNCH_PROVENANCE:"auto-spawn",...K?{PI_BIN:K}:{}}});Q.once("error",()=>{}),Q.unref()},sleep:(J)=>new Promise((Z)=>setTimeout(Z,J))})}export{Oz as resolveVehicleClientTarget,l as resolvePiBinForSpawn,B as resolvePackedClientPaths,Hz as ensurePackedClient,c as ensureClient,d as connectPackedClient,C as PackedClient,x as InstallServiceError};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-packed",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.10",
|
|
4
4
|
"description": "Pi package lifecycle, validation, daemon, tools, profiles, and TUI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,12 +27,12 @@
|
|
|
27
27
|
"typecheck": "bunx tsc --noEmit"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@danypops/armada": "^0.
|
|
30
|
+
"@danypops/armada": "^0.5.0",
|
|
31
31
|
"@danypops/packed": "^0.7.0",
|
|
32
32
|
"@danypops/pi-extension-harness": "^0.7.0",
|
|
33
|
-
"@danypops/vehicle-client": "^0.10.
|
|
33
|
+
"@danypops/vehicle-client": "^0.10.3",
|
|
34
34
|
"@danypops/vehicle-core": "^0.17.1",
|
|
35
|
-
"@danypops/vehicle-server": "^0.
|
|
35
|
+
"@danypops/vehicle-server": "^0.25.2",
|
|
36
36
|
"jiti": "^2.7.0",
|
|
37
37
|
"malevich-tui-components": "^0.21.1",
|
|
38
38
|
"publint": "0.3.22",
|
|
@@ -40,11 +40,11 @@
|
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@danypops/pi-tui-harness": "^0.0.1",
|
|
43
|
-
"@danypops/vehicle-client-pi": "^0.
|
|
43
|
+
"@danypops/vehicle-client-pi": "^0.43.1",
|
|
44
44
|
"@types/semver": "^7.7.1"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"@danypops/vehicle-client-pi": "^0.
|
|
47
|
+
"@danypops/vehicle-client-pi": "^0.43.1",
|
|
48
48
|
"@earendil-works/pi-coding-agent": "*",
|
|
49
49
|
"@earendil-works/pi-tui": "*",
|
|
50
50
|
"typebox": "*"
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
startDaemon,
|
|
9
9
|
} from "@danypops/vehicle-server/daemon";
|
|
10
10
|
import { ensureAuthToken } from "@danypops/vehicle-server/paths";
|
|
11
|
+
import { openVehicleMetricsStore } from "@danypops/vehicle-server/metrics";
|
|
11
12
|
import { captureLoadedModules, checkModuleFreshnessAll, ownRuntimeDependencyNames } from "../adoption/module-freshness.ts";
|
|
12
13
|
import {
|
|
13
14
|
type DaemonServiceInstaller,
|
|
@@ -81,6 +82,7 @@ export function daemonOptions(options: StartPackedDaemonOptions): StartDaemonOpt
|
|
|
81
82
|
const reg = options.reg ?? new HttpRegistry();
|
|
82
83
|
const piHome = options.piHome ?? defaultPiHome();
|
|
83
84
|
const database = openDb(paths.database);
|
|
85
|
+
const vehicleMetrics = openVehicleMetricsStore(paths.metrics);
|
|
84
86
|
const moduleSnapshot = captureOwnModuleSnapshot();
|
|
85
87
|
// Wires ExecInstaller.update()'s own out-of-range cross-check to the exact
|
|
86
88
|
// same registry-mirror source of truth the package-update-check task below
|
|
@@ -180,8 +182,12 @@ export function daemonOptions(options: StartPackedDaemonOptions): StartDaemonOpt
|
|
|
180
182
|
piHome,
|
|
181
183
|
daemonServiceInstaller,
|
|
182
184
|
moduleFreshness: () => checkModuleFreshnessAll(moduleSnapshot),
|
|
185
|
+
vehicleMetrics,
|
|
183
186
|
}),
|
|
184
|
-
onShutdown: () =>
|
|
187
|
+
onShutdown: () => {
|
|
188
|
+
database.close();
|
|
189
|
+
vehicleMetrics.close();
|
|
190
|
+
},
|
|
185
191
|
};
|
|
186
192
|
}
|
|
187
193
|
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
import { existsSync as fileExistsSync } from "node:fs";
|
|
8
8
|
import { VehicleRegistry } from "@danypops/vehicle-server";
|
|
9
|
+
import { createVehicleMetricsMiddleware } from "@danypops/vehicle-server/metrics-middleware";
|
|
10
|
+
import { registerVehicleMetricsOperations } from "@danypops/vehicle-server/metrics-operations";
|
|
11
|
+
import type { VehicleMetricsStore } from "@danypops/vehicle-server/metrics";
|
|
9
12
|
import { createVehicleHttpApp } from "@danypops/vehicle-server/http";
|
|
10
13
|
import { errorResponse, healthResponse, jsonResponse, readyResponse, requireBearerToken } from "@danypops/vehicle-server/rpc-http";
|
|
11
14
|
import type { ServiceSpec } from "@danypops/vehicle-server/service";
|
|
@@ -88,6 +91,8 @@ export interface Deps {
|
|
|
88
91
|
moduleFreshness?: () => ModuleFreshnessDiagnostic[];
|
|
89
92
|
piVersion?: { check(options?: { timeoutMs?: number }): Promise<PiVersionReport> };
|
|
90
93
|
advisories?: { scan(installed: Record<string, string>): Promise<AdvisoryReport> };
|
|
94
|
+
/** Tool/operation usage metrics store -- see the vehicleRegistry construction below. Opened/closed by daemon.ts. */
|
|
95
|
+
vehicleMetrics?: VehicleMetricsStore;
|
|
91
96
|
}
|
|
92
97
|
|
|
93
98
|
export type OperationName =
|
|
@@ -287,6 +292,16 @@ export function createApp(deps: Deps): { fetch: (req: Request) => Promise<Respon
|
|
|
287
292
|
// is on disk at startup, then tracks every /security POST from here on (see below) --
|
|
288
293
|
// never a fixed per-deployment constant baked in once and forgotten.
|
|
289
294
|
vehicleRegistry.configureApprovals({ enabled: readSecuritySettings(deps.stateDir).mutationApproval === "always" });
|
|
295
|
+
// Records how often each real operation is invoked (server-side, every caller) plus, via
|
|
296
|
+
// metrics.recordClientEvent, client-observed Vehicle Shell meta-tool calls -- see
|
|
297
|
+
// @danypops/vehicle-server's own metrics README section. The store itself is opened/closed by
|
|
298
|
+
// daemon.ts (same lifecycle as the packages db), not here -- createApp() only wires it onto
|
|
299
|
+
// this registry when given one, so a caller that omits vehicleMetrics (e.g. a unit test) is
|
|
300
|
+
// unaffected.
|
|
301
|
+
if (deps.vehicleMetrics) {
|
|
302
|
+
vehicleRegistry.useExecutionMiddleware(createVehicleMetricsMiddleware(deps.vehicleMetrics, "packed"));
|
|
303
|
+
registerVehicleMetricsOperations(vehicleRegistry, deps.vehicleMetrics, "packed");
|
|
304
|
+
}
|
|
290
305
|
const vehicleApp = createVehicleHttpApp({ registry: vehicleRegistry, token: deps.token });
|
|
291
306
|
|
|
292
307
|
function authorize(operation: PackageOperation, approved: boolean): Response | undefined {
|
|
@@ -26,6 +26,7 @@ export function resolvePackedPaths(options: PathEnvironment = {}): PackedPaths {
|
|
|
26
26
|
token: join(directory, PATH_NAMES.tokenFilename),
|
|
27
27
|
handle: join(directory, PATH_NAMES.handleFilename),
|
|
28
28
|
serviceDescriptor: join(directory, PATH_NAMES.systemdUnitName),
|
|
29
|
+
metrics: join(directory, "metrics.sqlite"),
|
|
29
30
|
stateDirectory: directory,
|
|
30
31
|
};
|
|
31
32
|
}
|