Files
paseo/packages/cli/src/commands/daemon/status.ts
T
Mohamed Boudra 177153e5e8 chore: apply oxlint autofix (safe level)
Auto-fixed 102 warnings (6040 → 5938) across 57 files. Main categories:
- preserve-caught-error: add { cause: error } to re-thrown errors
- unicorn/no-array-reverse: .reverse() → .toReversed()
- unicorn/no-useless-fallback-in-spread: drop unnecessary ?? {} in spreads
- oxc/no-map-spread: rewrite object spread in .map() to Object.assign
- react-hooks/exhaustive-deps: trim module-level constants from dep arrays

Ran npm run format after to clean up whitespace from lint mutations.
2026-04-23 21:47:36 +07:00

329 lines
10 KiB
TypeScript

import type { Command } from "commander";
import { createRequire } from "node:module";
import {
getOrCreateServerId,
findExecutable,
applyProviderEnv,
execCommand,
} from "@getpaseo/server";
import { tryConnectToDaemon } from "../../utils/client.js";
import type { CommandOptions, ListResult, OutputSchema } from "../../output/index.js";
import { resolveLocalDaemonState, resolveTcpHostFromListen } from "./local-daemon.js";
import { resolveNodePathFromPid } from "./runtime-toolchain.js";
interface ProviderBinaryStatus {
label: string;
path: string | null;
version: string | null;
}
interface DaemonStatus {
serverId: string | null;
localDaemon: "running" | "stopped" | "stale_pid" | "unresponsive";
connectedDaemon: "reachable" | "unreachable" | "not_probed";
home: string;
listen: string;
hostname: string | null;
pid: number | null;
startedAt: string | null;
owner: string | null;
logPath: string;
runningAgents: number | null;
idleAgents: number | null;
daemonNode: string;
cliNode: string;
cliVersion: string;
daemonVersion: string | null;
desktopManaged: boolean;
providers: ProviderBinaryStatus[];
note?: string;
}
interface StatusRow {
key: string;
value: string;
}
type CliPackageJson = {
version?: unknown;
};
const require = createRequire(import.meta.url);
function normalizeError(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
return String(error);
}
function shortenMessage(message: string, max = 120): string {
const normalized = message.replace(/\s+/g, " ").trim();
if (normalized.length <= max) {
return normalized;
}
return `${normalized.slice(0, max - 3)}...`;
}
function appendNote(current: string | undefined, next: string | undefined): string | undefined {
if (!next) return current;
if (!current) return next;
return `${current}; ${next}`;
}
function resolveCliVersion(): string {
try {
const packageJson = require("../../../package.json") as CliPackageJson;
if (typeof packageJson.version === "string" && packageJson.version.trim().length > 0) {
return packageJson.version.trim();
}
} catch {
// Fall through.
}
return "unknown";
}
function createStatusSchema(status: DaemonStatus): OutputSchema<StatusRow> {
return {
idField: "key",
columns: [
{ header: "KEY", field: "key" },
{
header: "VALUE",
field: "value",
color: (_, item) => {
if (item.key === "Local Daemon") {
if (item.value === "running") return "green";
if (item.value === "unresponsive") return "yellow";
return "red";
}
if (item.key === "Connected Daemon") {
if (item.value === "reachable") return "green";
if (item.value === "not_probed") return "yellow";
return "red";
}
if (item.key.startsWith(" ")) {
if (item.value === "not found") return "red";
if (item.value.endsWith("(--version failed)")) return "yellow";
return "green";
}
return undefined;
},
},
],
serialize: () => status,
};
}
function toStatusRows(status: DaemonStatus): StatusRow[] {
const rows: StatusRow[] = [
{ key: "Server ID", value: status.serverId ?? "-" },
{ key: "Local Daemon", value: status.localDaemon },
{ key: "Connected Daemon", value: status.connectedDaemon },
{ key: "Home", value: status.home },
{ key: "Listen", value: status.listen },
{ key: "Hostname", value: status.hostname ?? "-" },
{ key: "PID", value: status.pid === null ? "-" : String(status.pid) },
{ key: "Started", value: status.startedAt ?? "-" },
{ key: "Owner", value: status.owner ?? "-" },
{ key: "Logs", value: status.logPath },
{ key: "Daemon Node", value: status.daemonNode },
{ key: "CLI Node", value: status.cliNode },
{ key: "CLI", value: status.cliVersion },
{ key: "Daemon Version", value: status.daemonVersion ?? "-" },
];
if (status.runningAgents !== null && status.idleAgents !== null) {
rows.push({
key: "Agents",
value: `${status.runningAgents} running, ${status.idleAgents} idle`,
});
} else {
rows.push({
key: "Agents",
value: "Unavailable (daemon API not reachable)",
});
}
if (status.note) {
rows.push({ key: "Note", value: status.note });
}
rows.push({ key: "", value: "" });
rows.push({ key: "Providers", value: "" });
for (const provider of status.providers) {
if (!provider.path) {
rows.push({ key: ` ${provider.label}`, value: "not found" });
} else if (!provider.version) {
rows.push({ key: ` ${provider.label}`, value: `${provider.path} (--version failed)` });
} else {
rows.push({ key: ` ${provider.label}`, value: `${provider.path} (${provider.version})` });
}
}
return rows;
}
const PROVIDER_BINARIES: { label: string; binary: string }[] = [
{ label: "Claude", binary: "claude" },
{ label: "Codex", binary: "codex" },
{ label: "OpenCode", binary: "opencode" },
];
async function checkProviderBinary(
binary: string,
): Promise<{ path: string | null; version: string | null }> {
const binaryPath = await findExecutable(binary);
if (!binaryPath) {
return { path: null, version: null };
}
const env = applyProviderEnv(process.env);
try {
const { stdout } = await execCommand(binaryPath, ["--version"], {
timeout: 5000,
env,
});
return { path: binaryPath, version: stdout.trim() || null };
} catch {
return { path: binaryPath, version: null };
}
}
async function checkProviderBinaries(): Promise<ProviderBinaryStatus[]> {
const results = await Promise.all(
PROVIDER_BINARIES.map(async ({ label, binary }) => {
const result = await checkProviderBinary(binary);
return Object.assign({ label }, result);
}),
);
return results;
}
function resolveOwnerLabel(uid: number | undefined, hostname: string | undefined): string | null {
if (uid === undefined && !hostname) {
return null;
}
const uidPart = uid === undefined ? "?" : String(uid);
const hostPart = hostname ?? "unknown-host";
return `${uidPart}@${hostPart}`;
}
export type StatusResult = ListResult<StatusRow>;
export async function runStatusCommand(
options: CommandOptions,
_command: Command,
): Promise<StatusResult> {
const home = typeof options.home === "string" ? options.home : undefined;
const state = resolveLocalDaemonState({ home });
const host = resolveTcpHostFromListen(state.listen);
const owner = resolveOwnerLabel(state.pidInfo?.uid, state.pidInfo?.hostname);
let daemonNode: string;
if (!state.running) {
daemonNode = "-";
} else if (state.pidInfo?.pid) {
const fromPid = await resolveNodePathFromPid(state.pidInfo.pid);
daemonNode = fromPid.nodePath ?? `unknown (${fromPid.error ?? "could not resolve from PID"})`;
} else {
daemonNode = "unknown (no PID available)";
}
const cliNode = process.execPath;
let localDaemon: DaemonStatus["localDaemon"] = state.running ? "running" : "stopped";
let connectedDaemon: DaemonStatus["connectedDaemon"] = "not_probed";
let runningAgents: number | null = null;
let idleAgents: number | null = null;
let daemonVersion: string | null = null;
let note: string | undefined;
if (!state.running && state.stalePidFile && state.pidInfo) {
localDaemon = "stale_pid";
note = `Stale PID file found for PID ${state.pidInfo.pid}`;
}
if (host) {
const client = await tryConnectToDaemon({ host, timeout: 1500 });
if (client) {
connectedDaemon = "reachable";
daemonVersion = client.getLastServerInfoMessage()?.version ?? null;
try {
const agentsPayload = await client.fetchAgents({ filter: { includeArchived: true } });
const agents = agentsPayload.entries.map((entry) => entry.agent);
runningAgents = agents.filter((a) => a.status === "running").length;
idleAgents = agents.filter((a) => a.status === "idle").length;
if (!state.running) {
daemonNode = "unknown (API reachable, PID unresolved)";
note = appendNote(
note,
state.pidInfo
? `Connected daemon is reachable at ${host} even though local daemon PID ${state.pidInfo.pid} is stale`
: `Connected daemon is reachable at ${host} but no local daemon PID file was found`,
);
}
} catch {
if (state.running) {
localDaemon = "unresponsive";
}
note = appendNote(
note,
state.running
? `Local daemon PID is running but API requests to ${host} failed`
: `Connected daemon websocket is reachable at ${host} but fetch_agents failed`,
);
} finally {
await client.close().catch(() => {});
}
} else if (state.running) {
connectedDaemon = "unreachable";
localDaemon = "unresponsive";
note = appendNote(
note,
`Local daemon PID is running but websocket at ${host} is not reachable`,
);
} else {
connectedDaemon = "unreachable";
}
} else {
note = appendNote(note, "Daemon is configured for unix socket listen; API probe skipped");
}
const cliVersion = resolveCliVersion();
let serverId: string | null = null;
try {
serverId = getOrCreateServerId(state.home);
} catch (error) {
note = appendNote(note, `serverId unavailable: ${shortenMessage(normalizeError(error))}`);
}
const providers = await checkProviderBinaries();
const daemonStatus: DaemonStatus = {
serverId,
localDaemon,
connectedDaemon,
home: state.home,
listen: state.listen,
hostname: state.pidInfo?.hostname ?? null,
pid: state.pidInfo?.pid ?? null,
startedAt: state.pidInfo?.startedAt ?? null,
owner,
logPath: state.logPath,
runningAgents,
idleAgents,
daemonNode,
cliNode,
cliVersion,
daemonVersion,
desktopManaged: state.pidInfo?.desktopManaged === true,
providers,
note,
};
return {
type: "list",
data: toStatusRows(daemonStatus),
schema: createStatusSchema(daemonStatus),
};
}