Archived
feat: add pi-web version reporting
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@jmfederico/pi-web": patch
|
||||
---
|
||||
|
||||
Add `pi-web version` and include installed and running PI WEB version details in doctor output.
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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());
|
||||
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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,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,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");
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user