feat: add pi-web version reporting

This commit is contained in:
Federico Jaramillo Martinez
2026-05-28 21:33:14 +02:00
parent 4043ce7eca
commit 50906617a0
18 changed files with 389 additions and 70 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@jmfederico/pi-web": patch
---
Add `pi-web version` and include installed and running PI WEB version details in doctor output.
+4
View File
@@ -150,9 +150,12 @@ pi-web status
pi-web logs
pi-web restart
pi-web doctor
pi-web version
pi-web uninstall
```
Use `pi-web version` to compare the installed package version with the versions reported by the running Web/UI and session daemon services.
One-line install is also available for users who prefer it:
```bash
@@ -173,6 +176,7 @@ Then in Pi:
/pi-web logs
/pi-web restart
/pi-web doctor
/pi-web version
```
The Pi command is a convenience wrapper around the same service installer. When installed this way, the service installer can use PI WEB's package-local server entrypoints, so `pi-web-server` and `pi-web-sessiond` do not need to be on your shell `PATH`. `/pi-web logs` shows the last 100 service log lines; use `pi-web logs` in a shell when you want to follow logs continuously.
+2 -1
View File
@@ -121,7 +121,8 @@
<h2>What does <code>pi-web doctor</code> check?</h2>
<p>
It checks whether the service shell and native service environment can find Node 22+, npm, Pi, and the Pi
Web binaries. It also reports user service lingering when relevant for server-style installs.
Web binaries. It also prints installed and running PI WEB versions when available, and reports user service
lingering when relevant for server-style installs.
</p>
<p>
If something works in your terminal but fails in doctor, treat that as a login-shell PATH mismatch and
+1
View File
@@ -257,6 +257,7 @@
<pre id="home-install"><code><span class="prompt">$</span> npm install -g @jmfederico/pi-web
<span class="prompt">$</span> pi-web install
<span class="prompt">$</span> pi-web doctor
<span class="prompt">$</span> pi-web version
<span class="comment"># Open http://127.0.0.1:8504</span></code></pre>
</div>
</div>
+4 -1
View File
@@ -138,7 +138,8 @@
/pi-web install
/pi-web status
/pi-web logs
/pi-web doctor</code></pre>
/pi-web doctor
/pi-web version</code></pre>
</div>
</section>
@@ -195,6 +196,7 @@
<section id="manage-services">
<h2>Manage services</h2>
<p><code>pi-web version</code> compares the installed package version with the versions reported by the running Web/UI and session daemon services.</p>
<div class="code-card">
<div class="copy-row">
<strong>Useful commands</strong>
@@ -204,6 +206,7 @@
<span class="prompt">$</span> pi-web logs
<span class="prompt">$</span> pi-web restart
<span class="prompt">$</span> pi-web doctor
<span class="prompt">$</span> pi-web version
<span class="comment"># From a checkout, install the split development services:</span>
<span class="prompt">$</span> pi-web install --dev</code></pre>
+2 -1
View File
@@ -18,6 +18,7 @@ const subcommands = [
"start",
"stop",
"doctor",
"version",
"uninstall",
"open",
"help",
@@ -88,7 +89,7 @@ async function boundedLogs(): Promise<{ code: number; output: string }> {
export default function piWebExtension(pi: ExtensionAPI): void {
pi.registerCommand("pi-web", {
description: "Manage PI WEB services: install, status, logs, restart, start, stop, doctor, open",
description: "Manage PI WEB services: install, status, logs, restart, start, stop, doctor, version, open",
getArgumentCompletions(prefix: string): { value: string; label: string }[] | null {
const [first = ""] = parseArgs(prefix);
const items = subcommands
+18 -7
View File
@@ -6,6 +6,7 @@ import { homedir, userInfo } from "node:os";
import { basename, dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { defaultPiWebConfigPath, defaultPiWebDataDir, examplePiWebConfig } from "./config.js";
import { packageVersion, printPiWebVersionReport } from "./piWebVersionReport.js";
import { checkNodePtyDarwinSpawnHelper, formatNodePtyDarwinSpawnHelperCheck } from "./server/diagnostics/nodePtySpawnHelper.js";
const PI_WEB_PACKAGE_NAME = "@jmfederico/pi-web";
@@ -885,6 +886,10 @@ function commandCheck(command: string): string {
return `command -v ${command}`;
}
function commandWithVersionCheck(command: string): string {
return `${commandCheck(command)} && (${command} --version 2>&1 || true)`;
}
function nodeVersionCheck(): string {
return [
commandCheck("node"),
@@ -898,21 +903,21 @@ function doctorChecks(): Check[] {
if (backend === undefined) {
return [
[`${shell} can find node >= 22`, serviceShellCommand(nodeVersionCheck())],
[`${shell} can find npm`, serviceShellCommand(commandCheck("npm"))],
[`${shell} can find pi`, serviceShellCommand(commandCheck("pi"))],
[`${shell} can find npm`, serviceShellCommand(commandWithVersionCheck("npm"))],
[`${shell} can find pi`, serviceShellCommand(commandWithVersionCheck("pi"))],
];
}
const checks: Check[] = [
...backendAvailabilityChecks(backend),
...baseShellChecks(backend),
[`${shell} can find npm`, serviceShellCommand(commandCheck("npm"))],
[`${shell} can find pi`, serviceShellCommand(commandCheck("pi"))],
[`${shell} can find npm`, serviceShellCommand(commandWithVersionCheck("npm"))],
[`${shell} can find pi`, serviceShellCommand(commandWithVersionCheck("pi"))],
];
const executables = resolveServiceExecutables(backend);
checks.push(...executables.web.checks, ...executables.sessiond.checks);
if (backend.kind === "systemd") {
checks.push([`systemd user ${shell} can find pi`, systemdUserServiceShellCommand(commandCheck("pi"))]);
checks.push([`systemd user ${shell} can find pi`, systemdUserServiceShellCommand(commandWithVersionCheck("pi"))]);
}
return checks;
}
@@ -952,7 +957,7 @@ function printPathSetupAdvice(): void {
}
}
function doctor(): void {
async function doctor(): Promise<void> {
const backend = currentServiceBackend();
console.log(`Platform: ${platformLabel()}`);
console.log(`Service backend: ${backend?.label ?? "manual run only"}`);
@@ -960,6 +965,9 @@ function doctor(): void {
if (backend === undefined) {
console.log(`- Native user service checks skipped on ${platformLabel()}`);
}
console.log("");
await printPiWebVersionReport();
console.log("\nDoctor checks:");
const ok = runChecks(doctorChecks());
const nodePtySpawnHelperOk = printNodePtyDarwinSpawnHelperCheck();
@@ -1011,6 +1019,7 @@ Usage:
pi-web uninstall
pi-web start|stop|restart|status|logs
pi-web doctor
pi-web version
Recommended install:
npm install -g @jmfederico/pi-web
@@ -1027,7 +1036,9 @@ async function main(): Promise<void> {
else if (command === "uninstall") await uninstall();
else if (command === "start" || command === "stop" || command === "restart" || command === "status") serviceAction(command);
else if (command === "logs") logs();
else if (command === "doctor") doctor();
else if (command === "doctor") await doctor();
else if (command === "version") await printPiWebVersionReport();
else if (command === "--version" || command === "-v") console.log(packageVersion());
else if (command === "help" || command === "--help" || command === "-h") help();
else throw new Error(`Unknown command: ${command}`);
}
+239
View File
@@ -0,0 +1,239 @@
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { effectivePiWebConfig } from "./config.js";
import { SessionDaemonClient } from "./sessiond/sessionDaemonClient.js";
import type { PiWebComponentStatus, PiWebInstallationInfo, PiWebVersionResponse } from "./shared/apiTypes.js";
import { parsePiWebComponentStatus, parsePiWebVersionResponse } from "./shared/piWebStatusParsing.js";
const PI_WEB_PACKAGE_NAME = "@jmfederico/pi-web";
const PI_WEB_VERSION_TIMEOUT_MS = 2000;
const PI_WEB_VERSION_ENDPOINT_PATH = "/api/pi-web/version";
const PI_WEB_STATUS_ENDPOINT_PATH = "/api/pi-web/status";
const DEFAULT_PACKAGE_VERSION = "0.0.0-dev";
interface PackageInfo {
name: string;
version: string;
path: string;
}
interface RunningVersionInfo {
generatedAt?: string;
web?: PiWebComponentStatus;
sessiond?: PiWebComponentStatus;
webError?: string;
sessiondError?: string;
}
export function packageVersion(): string {
return readPackageInfo()?.version ?? DEFAULT_PACKAGE_VERSION;
}
export async function printPiWebVersionReport(): Promise<void> {
console.log("PI WEB version");
printInstalledPackageVersions();
printRunningVersionInfo(await collectRunningVersionInfo());
}
function packageRootPath(): string {
return dirname(dirname(fileURLToPath(import.meta.url)));
}
function packageJsonPath(): string {
return join(packageRootPath(), "package.json");
}
function readPackageInfo(): PackageInfo | undefined {
const path = packageJsonPath();
try {
return parsePackageInfo(JSON.parse(readFileSync(path, "utf8")), path);
} catch {
return undefined;
}
}
function parsePackageInfo(value: unknown, path: string): PackageInfo | undefined {
if (!isRecord(value)) return undefined;
const name = value["name"];
const version = value["version"];
if (typeof name !== "string" || name === "" || typeof version !== "string" || version === "") return undefined;
return { name, version, path };
}
function webVersionEndpoint(): { endpoint?: string; error?: string } {
try {
const { config } = effectivePiWebConfig();
const host = httpClientHost(config.host);
const port = config.port ?? 8504;
return { endpoint: `http://${urlHost(host)}:${String(port)}${PI_WEB_VERSION_ENDPOINT_PATH}` };
} catch (error) {
return { error: `could not read PI WEB config: ${errorMessage(error)}` };
}
}
function httpClientHost(configuredHost: string | undefined): string {
const host = configuredHost === undefined || configuredHost === "" ? "127.0.0.1" : configuredHost;
if (host === "0.0.0.0" || host === "::" || host === "[::]") return "127.0.0.1";
return host;
}
function urlHost(host: string): string {
if (host.startsWith("[") || !host.includes(":")) return host;
return `[${host}]`;
}
function statusEndpointFor(versionEndpoint: string): string {
if (!versionEndpoint.endsWith(PI_WEB_VERSION_ENDPOINT_PATH)) return versionEndpoint;
return `${versionEndpoint.slice(0, -PI_WEB_VERSION_ENDPOINT_PATH.length)}${PI_WEB_STATUS_ENDPOINT_PATH}`;
}
async function fetchPiWebVersionResponse(endpoint: string): Promise<PiWebVersionResponse> {
const response = await fetch(endpoint, {
headers: { accept: "application/json" },
signal: AbortSignal.timeout(PI_WEB_VERSION_TIMEOUT_MS),
});
if (!response.ok) throw new Error(`HTTP ${String(response.status)}`);
const parsed: unknown = await response.json();
const status = parsePiWebVersionResponse(parsed);
if (status === undefined) throw new Error("response did not include PI WEB version information");
return status;
}
async function collectRunningVersionInfo(): Promise<RunningVersionInfo> {
const endpoint = webVersionEndpoint();
if (endpoint.endpoint !== undefined) {
try {
const status = await fetchPiWebVersionResponse(endpoint.endpoint);
return { generatedAt: status.generatedAt, web: status.components.web, sessiond: status.components.sessiond };
} catch (error) {
let webError = `${endpoint.endpoint}: ${errorMessage(error)}`;
const statusEndpoint = statusEndpointFor(endpoint.endpoint);
if (statusEndpoint !== endpoint.endpoint && isHttpNotFound(error)) {
try {
const status = await fetchPiWebVersionResponse(statusEndpoint);
return { generatedAt: status.generatedAt, web: status.components.web, sessiond: status.components.sessiond };
} catch (statusError) {
webError = `${webError}; ${statusEndpoint}: ${errorMessage(statusError)}`;
}
}
return runningVersionInfoWithSessiondFallback({ webError });
}
}
return runningVersionInfoWithSessiondFallback({ webError: endpoint.error ?? "web/API status endpoint unavailable" });
}
async function runningVersionInfoWithSessiondFallback(base: { webError: string }): Promise<RunningVersionInfo> {
const sessiond = await collectRunningSessiondInfo();
return {
webError: base.webError,
...(sessiond.component === undefined ? {} : { sessiond: sessiond.component }),
...(sessiond.error === undefined ? {} : { sessiondError: sessiond.error }),
};
}
async function collectRunningSessiondInfo(): Promise<{ component?: PiWebComponentStatus; error?: string }> {
try {
const response = await withTimeout(
new SessionDaemonClient().request("GET", "/health"),
PI_WEB_VERSION_TIMEOUT_MS,
"session daemon health check timed out",
);
if (response.statusCode < 200 || response.statusCode >= 300) throw new Error(`HTTP ${String(response.statusCode)}`);
const parsed: unknown = response.body === "" ? undefined : JSON.parse(response.body);
const version = isRecord(parsed) ? parsed["version"] : undefined;
const component = parsePiWebComponentStatus(version);
if (component === undefined) throw new Error("health response did not include version information");
return { component };
} catch (error) {
return { error: errorMessage(error) };
}
}
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, timeoutMessage: string): Promise<T> {
let timeout: ReturnType<typeof setTimeout> | undefined;
try {
return await Promise.race([
promise,
new Promise<T>((_, reject) => {
timeout = setTimeout(() => {
reject(new Error(timeoutMessage));
}, timeoutMs);
}),
]);
} finally {
if (timeout !== undefined) clearTimeout(timeout);
}
}
function isHttpNotFound(error: unknown): boolean {
return error instanceof Error && error.message === "HTTP 404";
}
function printInstalledPackageVersions(): void {
const info = readPackageInfo();
console.log("Installed packages:");
if (info === undefined) {
console.log(`? ${PI_WEB_PACKAGE_NAME}: unknown`);
console.log(` missing package metadata: ${packageJsonPath()}`);
return;
}
console.log(`${info.name}: ${info.version}`);
console.log(` ${info.path}`);
}
function printRunningVersionInfo(info: RunningVersionInfo): void {
console.log("Running services:");
if (info.web === undefined) printUnavailableComponent("Web/UI", info.webError);
else printComponentVersion(info.web);
if (info.sessiond === undefined) printUnavailableComponent("Session daemon", info.sessiondError);
else printComponentVersion(info.sessiond);
if (info.generatedAt !== undefined) console.log(` reported by web/API at ${info.generatedAt}`);
}
function printComponentVersion(component: PiWebComponentStatus): void {
const icon = component.available ? component.stale ? "!" : "✓" : "?";
const status = !component.available ? "unavailable" : component.stale ? "restart needed" : "current";
console.log(`${icon} ${component.label}: ${status}`);
if (component.available || component.runtimeVersion !== undefined || component.installedVersion !== undefined) {
console.log(` running: ${formatVersion(component.runtimeVersion)}; installed: ${formatVersion(component.installedVersion)}`);
}
const installation = installationLabel(component.installation);
if (installation !== undefined) console.log(` installation: ${installation}`);
if (component.error !== undefined) console.log(` ${component.error}`);
}
function printUnavailableComponent(label: string, error: string | undefined): void {
console.log(`? ${label}: unavailable`);
if (error !== undefined && error !== "") console.log(` ${error}`);
}
function installationLabel(installation: PiWebInstallationInfo | undefined): string | undefined {
if (installation === undefined) return undefined;
if (installation.kind === "pi-package") {
const source = installation.source ?? "Pi package";
const scope = installation.scope === undefined ? "" : ` · ${installation.scope}`;
const path = installation.path === undefined ? "" : ` · ${installation.path}`;
return `${source}${scope}${path}`;
}
if (installation.kind === "npm-global") {
const npmRoot = installation.npmRoot === undefined ? "" : ` · ${installation.npmRoot}`;
const path = installation.path === undefined ? "" : ` · ${installation.path}`;
return `global npm package${npmRoot}${path}`;
}
if (installation.kind === "local") return installation.path === undefined ? "local checkout" : `local checkout · ${installation.path}`;
return installation.path === undefined ? "installation unknown" : `installation unknown · ${installation.path}`;
}
function formatVersion(version: string | undefined): string {
return version === undefined || version === "" ? "unknown" : version;
}
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
+2 -1
View File
@@ -14,7 +14,7 @@ import { registerWorkspaceExplorerRoutes } from "./workspaceExplorerRoutes.js";
import { registerGitRoutes } from "./gitRoutes.js";
import { registerTerminalProxyRoutes } from "./terminalProxyRoutes.js";
import { PiWebPluginService } from "./piWebPluginService.js";
import { getPiWebStatus } from "./piWebStatus.js";
import { getPiWebStatus, getPiWebVersionStatus } from "./piWebStatus.js";
export interface AppDependencies {
projects?: ProjectService;
@@ -41,6 +41,7 @@ export async function buildApp(deps: AppDependencies = {}): Promise<FastifyInsta
});
app.get("/api/pi-web/status", async () => getPiWebStatus());
app.get("/api/pi-web/version", async () => getPiWebVersionStatus());
app.get("/api/projects", async () => projects.list());
+27 -2
View File
@@ -1,6 +1,6 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { comparePackageVersions, getPiWebStatus } from "./piWebStatus.js";
import { SessionDaemonClient } from "./sessiond/sessionDaemonClient.js";
import { comparePackageVersions, getPiWebStatus, getPiWebVersionStatus } from "./piWebStatus.js";
import { SessionDaemonClient } from "../sessiond/sessionDaemonClient.js";
const originalSkipVersionCheck = process.env["PI_WEB_SKIP_VERSION_CHECK"];
@@ -17,6 +17,31 @@ describe("PI WEB status", () => {
expect(comparePackageVersions("1.202605.7", "1.202605.8")).toBeLessThan(0);
});
it("returns installed and running version components without release metadata", async () => {
const daemon = new SessionDaemonClient();
vi.spyOn(daemon, "request").mockResolvedValue({
statusCode: 200,
headers: { "content-type": "application/json" },
body: JSON.stringify({
version: {
component: "sessiond",
label: "Session daemon",
runtimeVersion: "1.202605.7",
installedVersion: "1.202605.8",
stale: true,
available: true,
},
}),
});
const status = await getPiWebVersionStatus(daemon);
expect(status.packageName).toBe("@jmfederico/pi-web");
expect(status.components.web.component).toBe("web");
expect(status.components.sessiond.runtimeVersion).toBe("1.202605.7");
expect(status).not.toHaveProperty("release");
});
it("reports stale session daemon versions as messages", async () => {
process.env["PI_WEB_SKIP_VERSION_CHECK"] = "1";
const daemon = new SessionDaemonClient();
+19 -52
View File
@@ -4,8 +4,9 @@ import { readFile, realpath, stat } from "node:fs/promises";
import { dirname, join, relative, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";
import { DefaultPackageManager, getAgentDir, SettingsManager } from "@earendil-works/pi-coding-agent";
import type { PiWebComponentStatus, PiWebInstallationInfo, PiWebReleaseStatus, PiWebServiceComponent, PiWebStatusMessage, PiWebStatusResponse } from "../shared/apiTypes.js";
import { SessionDaemonClient } from "./sessiond/sessionDaemonClient.js";
import type { PiWebComponentStatus, PiWebInstallationInfo, PiWebReleaseStatus, PiWebServiceComponent, PiWebStatusMessage, PiWebStatusResponse, PiWebVersionResponse } from "../shared/apiTypes.js";
import { parsePiWebComponentStatus } from "../shared/piWebStatusParsing.js";
import { SessionDaemonClient } from "../sessiond/sessionDaemonClient.js";
const PI_WEB_PACKAGE_NAME = "@jmfederico/pi-web";
const PI_WEB_NPM_SOURCE = `npm:${PI_WEB_PACKAGE_NAME}`;
@@ -47,20 +48,27 @@ export async function getPiWebComponentStatus(component: PiWebServiceComponent):
};
}
export async function getPiWebStatus(daemon = new SessionDaemonClient()): Promise<PiWebStatusResponse> {
const web = await getPiWebComponentStatus("web");
const [installed, sessiond] = await Promise.all([
readInstalledPackageInfo(),
export async function getPiWebVersionStatus(daemon = new SessionDaemonClient()): Promise<PiWebVersionResponse> {
const [web, sessiond] = await Promise.all([
getPiWebComponentStatus("web"),
getSessiondComponentStatus(daemon),
]);
const release = await getLatestReleaseStatus(installed?.version ?? web.runtimeVersion ?? DEFAULT_VERSION);
return {
packageName: PI_WEB_PACKAGE_NAME,
generatedAt: new Date().toISOString(),
components: { web, sessiond },
};
}
export async function getPiWebStatus(daemon = new SessionDaemonClient()): Promise<PiWebStatusResponse> {
const versionStatus = await getPiWebVersionStatus(daemon);
const { web, sessiond } = versionStatus.components;
const release = await getLatestReleaseStatus(web.installedVersion ?? web.runtimeVersion ?? DEFAULT_VERSION);
const components = { web, sessiond };
const commands = commandsFor(web.installation ?? sessiond.installation);
const messages = buildMessages(components, release, commands);
return {
packageName: PI_WEB_PACKAGE_NAME,
generatedAt: new Date().toISOString(),
components,
...versionStatus,
release,
commands,
messages,
@@ -179,54 +187,13 @@ async function getSessiondComponentStatus(daemon: SessionDaemonClient): Promise<
}
const parsed: unknown = upstream.body === "" ? undefined : JSON.parse(upstream.body);
const version = isRecord(parsed) ? parsed["version"] : undefined;
const component = parseComponentStatus(version);
const component = parsePiWebComponentStatus(version);
return component ?? unavailableSessiond("health response did not include version information");
} catch (error) {
return unavailableSessiond(error instanceof Error ? error.message : String(error));
}
}
function parseComponentStatus(value: unknown): PiWebComponentStatus | undefined {
if (!isRecord(value)) return undefined;
const component = value["component"];
const label = value["label"];
const runtimeVersion = value["runtimeVersion"];
const installedVersion = value["installedVersion"];
const stale = value["stale"];
const available = value["available"];
const error = value["error"];
const installation = parseInstallationInfo(value["installation"]);
if (component !== "web" && component !== "sessiond") return undefined;
if (typeof label !== "string" || typeof stale !== "boolean" || typeof available !== "boolean") return undefined;
return {
component,
label,
...(typeof runtimeVersion === "string" ? { runtimeVersion } : {}),
...(typeof installedVersion === "string" ? { installedVersion } : {}),
stale,
available,
...(installation === undefined ? {} : { installation }),
...(typeof error === "string" ? { error } : {}),
};
}
function parseInstallationInfo(value: unknown): PiWebInstallationInfo | undefined {
if (!isRecord(value)) return undefined;
const kind = value["kind"];
const path = value["path"];
const source = value["source"];
const scope = value["scope"];
const npmRoot = value["npmRoot"];
if (kind !== "pi-package" && kind !== "npm-global" && kind !== "local" && kind !== "unknown") return undefined;
return {
kind,
...(typeof path === "string" ? { path } : {}),
...(typeof source === "string" ? { source } : {}),
...(scope === "user" || scope === "project" ? { scope } : {}),
...(typeof npmRoot === "string" ? { npmRoot } : {}),
};
}
function unavailableSessiond(error: string): PiWebComponentStatus {
return {
component: "sessiond",
+1 -1
View File
@@ -10,7 +10,7 @@ import { AuthService } from "./sessions/authService.js";
import { registerAuthRoutes } from "./sessions/authRoutes.js";
import { PiSessionService } from "./sessions/piSessionService.js";
import { registerSessionRoutes } from "./sessions/sessionRoutes.js";
import { sessiondSocketPath } from "./sessiond/config.js";
import { sessiondSocketPath } from "../sessiond/config.js";
import { TerminalService } from "./terminals/terminalService.js";
import { registerTerminalRoutes } from "./terminals/terminalRoutes.js";
import { getPiWebComponentStatus } from "./piWebStatus.js";
+1 -1
View File
@@ -1,6 +1,6 @@
import type { FastifyInstance, FastifyReply } from "fastify";
import { WebSocket, type RawData } from "ws";
import { SessionDaemonClient } from "./sessionDaemonClient.js";
import { SessionDaemonClient } from "../../sessiond/sessionDaemonClient.js";
export function registerSessionProxyRoutes(app: FastifyInstance, daemon = new SessionDaemonClient()): void {
const proxy = async (request: { method: string; url: string; body?: unknown }, reply: FastifyReply) => {
+1 -1
View File
@@ -1,6 +1,6 @@
import type { FastifyInstance, FastifyReply } from "fastify";
import type { ProjectService } from "./projects/projectService.js";
import { SessionDaemonClient } from "./sessiond/sessionDaemonClient.js";
import { SessionDaemonClient } from "../sessiond/sessionDaemonClient.js";
import { resolveWorkspaceContext } from "./workspaces/workspaceContext.js";
import type { WorkspaceService } from "./workspaces/workspaceService.js";
import { terminalSizeQuery } from "./terminals/terminalSize.js";
@@ -1,5 +1,5 @@
import { join } from "node:path";
import { piWebDataDir } from "../../config.js";
import { piWebDataDir } from "../config.js";
export function sessiondSocketPath(): string {
return process.env["PI_WEB_SESSIOND_SOCKET"] ?? join(piWebDataDir(), "sessiond.sock");
+4 -1
View File
@@ -284,13 +284,16 @@ export interface PiWebStatusMessage {
command?: string;
}
export interface PiWebStatusResponse {
export interface PiWebVersionResponse {
packageName: string;
generatedAt: string;
components: {
web: PiWebComponentStatus;
sessiond: PiWebComponentStatus;
};
}
export interface PiWebStatusResponse extends PiWebVersionResponse {
release: PiWebReleaseStatus;
commands: {
update: string;
+58
View File
@@ -0,0 +1,58 @@
import type { PiWebComponentStatus, PiWebInstallationInfo, PiWebVersionResponse } from "./apiTypes.js";
export function parsePiWebVersionResponse(value: unknown): PiWebVersionResponse | undefined {
if (!isRecord(value)) return undefined;
const packageName = value["packageName"];
const generatedAt = value["generatedAt"];
const components = value["components"];
if (typeof packageName !== "string" || packageName === "" || typeof generatedAt !== "string" || generatedAt === "" || !isRecord(components)) return undefined;
const web = parsePiWebComponentStatus(components["web"]);
const sessiond = parsePiWebComponentStatus(components["sessiond"]);
if (web === undefined || sessiond === undefined) return undefined;
return { packageName, generatedAt, components: { web, sessiond } };
}
export function parsePiWebComponentStatus(value: unknown): PiWebComponentStatus | undefined {
if (!isRecord(value)) return undefined;
const component = value["component"];
const label = value["label"];
const runtimeVersion = value["runtimeVersion"];
const installedVersion = value["installedVersion"];
const stale = value["stale"];
const available = value["available"];
const error = value["error"];
const installation = parsePiWebInstallationInfo(value["installation"]);
if (component !== "web" && component !== "sessiond") return undefined;
if (typeof label !== "string" || label === "" || typeof stale !== "boolean" || typeof available !== "boolean") return undefined;
return {
component,
label,
...(typeof runtimeVersion === "string" ? { runtimeVersion } : {}),
...(typeof installedVersion === "string" ? { installedVersion } : {}),
stale,
available,
...(installation === undefined ? {} : { installation }),
...(typeof error === "string" ? { error } : {}),
};
}
export function parsePiWebInstallationInfo(value: unknown): PiWebInstallationInfo | undefined {
if (!isRecord(value)) return undefined;
const kind = value["kind"];
const path = value["path"];
const source = value["source"];
const scope = value["scope"];
const npmRoot = value["npmRoot"];
if (kind !== "pi-package" && kind !== "npm-global" && kind !== "local" && kind !== "unknown") return undefined;
return {
kind,
...(typeof path === "string" ? { path } : {}),
...(typeof source === "string" ? { source } : {}),
...(scope === "user" || scope === "project" ? { scope } : {}),
...(typeof npmRoot === "string" ? { npmRoot } : {}),
};
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}