Archived
feat: expose package management capability
This commit is contained in:
@@ -2,4 +2,4 @@
|
|||||||
"@jmfederico/pi-web": patch
|
"@jmfederico/pi-web": patch
|
||||||
---
|
---
|
||||||
|
|
||||||
Keep gateway Settings panels responsive while selected-machine Pi packages load or fail separately.
|
Keep gateway Settings panels responsive while selected-machine Pi packages load or fail separately, and report Pi package-management support through runtime capabilities.
|
||||||
|
|||||||
@@ -24,11 +24,11 @@ describe("API parsers", () => {
|
|||||||
packageName: "@jmfederico/pi-web",
|
packageName: "@jmfederico/pi-web",
|
||||||
generatedAt: "now",
|
generatedAt: "now",
|
||||||
components: {
|
components: {
|
||||||
web: { component: "web", label: "Web/UI", runtimeVersion: "1.0.0", available: true, capabilities: [PI_WEB_CAPABILITIES.sessionsDeleteArchived] },
|
web: { component: "web", label: "Web/UI", runtimeVersion: "1.0.0", available: true, capabilities: [PI_WEB_CAPABILITIES.sessionsDeleteArchived, PI_WEB_CAPABILITIES.piPackagesManage, "future.capability"] },
|
||||||
sessiond: { component: "sessiond", label: "Session daemon", runtimeVersion: "1.0.0", available: true, capabilities: [PI_WEB_CAPABILITIES.sessionsDeleteArchived] },
|
sessiond: { component: "sessiond", label: "Session daemon", 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"],
|
||||||
})).toMatchObject({ capabilities: [PI_WEB_CAPABILITIES.sessionsDeleteArchived] });
|
})).toMatchObject({ capabilities: [PI_WEB_CAPABILITIES.sessionsDeleteArchived, PI_WEB_CAPABILITIES.piPackagesManage] });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("parses Pi package list and mutation responses", () => {
|
it("parses Pi package list and mutation responses", () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ArchiveSessionsResponse, AuthProviderOption, AuthProviderStatus, AuthProvidersResponse, AuthStatusSource, AuthType, CommandOption, CommandResult, DeleteWorkspaceFileResponse, FileContentResponse, FileSuggestion, FileTreeEntry, FileTreeResponse, GitDiffResponse, GitFileState, GitStatusFile, GitStatusResponse, Machine, MachineHealth, MachineKind, MachineRuntime, MachineStatus, MessagePage, ModelSelectionResponse, MoveWorkspaceFileResponse, OAuthFlowState, PiWebCapability, PiWebComponentStatus, PiWebConfigEnvOverrides, PiWebConfigResponse, PiWebConfigValues, PiWebInstallationInfo, PiWebPluginConfigMap, PiWebPluginInfo, PiWebPluginsResponse, PiWebPluginScope, PiWebReleaseStatus, PiWebRuntimeComponent, PiWebRuntimeResponse, PiWebServiceComponent, PiWebShortcutConfig, PiWebStatusMessage, PiWebStatusResponse, PiWebStatusSeverity, Project, QueuedSessionMessage, SavedPromptAttachment, SessionBulkArchiveResponse, SessionBulkDeleteArchivedResponse, SessionBulkFailure, SessionCleanupExecuteResponse, SessionCleanupPreviewResponse, SessionCleanupProjectSummary, SessionCleanupThresholds, SessionCleanupTotals, SessionInfo, SessionModel, SessionStatus, SlashCommand, TerminalCommandRun, TerminalCommandRunStatus, TerminalInfo, ThinkingLevelsResponse, WriteWorkspaceFileResponse, Workspace, WorkspaceActivity, WorkspaceActivityResponse } from "../../../shared/apiTypes";
|
import type { ArchiveSessionsResponse, AuthProviderOption, AuthProviderStatus, AuthProvidersResponse, AuthStatusSource, AuthType, CommandOption, CommandResult, DeleteWorkspaceFileResponse, FileContentResponse, FileSuggestion, FileTreeEntry, FileTreeResponse, GitDiffResponse, GitFileState, GitStatusFile, GitStatusResponse, Machine, MachineHealth, MachineKind, MachineRuntime, MachineStatus, MessagePage, ModelSelectionResponse, MoveWorkspaceFileResponse, OAuthFlowState, PiWebCapability, PiWebComponentStatus, PiWebConfigEnvOverrides, PiWebConfigResponse, PiWebConfigValues, PiWebInstallationInfo, PiWebPluginConfigMap, PiWebPluginInfo, PiWebPluginsResponse, PiWebPluginScope, PiWebReleaseStatus, PiWebRuntimeComponent, PiWebRuntimeResponse, PiWebServiceComponent, PiWebShortcutConfig, PiWebStatusMessage, PiWebStatusResponse, PiWebStatusSeverity, Project, QueuedSessionMessage, SavedPromptAttachment, SessionBulkArchiveResponse, SessionBulkDeleteArchivedResponse, SessionBulkFailure, SessionCleanupExecuteResponse, SessionCleanupPreviewResponse, SessionCleanupProjectSummary, SessionCleanupThresholds, SessionCleanupTotals, SessionInfo, SessionModel, SessionStatus, SlashCommand, TerminalCommandRun, TerminalCommandRunStatus, TerminalInfo, ThinkingLevelsResponse, WriteWorkspaceFileResponse, Workspace, WorkspaceActivity, WorkspaceActivityResponse } from "../../../shared/apiTypes";
|
||||||
import type { PiPackageInfo, PiPackageMutationAction, PiPackageMutationResponse, PiPackageScope, PiPackagesResponse } from "../../../shared/apiTypes";
|
import type { PiPackageInfo, PiPackageMutationAction, PiPackageMutationResponse, PiPackageScope, PiPackagesResponse } from "../../../shared/apiTypes";
|
||||||
import { isPiWebCapability } from "../../../shared/capabilities";
|
import { parseKnownPiWebCapabilities } from "../../../shared/capabilities";
|
||||||
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
return typeof value === "object" && value !== null;
|
return typeof value === "object" && value !== null;
|
||||||
@@ -810,8 +810,9 @@ function parsePiWebServiceComponent(value: unknown): PiWebServiceComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parsePiWebCapabilities(value: unknown): PiWebCapability[] {
|
function parsePiWebCapabilities(value: unknown): PiWebCapability[] {
|
||||||
if (!Array.isArray(value) || !value.every(isPiWebCapability)) throw new Error("Invalid PI WEB capabilities");
|
const capabilities = parseKnownPiWebCapabilities(value);
|
||||||
return value;
|
if (capabilities === undefined) throw new Error("Invalid PI WEB capabilities");
|
||||||
|
return capabilities;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parsePiWebStatusSeverity(value: unknown): PiWebStatusSeverity {
|
function parsePiWebStatusSeverity(value: unknown): PiWebStatusSeverity {
|
||||||
|
|||||||
@@ -127,10 +127,10 @@ describe("buildApp", () => {
|
|||||||
packageName: "@jmfederico/pi-web",
|
packageName: "@jmfederico/pi-web",
|
||||||
generatedAt: "2026-05-25T00:00:00.000Z",
|
generatedAt: "2026-05-25T00:00:00.000Z",
|
||||||
components: {
|
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] },
|
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 });
|
remoteClient = fakeRemoteClient({ requestJson });
|
||||||
@@ -138,7 +138,7 @@ describe("buildApp", () => {
|
|||||||
const runtime = await app.inject({ method: "GET", url: `/api/machines/${remote.id}/runtime` });
|
const runtime = await app.inject({ method: "GET", url: `/api/machines/${remote.id}/runtime` });
|
||||||
|
|
||||||
expect(runtime.statusCode).toBe(200);
|
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 });
|
expect(requestJson).toHaveBeenCalledWith("GET", "/api/pi-web/runtime", undefined, { timeoutMs: 3000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Machine, MachineHealth, MachineRuntime, PiWebComponentStatus, PiWebRuntimeComponent, PiWebRuntimeResponse, PiWebStatusResponse } from "../../shared/apiTypes.js";
|
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 { getPiWebRuntime } from "../piWebStatus.js";
|
||||||
import { DEFAULT_REMOTE_HEALTH_TIMEOUT_MS, RemoteMachineClient, type MachineClient, validateConfiguredMachineHeaders } from "./machineClient.js";
|
import { DEFAULT_REMOTE_HEALTH_TIMEOUT_MS, RemoteMachineClient, type MachineClient, validateConfiguredMachineHeaders } from "./machineClient.js";
|
||||||
import { MachineStore, type StoredMachine } from "./machineStore.js";
|
import { MachineStore, type StoredMachine } from "./machineStore.js";
|
||||||
@@ -151,7 +151,8 @@ export class MachineService {
|
|||||||
const checkedAt = this.now().toISOString();
|
const checkedAt = this.now().toISOString();
|
||||||
try {
|
try {
|
||||||
const response = await this.clientFor(machine).requestJson("GET", "/api/pi-web/runtime", undefined, { timeoutMs: DEFAULT_REMOTE_HEALTH_TIMEOUT_MS });
|
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)}` };
|
return { machineId: id, ok: false, checkedAt, error: `Remote runtime returned HTTP ${String(response.statusCode)}` };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { machineId: id, ok: false, checkedAt, error: errorMessage(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"]);
|
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 {
|
function isPiWebComponentStatus(value: unknown): value is PiWebComponentStatus {
|
||||||
if (!isRecord(value)) return false;
|
if (!isRecord(value)) return false;
|
||||||
const component = value["component"];
|
const component = value["component"];
|
||||||
@@ -260,19 +251,6 @@ function isPiWebComponentStatus(value: unknown): value is PiWebComponentStatus {
|
|||||||
&& typeof value["available"] === "boolean";
|
&& 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> {
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
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 { SessionDaemonClient } from "../sessiond/sessionDaemonClient.js";
|
||||||
import type { PiWebComponentStatus } from "../shared/apiTypes.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 originalSkipVersionCheck = process.env["PI_WEB_SKIP_VERSION_CHECK"];
|
||||||
const originalHome = process.env["HOME"];
|
const originalHome = process.env["HOME"];
|
||||||
@@ -40,6 +41,23 @@ describe("PI WEB status", () => {
|
|||||||
expect(status).not.toHaveProperty("release");
|
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 () => {
|
it("reports stale session daemon versions as messages", async () => {
|
||||||
process.env["PI_WEB_SKIP_VERSION_CHECK"] = "1";
|
process.env["PI_WEB_SKIP_VERSION_CHECK"] = "1";
|
||||||
const daemon = daemonWithComponent({
|
const daemon = daemonWithComponent({
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const PI_WEB_CAPABILITIES = {
|
|||||||
sessionsReload: "sessions.reload",
|
sessionsReload: "sessions.reload",
|
||||||
promptAttachments: "prompt.attachments",
|
promptAttachments: "prompt.attachments",
|
||||||
workspaceFileSuggestions: "workspace.fileSuggestions",
|
workspaceFileSuggestions: "workspace.fileSuggestions",
|
||||||
|
piPackagesManage: "piPackages.manage",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type PiWebCapability = typeof PI_WEB_CAPABILITIES[keyof typeof PI_WEB_CAPABILITIES];
|
export type PiWebCapability = typeof PI_WEB_CAPABILITIES[keyof typeof PI_WEB_CAPABILITIES];
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { effectivePiWebCapabilities, PI_WEB_CAPABILITIES, SESSIOND_RUNTIME_CAPABILITIES, WEB_RUNTIME_CAPABILITIES, parseKnownPiWebCapabilities } from "./capabilities";
|
||||||
|
|
||||||
|
describe("PI WEB capabilities", () => {
|
||||||
|
it("advertises Pi package management from the web runtime only", () => {
|
||||||
|
expect(WEB_RUNTIME_CAPABILITIES).toContain(PI_WEB_CAPABILITIES.piPackagesManage);
|
||||||
|
expect(SESSIOND_RUNTIME_CAPABILITIES).not.toContain(PI_WEB_CAPABILITIES.piPackagesManage);
|
||||||
|
|
||||||
|
expect(effectivePiWebCapabilities({
|
||||||
|
web: { available: true, capabilities: [PI_WEB_CAPABILITIES.piPackagesManage] },
|
||||||
|
sessiond: { available: false, capabilities: [] },
|
||||||
|
})).toEqual([PI_WEB_CAPABILITIES.piPackagesManage]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps only known string capabilities when parsing runtime data", () => {
|
||||||
|
expect(parseKnownPiWebCapabilities([PI_WEB_CAPABILITIES.piPackagesManage, "future.capability"])).toEqual([PI_WEB_CAPABILITIES.piPackagesManage]);
|
||||||
|
expect(parseKnownPiWebCapabilities([PI_WEB_CAPABILITIES.piPackagesManage, 1])).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,8 +6,23 @@ export type { PiWebCapability };
|
|||||||
export const KNOWN_PI_WEB_CAPABILITIES = Object.values(PI_WEB_CAPABILITIES);
|
export const KNOWN_PI_WEB_CAPABILITIES = Object.values(PI_WEB_CAPABILITIES);
|
||||||
const knownPiWebCapabilities: ReadonlySet<string> = new Set(KNOWN_PI_WEB_CAPABILITIES);
|
const knownPiWebCapabilities: ReadonlySet<string> = new Set(KNOWN_PI_WEB_CAPABILITIES);
|
||||||
|
|
||||||
export const WEB_RUNTIME_CAPABILITIES = [PI_WEB_CAPABILITIES.sessionsDeleteArchived, PI_WEB_CAPABILITIES.sessionsBulkMutations, PI_WEB_CAPABILITIES.sessionsCleanup, PI_WEB_CAPABILITIES.sessionsReload, PI_WEB_CAPABILITIES.promptAttachments, PI_WEB_CAPABILITIES.workspaceFileSuggestions] as const satisfies readonly PiWebCapability[];
|
export const WEB_RUNTIME_CAPABILITIES = [
|
||||||
export const SESSIOND_RUNTIME_CAPABILITIES = [PI_WEB_CAPABILITIES.sessionsDeleteArchived, PI_WEB_CAPABILITIES.sessionsBulkMutations, PI_WEB_CAPABILITIES.sessionsCleanup, PI_WEB_CAPABILITIES.sessionsReload, PI_WEB_CAPABILITIES.promptAttachments] as const satisfies readonly PiWebCapability[];
|
PI_WEB_CAPABILITIES.sessionsDeleteArchived,
|
||||||
|
PI_WEB_CAPABILITIES.sessionsBulkMutations,
|
||||||
|
PI_WEB_CAPABILITIES.sessionsCleanup,
|
||||||
|
PI_WEB_CAPABILITIES.sessionsReload,
|
||||||
|
PI_WEB_CAPABILITIES.promptAttachments,
|
||||||
|
PI_WEB_CAPABILITIES.workspaceFileSuggestions,
|
||||||
|
PI_WEB_CAPABILITIES.piPackagesManage,
|
||||||
|
] as const satisfies readonly PiWebCapability[];
|
||||||
|
|
||||||
|
export const SESSIOND_RUNTIME_CAPABILITIES = [
|
||||||
|
PI_WEB_CAPABILITIES.sessionsDeleteArchived,
|
||||||
|
PI_WEB_CAPABILITIES.sessionsBulkMutations,
|
||||||
|
PI_WEB_CAPABILITIES.sessionsCleanup,
|
||||||
|
PI_WEB_CAPABILITIES.sessionsReload,
|
||||||
|
PI_WEB_CAPABILITIES.promptAttachments,
|
||||||
|
] as const satisfies readonly PiWebCapability[];
|
||||||
|
|
||||||
const EFFECTIVE_CAPABILITY_REQUIREMENTS = {
|
const EFFECTIVE_CAPABILITY_REQUIREMENTS = {
|
||||||
[PI_WEB_CAPABILITIES.sessionsDeleteArchived]: ["web", "sessiond"],
|
[PI_WEB_CAPABILITIES.sessionsDeleteArchived]: ["web", "sessiond"],
|
||||||
@@ -16,6 +31,7 @@ const EFFECTIVE_CAPABILITY_REQUIREMENTS = {
|
|||||||
[PI_WEB_CAPABILITIES.sessionsReload]: ["web", "sessiond"],
|
[PI_WEB_CAPABILITIES.sessionsReload]: ["web", "sessiond"],
|
||||||
[PI_WEB_CAPABILITIES.promptAttachments]: ["web", "sessiond"],
|
[PI_WEB_CAPABILITIES.promptAttachments]: ["web", "sessiond"],
|
||||||
[PI_WEB_CAPABILITIES.workspaceFileSuggestions]: ["web"],
|
[PI_WEB_CAPABILITIES.workspaceFileSuggestions]: ["web"],
|
||||||
|
[PI_WEB_CAPABILITIES.piPackagesManage]: ["web"],
|
||||||
} as const satisfies Record<PiWebCapability, readonly PiWebServiceComponent[]>;
|
} as const satisfies Record<PiWebCapability, readonly PiWebServiceComponent[]>;
|
||||||
|
|
||||||
export function isPiWebCapability(value: unknown): value is PiWebCapability {
|
export function isPiWebCapability(value: unknown): value is PiWebCapability {
|
||||||
@@ -26,6 +42,11 @@ export function supportsPiWebCapability(source: { capabilities?: readonly PiWebC
|
|||||||
return source?.capabilities?.includes(capability) === true;
|
return source?.capabilities?.includes(capability) === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseKnownPiWebCapabilities(value: unknown): PiWebCapability[] | undefined {
|
||||||
|
if (!Array.isArray(value) || !value.every((capability) => typeof capability === "string")) return undefined;
|
||||||
|
return value.filter(isPiWebCapability);
|
||||||
|
}
|
||||||
|
|
||||||
export function effectivePiWebCapabilities(components: Partial<Record<PiWebServiceComponent, Pick<PiWebRuntimeComponent, "available" | "capabilities">>>): PiWebCapability[] {
|
export function effectivePiWebCapabilities(components: Partial<Record<PiWebServiceComponent, Pick<PiWebRuntimeComponent, "available" | "capabilities">>>): PiWebCapability[] {
|
||||||
return KNOWN_PI_WEB_CAPABILITIES.filter((capability) => {
|
return KNOWN_PI_WEB_CAPABILITIES.filter((capability) => {
|
||||||
const requiredComponents = EFFECTIVE_CAPABILITY_REQUIREMENTS[capability];
|
const requiredComponents = EFFECTIVE_CAPABILITY_REQUIREMENTS[capability];
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { PI_WEB_CAPABILITIES } from "./capabilities";
|
||||||
|
import { parsePiWebRuntimeResponse } from "./piWebStatusParsing";
|
||||||
|
|
||||||
|
describe("PI WEB status parsing", () => {
|
||||||
|
it("parses package-management runtime capabilities and ignores unknown string capabilities", () => {
|
||||||
|
expect(parsePiWebRuntimeResponse({
|
||||||
|
packageName: "@jmfederico/pi-web",
|
||||||
|
generatedAt: "now",
|
||||||
|
components: {
|
||||||
|
web: { component: "web", label: "Web/UI", runtimeVersion: "1.0.0", available: true, capabilities: [PI_WEB_CAPABILITIES.piPackagesManage, "future.capability"] },
|
||||||
|
sessiond: { component: "sessiond", label: "Session daemon", runtimeVersion: "1.0.0", available: true, capabilities: ["future.sessiondCapability"] },
|
||||||
|
},
|
||||||
|
capabilities: [PI_WEB_CAPABILITIES.piPackagesManage, "future.capability"],
|
||||||
|
})).toMatchObject({
|
||||||
|
components: {
|
||||||
|
web: { capabilities: [PI_WEB_CAPABILITIES.piPackagesManage] },
|
||||||
|
sessiond: { capabilities: [] },
|
||||||
|
},
|
||||||
|
capabilities: [PI_WEB_CAPABILITIES.piPackagesManage],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects malformed capability arrays", () => {
|
||||||
|
expect(parsePiWebRuntimeResponse({
|
||||||
|
packageName: "@jmfederico/pi-web",
|
||||||
|
generatedAt: "now",
|
||||||
|
components: {
|
||||||
|
web: { component: "web", label: "Web/UI", available: true, capabilities: [PI_WEB_CAPABILITIES.piPackagesManage, 1] },
|
||||||
|
sessiond: { component: "sessiond", label: "Session daemon", available: true, capabilities: [] },
|
||||||
|
},
|
||||||
|
capabilities: [PI_WEB_CAPABILITIES.piPackagesManage],
|
||||||
|
})).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { PiWebCapability, PiWebComponentStatus, PiWebInstallationInfo, PiWebRuntimeComponent, PiWebVersionResponse } from "./apiTypes.js";
|
import type { PiWebComponentStatus, PiWebInstallationInfo, PiWebRuntimeComponent, PiWebRuntimeResponse, PiWebVersionResponse } from "./apiTypes.js";
|
||||||
import { isPiWebCapability } from "./capabilities.js";
|
import { parseKnownPiWebCapabilities } from "./capabilities.js";
|
||||||
|
|
||||||
export function parsePiWebVersionResponse(value: unknown): PiWebVersionResponse | undefined {
|
export function parsePiWebVersionResponse(value: unknown): PiWebVersionResponse | undefined {
|
||||||
if (!isRecord(value)) return undefined;
|
if (!isRecord(value)) return undefined;
|
||||||
@@ -13,13 +13,26 @@ export function parsePiWebVersionResponse(value: unknown): PiWebVersionResponse
|
|||||||
return { packageName, generatedAt, components: { web, sessiond } };
|
return { packageName, generatedAt, components: { web, sessiond } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parsePiWebRuntimeResponse(value: unknown): PiWebRuntimeResponse | undefined {
|
||||||
|
if (!isRecord(value)) return undefined;
|
||||||
|
const packageName = value["packageName"];
|
||||||
|
const generatedAt = value["generatedAt"];
|
||||||
|
const components = value["components"];
|
||||||
|
const capabilities = parseKnownPiWebCapabilities(value["capabilities"]);
|
||||||
|
if (typeof packageName !== "string" || packageName === "" || typeof generatedAt !== "string" || generatedAt === "" || !isRecord(components) || capabilities === undefined) return undefined;
|
||||||
|
const web = parsePiWebRuntimeComponent(components["web"]);
|
||||||
|
const sessiond = parsePiWebRuntimeComponent(components["sessiond"]);
|
||||||
|
if (web === undefined || sessiond === undefined) return undefined;
|
||||||
|
return { packageName, generatedAt, components: { web, sessiond }, capabilities };
|
||||||
|
}
|
||||||
|
|
||||||
export function parsePiWebRuntimeComponent(value: unknown): PiWebRuntimeComponent | undefined {
|
export function parsePiWebRuntimeComponent(value: unknown): PiWebRuntimeComponent | undefined {
|
||||||
if (!isRecord(value)) return undefined;
|
if (!isRecord(value)) return undefined;
|
||||||
const component = value["component"];
|
const component = value["component"];
|
||||||
const label = value["label"];
|
const label = value["label"];
|
||||||
const runtimeVersion = value["runtimeVersion"];
|
const runtimeVersion = value["runtimeVersion"];
|
||||||
const available = value["available"];
|
const available = value["available"];
|
||||||
const capabilities = parsePiWebCapabilities(value["capabilities"]);
|
const capabilities = parseKnownPiWebCapabilities(value["capabilities"]);
|
||||||
const error = value["error"];
|
const error = value["error"];
|
||||||
if (component !== "web" && component !== "sessiond") return undefined;
|
if (component !== "web" && component !== "sessiond") return undefined;
|
||||||
if (typeof label !== "string" || label === "" || typeof available !== "boolean" || capabilities === undefined) return undefined;
|
if (typeof label !== "string" || label === "" || typeof available !== "boolean" || capabilities === undefined) return undefined;
|
||||||
@@ -57,11 +70,6 @@ export function parsePiWebComponentStatus(value: unknown): PiWebComponentStatus
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function parsePiWebCapabilities(value: unknown): PiWebCapability[] | undefined {
|
|
||||||
if (!Array.isArray(value) || !value.every(isPiWebCapability)) return undefined;
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parsePiWebInstallationInfo(value: unknown): PiWebInstallationInfo | undefined {
|
export function parsePiWebInstallationInfo(value: unknown): PiWebInstallationInfo | undefined {
|
||||||
if (!isRecord(value)) return undefined;
|
if (!isRecord(value)) return undefined;
|
||||||
const kind = value["kind"];
|
const kind = value["kind"];
|
||||||
|
|||||||
Reference in New Issue
Block a user