mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-01 14:12:51 +00:00
* fix(daemon): report the local daemon's server id when a password is set Since #4575, `paseo daemon status` only learned the server id from an authenticated connection. The desktop app runs that command without the daemon password, so a password-protected desktop daemon reported no server id, the app treated it as remote, and hid Open in editor. The worker now announces its server id with readiness and the supervisor publishes it in the instance record next to the listen address, so status reports the running daemon's identity from local state. Closes #5300 * fix(cli): omit serverId from status until a daemon publishes one * chore: rerun CI after macOS file observation flake --------- Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
135 lines
5.0 KiB
TypeScript
135 lines
5.0 KiB
TypeScript
import { Command } from "commander";
|
|
import { DaemonConnectionError } from "@getpaseo/client/internal/daemon-client";
|
|
import {
|
|
readDaemonInstance,
|
|
daemonLogPath,
|
|
isSameDaemonInstance,
|
|
DaemonInstanceError,
|
|
} from "@getpaseo/server/daemon-control";
|
|
import { readPersistedConfig, resolveConfigFromPersisted } from "@getpaseo/server/configuration";
|
|
import { connectToDaemon, buildDaemonConnectionCommandError } from "../../utils/client.js";
|
|
import { withOutput, toCommandError, type CommandOptions } from "../../output/index.js";
|
|
import { addJsonAndDaemonHostOptions } from "../../utils/command-options.js";
|
|
import { describeDaemonTarget, type DaemonTarget } from "../../utils/daemon-target.js";
|
|
|
|
export function daemonStatusCommand(): Command {
|
|
return addJsonAndDaemonHostOptions(
|
|
new Command("status").description("Observe the selected daemon and its published endpoint"),
|
|
).action(withOutput(runStatusCommand));
|
|
}
|
|
|
|
export async function runStatusCommand(options: CommandOptions, _command: Command) {
|
|
const target = options.daemonTarget;
|
|
const instance = target.kind === "instance" ? await readDaemonInstance(target.home) : null;
|
|
const local =
|
|
target.kind === "instance"
|
|
? localStatus(target.home, instance)
|
|
: { host: describeDaemonTarget(target) };
|
|
const observed =
|
|
target.kind === "endpoint" || instance?.listen
|
|
? await probeDaemonStatus(target, instance, local)
|
|
: { connectedDaemon: "not_probed" };
|
|
const data: Record<string, unknown> = { ...local, ...observed };
|
|
return {
|
|
type: "single" as const,
|
|
data,
|
|
schema: {
|
|
idField: () => "daemon",
|
|
columns: [],
|
|
renderHuman: () =>
|
|
Object.entries(data)
|
|
.filter(([, value]) => value !== undefined)
|
|
.map(
|
|
([key, value]) =>
|
|
`${key}: ${typeof value === "object" ? JSON.stringify(value) : value}`,
|
|
)
|
|
.join("\n"),
|
|
},
|
|
};
|
|
}
|
|
|
|
async function probeDaemonStatus(
|
|
target: DaemonTarget,
|
|
instance: Awaited<ReturnType<typeof readDaemonInstance>>,
|
|
local: Record<string, unknown>,
|
|
) {
|
|
let connectedDaemon = "unreachable";
|
|
let note: string | undefined;
|
|
let live: Record<string, unknown> = {};
|
|
let client: Awaited<ReturnType<typeof connectToDaemon>> | undefined;
|
|
try {
|
|
client = await connectToDaemon({ target, instance: instance ?? undefined, timeout: 1_500 });
|
|
} catch (error) {
|
|
const failure = buildDaemonConnectionCommandError({ target, error });
|
|
if (target.kind === "endpoint") throw failure;
|
|
connectedDaemon = "unreachable";
|
|
if (failure.code === "AUTH_REQUIRED") connectedDaemon = "auth_required";
|
|
if (failure.code === "AUTH_FAILED") connectedDaemon = "auth_failed";
|
|
note = failure.message;
|
|
}
|
|
if (!client) return { connectedDaemon, note };
|
|
try {
|
|
let requestError: unknown;
|
|
const status = await client.getDaemonStatus({ timeout: 1_500 }).catch((error: unknown) => {
|
|
requestError = error;
|
|
return null;
|
|
});
|
|
// Recheck even when the RPC times out before combining local and live observations.
|
|
const current = target.kind === "instance" ? await readDaemonInstance(target.home) : null;
|
|
if (instance && (!current || !isSameDaemonInstance(instance, current)))
|
|
throw new DaemonInstanceError(
|
|
"DAEMON_REPLACED",
|
|
"Supervisor exited or was replaced during status observation.",
|
|
);
|
|
const info = client.getLastServerInfoMessage();
|
|
live = { serverId: info?.serverId, daemonVersion: info?.version };
|
|
connectedDaemon = client.isConnected ? "reachable" : "unreachable";
|
|
if (!status) {
|
|
const failure = toCommandError(requestError);
|
|
note = `Status details unavailable (${failure.code}): ${failure.message}`;
|
|
if (
|
|
target.kind === "endpoint" ||
|
|
!client.isConnected ||
|
|
!(requestError instanceof DaemonConnectionError) ||
|
|
requestError.code !== "DAEMON_REQUEST_TIMEOUT"
|
|
) {
|
|
throw { ...failure, message: note, details: { ...local, ...live, connectedDaemon } };
|
|
}
|
|
} else {
|
|
live = {
|
|
...live,
|
|
daemonVersion: status.version,
|
|
workerPid: status.pid,
|
|
daemonNode: status.nodePath,
|
|
providers: status.providers,
|
|
relay: status.relay,
|
|
};
|
|
}
|
|
} finally {
|
|
await client.close();
|
|
}
|
|
return { ...live, connectedDaemon, note };
|
|
}
|
|
|
|
function localStatus(home: string, instance: Awaited<ReturnType<typeof readDaemonInstance>>) {
|
|
const config = resolveConfigFromPersisted(
|
|
home,
|
|
readPersistedConfig(home, { defaultsIfMissing: true }),
|
|
{ env: {} },
|
|
);
|
|
let localDaemon = "stopped";
|
|
if (instance) localDaemon = instance.listen ? "running" : "not_ready";
|
|
return {
|
|
home,
|
|
pid: instance?.pid ?? null,
|
|
startedAt: instance?.startedAt ?? null,
|
|
listen: instance?.listen ?? null,
|
|
serverId: instance?.serverId ?? undefined,
|
|
hostname: instance?.hostname ?? null,
|
|
configuredListen: config.listen,
|
|
localDaemon,
|
|
desktopManaged: instance?.desktopManaged === true,
|
|
logPath: daemonLogPath(home),
|
|
};
|
|
}
|