From 958596aaa57982e8db7bc7fad74e19e66a36d529 Mon Sep 17 00:00:00 2001 From: Federico Jaramillo Martinez Date: Mon, 25 May 2026 16:44:17 +0200 Subject: [PATCH] fix(cli): summarize service status output --- .changeset/quiet-status-command.md | 5 + src/cli.ts | 151 ++++++++++++++++++++++++++--- 2 files changed, 144 insertions(+), 12 deletions(-) create mode 100644 .changeset/quiet-status-command.md diff --git a/.changeset/quiet-status-command.md b/.changeset/quiet-status-command.md new file mode 100644 index 0000000..50ec7f6 --- /dev/null +++ b/.changeset/quiet-status-command.md @@ -0,0 +1,5 @@ +--- +"@jmfederico/pi-web": patch +--- + +Make `pi-web status` print a concise service health report without invoking paged system service output. diff --git a/src/cli.ts b/src/cli.ts index 8864b46..d10d45d 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -72,6 +72,17 @@ interface ServiceExecutables { web: ServiceExecutable; } +type ServiceHealth = "running" | "stopped" | "not-installed" | "unknown"; + +interface ServiceRuntimeStatus { + ref: ServiceRef; + health: ServiceHealth; + detail: string; + target: string; + filePath: string; + pid?: string; +} + const serviceRefs: Record = { sessiond: { id: "sessiond", @@ -507,8 +518,16 @@ function launchdPlistPath(ref: ServiceRef): string { return join(launchdServiceDir, ref.launchdPlistName); } +function serviceFilePath(backend: ServiceBackend, ref: ServiceRef): string { + return backend.kind === "systemd" ? systemdServicePath(ref) : launchdPlistPath(ref); +} + function serviceFileExists(backend: ServiceBackend, ref: ServiceRef): boolean { - return existsSync(backend.kind === "systemd" ? systemdServicePath(ref) : launchdPlistPath(ref)); + return existsSync(serviceFilePath(backend, ref)); +} + +function installedServiceIds(backend: ServiceBackend): Set { + return new Set(allServiceRefs().filter((ref) => serviceFileExists(backend, ref)).map((ref) => ref.id)); } function installedServiceRefs(backend: ServiceBackend): ServiceRef[] { @@ -606,6 +625,117 @@ async function uninstallNativeServices(backend: ServiceBackend): Promise { else await uninstallLaunchdServices(); } +function serviceDisplayName(ref: ServiceRef): string { + if (ref.id === "sessiond") return "session daemon"; + if (ref.id === "uiDev") return "UI/API dev server"; + return "web server"; +} + +function statusServiceRefs(backend: ServiceBackend): ServiceRef[] { + const ids = installedServiceIds(backend); + if (ids.size === 0) return []; + if (ids.has("web") && ids.has("uiDev")) return startOrder(allServiceRefs()); + if (ids.has("uiDev")) return serviceRefList(["sessiond", "uiDev"]); + if (ids.has("web")) return productionServiceRefs(); + return serviceRefList(["sessiond"]); +} + +function serviceInstallMode(backend: ServiceBackend): string { + const ids = installedServiceIds(backend); + if (ids.size === 0) return "not installed"; + const hasSessiond = ids.has("sessiond"); + const hasWeb = ids.has("web"); + const hasUiDev = ids.has("uiDev"); + if (hasWeb && hasUiDev) return "mixed"; + if (hasUiDev) return hasSessiond ? "development" : "development (incomplete)"; + if (hasWeb) return hasSessiond ? "production" : "production (incomplete)"; + return "partial"; +} + +function makeServiceRuntimeStatus(ref: ServiceRef, health: ServiceHealth, detail: string, target: string, filePath: string, pid?: string): ServiceRuntimeStatus { + return { + ref, + health, + detail, + target, + filePath, + ...(pid === undefined ? {} : { pid }), + }; +} + +function firstOutputLine(...values: string[]): string | undefined { + for (const value of values) { + const line = value.trim().split("\n").find((candidate) => candidate.trim() !== ""); + if (line !== undefined) return line.trim(); + } + return undefined; +} + +function systemdMainPid(ref: ServiceRef): string | undefined { + const result = capture("systemctl", ["--user", "--no-pager", "show", ref.systemdName, "--property=MainPID", "--value"]); + if (result.status !== 0) return undefined; + const value = result.stdout.trim(); + return value === "" || value === "0" ? undefined : value; +} + +function systemdRuntimeStatus(backend: ServiceBackend, ref: ServiceRef): ServiceRuntimeStatus { + const target = ref.systemdName; + const filePath = serviceFilePath(backend, ref); + if (!serviceFileExists(backend, ref)) return makeServiceRuntimeStatus(ref, "not-installed", "not installed", target, filePath); + + const result = capture("systemctl", ["--user", "--no-pager", "is-active", target]); + const state = firstOutputLine(result.stdout, result.stderr) ?? "unknown"; + if (result.status === 0 && state === "active") return makeServiceRuntimeStatus(ref, "running", "running", target, filePath, systemdMainPid(ref)); + return makeServiceRuntimeStatus(ref, state === "unknown" ? "unknown" : "stopped", state, target, filePath); +} + +function parseLaunchdField(output: string, field: string): string | undefined { + const match = new RegExp(`^\\s*${field}\\s=\\s(.+)$`, "m").exec(output); + return match?.[1]?.trim(); +} + +function launchdRuntimeStatus(backend: ServiceBackend, ref: ServiceRef): ServiceRuntimeStatus { + const target = launchdServiceTarget(ref); + const filePath = serviceFilePath(backend, ref); + if (!serviceFileExists(backend, ref)) return makeServiceRuntimeStatus(ref, "not-installed", "not installed", target, filePath); + + const result = capture("launchctl", ["print", target]); + if (result.status !== 0) { + return makeServiceRuntimeStatus(ref, "stopped", firstOutputLine(result.stderr, result.stdout) ?? "not loaded", target, filePath); + } + + const state = parseLaunchdField(result.stdout, "state") ?? "unknown"; + const pid = parseLaunchdField(result.stdout, "pid"); + const health: ServiceHealth = state === "running" ? "running" : state === "unknown" ? "unknown" : "stopped"; + return makeServiceRuntimeStatus(ref, health, state === "running" ? "running" : state, target, filePath, pid); +} + +function runtimeStatus(backend: ServiceBackend, ref: ServiceRef): ServiceRuntimeStatus { + return backend.kind === "systemd" ? systemdRuntimeStatus(backend, ref) : launchdRuntimeStatus(backend, ref); +} + +function printServiceStatus(status: ServiceRuntimeStatus): void { + const icon = status.health === "running" ? "✓" : "✗"; + const pid = status.pid === undefined ? "" : `, pid ${status.pid}`; + console.log(`${icon} ${serviceDisplayName(status.ref)}: ${status.detail} (${status.target}${pid})`); + if (status.health === "not-installed") console.log(` missing service file: ${status.filePath}`); +} + +function printServiceStatusReport(backend: ServiceBackend): boolean { + const refs = statusServiceRefs(backend); + console.log(`PI WEB services: ${serviceInstallMode(backend)} (${backend.label})`); + if (refs.length === 0) { + console.log("✗ no PI WEB service files found"); + console.log(" Run `pi-web install` or `pi-web install --dev`."); + return false; + } + + const statuses = refs.map((ref) => runtimeStatus(backend, ref)); + for (const status of statuses) printServiceStatus(status); + console.log("\nUse `pi-web logs` for service logs."); + return statuses.every((status) => status.health === "running"); +} + function backendAvailabilityChecks(backend: ServiceBackend): Check[] { if (backend.kind === "systemd") return [["systemctl --user", ["systemctl", "--user", "--version"]]]; return [[`launchctl ${launchdDomain()}`, ["launchctl", "print", launchdDomain()]]]; @@ -695,20 +825,12 @@ async function uninstall(): Promise { console.log(`PI WEB ${backend.label} removed. Production and development service files were removed; config and data were left in place.`); } -function systemdServiceAction(action: "start" | "stop" | "restart" | "status", refs: ServiceRef[]): void { +function systemdServiceAction(action: "start" | "stop" | "restart", refs: ServiceRef[]): void { const orderedRefs = action === "stop" ? stopOrder(refs) : startOrder(refs); - run("systemctl", ["--user", action, ...orderedRefs.map((ref) => ref.systemdName)], { check: action !== "status" }); + run("systemctl", ["--user", action, ...orderedRefs.map((ref) => ref.systemdName)], { check: true }); } -function launchdServiceAction(action: "start" | "stop" | "restart" | "status", refs: ServiceRef[]): void { - if (action === "status") { - for (const ref of refs) { - console.log(`\n== ${ref.launchdLabel} ==`); - run("launchctl", ["print", launchdServiceTarget(ref)]); - } - return; - } - +function launchdServiceAction(action: "start" | "stop" | "restart", refs: ServiceRef[]): void { if (action === "stop") { for (const ref of stopOrder(refs)) launchdBootout(ref); return; @@ -723,6 +845,11 @@ function launchdServiceAction(action: "start" | "stop" | "restart" | "status", r function serviceAction(action: "start" | "stop" | "restart" | "status"): void { const backend = requireServiceBackend(`pi-web ${action}`); + if (action === "status") { + if (!printServiceStatusReport(backend)) process.exitCode = 1; + return; + } + const refs = installedServiceRefs(backend); if (backend.kind === "systemd") systemdServiceAction(action, refs); else launchdServiceAction(action, refs);