feat: expose package management capability

This commit is contained in:
Federico Jaramillo Martinez
2026-07-01 22:13:02 +02:00
parent 0a1b7f9699
commit 332db5faeb
11 changed files with 127 additions and 46 deletions
+3 -3
View File
@@ -127,10 +127,10 @@ describe("buildApp", () => {
packageName: "@jmfederico/pi-web",
generatedAt: "2026-05-25T00:00:00.000Z",
components: {
web: { component: "web", label: "Remote Web", runtimeVersion: "1.0.0", available: true, capabilities: [PI_WEB_CAPABILITIES.sessionsDeleteArchived] },
web: { component: "web", label: "Remote Web", runtimeVersion: "1.0.0", available: true, capabilities: [PI_WEB_CAPABILITIES.sessionsDeleteArchived, PI_WEB_CAPABILITIES.piPackagesManage, "future.capability"] },
sessiond: { component: "sessiond", label: "Remote Sessiond", runtimeVersion: "1.0.0", available: true, capabilities: [PI_WEB_CAPABILITIES.sessionsDeleteArchived] },
},
capabilities: [PI_WEB_CAPABILITIES.sessionsDeleteArchived],
capabilities: [PI_WEB_CAPABILITIES.sessionsDeleteArchived, PI_WEB_CAPABILITIES.piPackagesManage, "future.capability"],
},
}));
remoteClient = fakeRemoteClient({ requestJson });
@@ -138,7 +138,7 @@ describe("buildApp", () => {
const runtime = await app.inject({ method: "GET", url: `/api/machines/${remote.id}/runtime` });
expect(runtime.statusCode).toBe(200);
expect(runtime.json()).toMatchObject({ machineId: remote.id, ok: true, capabilities: [PI_WEB_CAPABILITIES.sessionsDeleteArchived] });
expect(runtime.json()).toMatchObject({ machineId: remote.id, ok: true, capabilities: [PI_WEB_CAPABILITIES.sessionsDeleteArchived, PI_WEB_CAPABILITIES.piPackagesManage] });
expect(requestJson).toHaveBeenCalledWith("GET", "/api/pi-web/runtime", undefined, { timeoutMs: 3000 });
});
+3 -25
View File
@@ -1,5 +1,5 @@
import type { Machine, MachineHealth, MachineRuntime, PiWebComponentStatus, PiWebRuntimeComponent, PiWebRuntimeResponse, PiWebStatusResponse } from "../../shared/apiTypes.js";
import { isPiWebCapability } from "../../shared/capabilities.js";
import { parsePiWebRuntimeResponse } from "../../shared/piWebStatusParsing.js";
import { getPiWebRuntime } from "../piWebStatus.js";
import { DEFAULT_REMOTE_HEALTH_TIMEOUT_MS, RemoteMachineClient, type MachineClient, validateConfiguredMachineHeaders } from "./machineClient.js";
import { MachineStore, type StoredMachine } from "./machineStore.js";
@@ -151,7 +151,8 @@ export class MachineService {
const checkedAt = this.now().toISOString();
try {
const response = await this.clientFor(machine).requestJson("GET", "/api/pi-web/runtime", undefined, { timeoutMs: DEFAULT_REMOTE_HEALTH_TIMEOUT_MS });
if (response.statusCode >= 200 && response.statusCode < 300 && isPiWebRuntimeResponse(response.body)) return machineRuntime(id, checkedAt, response.body);
const runtime = parsePiWebRuntimeResponse(response.body);
if (response.statusCode >= 200 && response.statusCode < 300 && runtime !== undefined) return machineRuntime(id, checkedAt, runtime);
return { machineId: id, ok: false, checkedAt, error: `Remote runtime returned HTTP ${String(response.statusCode)}` };
} catch (error) {
return { machineId: id, ok: false, checkedAt, error: errorMessage(error) };
@@ -241,16 +242,6 @@ function isPiWebStatusResponse(value: unknown): value is PiWebStatusResponse {
return isPiWebComponentStatus(components["web"]) && isPiWebComponentStatus(components["sessiond"]);
}
function isPiWebRuntimeResponse(value: unknown): value is PiWebRuntimeResponse {
if (!isRecord(value)) return false;
const packageName = value["packageName"];
const generatedAt = value["generatedAt"];
const components = value["components"];
const capabilities = value["capabilities"];
if (typeof packageName !== "string" || typeof generatedAt !== "string" || !isRecord(components) || !isPiWebCapabilityArray(capabilities)) return false;
return isPiWebRuntimeComponent(components["web"]) && isPiWebRuntimeComponent(components["sessiond"]);
}
function isPiWebComponentStatus(value: unknown): value is PiWebComponentStatus {
if (!isRecord(value)) return false;
const component = value["component"];
@@ -260,19 +251,6 @@ function isPiWebComponentStatus(value: unknown): value is PiWebComponentStatus {
&& typeof value["available"] === "boolean";
}
function isPiWebRuntimeComponent(value: unknown): boolean {
if (!isRecord(value)) return false;
const component = value["component"];
return (component === "web" || component === "sessiond")
&& typeof value["label"] === "string"
&& typeof value["available"] === "boolean"
&& isPiWebCapabilityArray(value["capabilities"]);
}
function isPiWebCapabilityArray(value: unknown): boolean {
return Array.isArray(value) && value.every(isPiWebCapability);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
+19 -1
View File
@@ -2,9 +2,10 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { comparePackageVersions, getPiWebStatus, getPiWebVersionStatus } from "./piWebStatus.js";
import { comparePackageVersions, getPiWebRuntime, getPiWebStatus, getPiWebVersionStatus } from "./piWebStatus.js";
import { SessionDaemonClient } from "../sessiond/sessionDaemonClient.js";
import type { PiWebComponentStatus } from "../shared/apiTypes.js";
import { PI_WEB_CAPABILITIES } from "../shared/capabilities.js";
const originalSkipVersionCheck = process.env["PI_WEB_SKIP_VERSION_CHECK"];
const originalHome = process.env["HOME"];
@@ -40,6 +41,23 @@ describe("PI WEB status", () => {
expect(status).not.toHaveProperty("release");
});
it("reports Pi package management as a web runtime capability", async () => {
const daemon = daemonWithComponent({
component: "sessiond",
label: "Session daemon",
runtimeVersion: "1.202605.7",
installedVersion: "1.202605.8",
stale: true,
available: true,
});
const runtime = await getPiWebRuntime(daemon);
expect(runtime.components.web.capabilities).toContain(PI_WEB_CAPABILITIES.piPackagesManage);
expect(runtime.components.sessiond.capabilities).not.toContain(PI_WEB_CAPABILITIES.piPackagesManage);
expect(runtime.capabilities).toContain(PI_WEB_CAPABILITIES.piPackagesManage);
});
it("reports stale session daemon versions as messages", async () => {
process.env["PI_WEB_SKIP_VERSION_CHECK"] = "1";
const daemon = daemonWithComponent({