Switch to double quotes and reformat codebase with Biome

This commit is contained in:
Mohamed Boudra
2026-03-21 20:22:15 +07:00
parent ab40786882
commit f6579dbeb4
805 changed files with 38132 additions and 40175 deletions
+34 -43
View File
@@ -1,53 +1,44 @@
import { Command } from 'commander'
import { startCommand } from './start.js'
import { runStatusCommand } from './status.js'
import { runStopCommand } from './stop.js'
import { runRestartCommand } from './restart.js'
import { pairCommand } from './pair.js'
import { withOutput } from '../../output/index.js'
import { addJsonOption } from '../../utils/command-options.js'
import { Command } from "commander";
import { startCommand } from "./start.js";
import { runStatusCommand } from "./status.js";
import { runStopCommand } from "./stop.js";
import { runRestartCommand } from "./restart.js";
import { pairCommand } from "./pair.js";
import { withOutput } from "../../output/index.js";
import { addJsonOption } from "../../utils/command-options.js";
export function createDaemonCommand(): Command {
const daemon = new Command('daemon').description('Manage the Paseo daemon')
const daemon = new Command("daemon").description("Manage the Paseo daemon");
daemon.addCommand(startCommand())
daemon.addCommand(pairCommand())
daemon.addCommand(startCommand());
daemon.addCommand(pairCommand());
addJsonOption(
daemon
.command('status')
.description('Show local daemon status')
)
.option('--home <path>', 'Paseo home directory (default: ~/.paseo)')
.action(withOutput(runStatusCommand))
addJsonOption(daemon.command("status").description("Show local daemon status"))
.option("--home <path>", "Paseo home directory (default: ~/.paseo)")
.action(withOutput(runStatusCommand));
addJsonOption(
daemon
.command('stop')
.description('Stop the local daemon')
)
.option('--home <path>', 'Paseo home directory (default: ~/.paseo)')
.option('--timeout <seconds>', 'Wait timeout before failing (default: 15)')
.option('--force', 'Send SIGKILL if graceful stop times out')
.action(withOutput(runStopCommand))
addJsonOption(daemon.command("stop").description("Stop the local daemon"))
.option("--home <path>", "Paseo home directory (default: ~/.paseo)")
.option("--timeout <seconds>", "Wait timeout before failing (default: 15)")
.option("--force", "Send SIGKILL if graceful stop times out")
.action(withOutput(runStopCommand));
addJsonOption(
daemon
.command('restart')
.description('Restart the local daemon')
)
.option('--home <path>', 'Paseo home directory (default: ~/.paseo)')
.option('--timeout <seconds>', 'Wait timeout before force step (default: 15)')
.option('--force', 'Send SIGKILL if graceful stop times out')
.option('--listen <listen>', 'Listen target for restarted daemon (host:port, port, or unix socket)')
.option('--port <port>', 'Port for restarted daemon listen target')
.option('--no-relay', 'Disable relay on restarted daemon')
.option('--no-mcp', 'Disable Agent MCP on restarted daemon')
addJsonOption(daemon.command("restart").description("Restart the local daemon"))
.option("--home <path>", "Paseo home directory (default: ~/.paseo)")
.option("--timeout <seconds>", "Wait timeout before force step (default: 15)")
.option("--force", "Send SIGKILL if graceful stop times out")
.option(
'--allowed-hosts <hosts>',
'Comma-separated Host allowlist values (example: "localhost,.example.com" or "true")'
"--listen <listen>",
"Listen target for restarted daemon (host:port, port, or unix socket)",
)
.action(withOutput(runRestartCommand))
.option("--port <port>", "Port for restarted daemon listen target")
.option("--no-relay", "Disable relay on restarted daemon")
.option("--no-mcp", "Disable Agent MCP on restarted daemon")
.option(
"--allowed-hosts <hosts>",
'Comma-separated Host allowlist values (example: "localhost,.example.com" or "true")',
)
.action(withOutput(runRestartCommand));
return daemon
return daemon;
}
+230 -231
View File
@@ -1,312 +1,313 @@
import { spawn, spawnSync } from 'node:child_process'
import { existsSync, readFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import path from 'node:path'
import { loadConfig, resolvePaseoHome } from '@getpaseo/server'
import { tryConnectToDaemon } from '../../utils/client.js'
import { spawn, spawnSync } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import { loadConfig, resolvePaseoHome } from "@getpaseo/server";
import { tryConnectToDaemon } from "../../utils/client.js";
export interface DaemonStartOptions {
port?: string
listen?: string
home?: string
foreground?: boolean
relay?: boolean
mcp?: boolean
allowedHosts?: string
port?: string;
listen?: string;
home?: string;
foreground?: boolean;
relay?: boolean;
mcp?: boolean;
allowedHosts?: string;
}
export interface LocalDaemonPidInfo {
pid: number
startedAt?: string
hostname?: string
uid?: number
listen?: string
pid: number;
startedAt?: string;
hostname?: string;
uid?: number;
listen?: string;
}
export interface LocalDaemonState {
home: string
listen: string
logPath: string
pidPath: string
pidInfo: LocalDaemonPidInfo | null
running: boolean
stalePidFile: boolean
home: string;
listen: string;
logPath: string;
pidPath: string;
pidInfo: LocalDaemonPidInfo | null;
running: boolean;
stalePidFile: boolean;
}
export interface DetachedStartResult {
pid: number | null
logPath: string
pid: number | null;
logPath: string;
}
export interface StopLocalDaemonOptions {
home?: string
timeoutMs?: number
force?: boolean
home?: string;
timeoutMs?: number;
force?: boolean;
}
export interface StopLocalDaemonResult {
action: 'stopped' | 'not_running'
home: string
pid: number | null
forced: boolean
message: string
action: "stopped" | "not_running";
home: string;
pid: number | null;
forced: boolean;
message: string;
}
type ProcessExitDetails = {
code: number | null
signal: NodeJS.Signals | null
error?: Error
}
code: number | null;
signal: NodeJS.Signals | null;
error?: Error;
};
type DetachedStartupResult =
| { exitedEarly: false }
| ({ exitedEarly: true } & ProcessExitDetails)
type DetachedStartupResult = { exitedEarly: false } | ({ exitedEarly: true } & ProcessExitDetails);
const DETACHED_STARTUP_GRACE_MS = 1200
const PID_POLL_INTERVAL_MS = 100
const KILL_TIMEOUT_MS = 3000
const DAEMON_LOG_FILENAME = 'daemon.log'
const DAEMON_PID_FILENAME = 'paseo.pid'
const DETACHED_STARTUP_GRACE_MS = 1200;
const PID_POLL_INTERVAL_MS = 100;
const KILL_TIMEOUT_MS = 3000;
const DAEMON_LOG_FILENAME = "daemon.log";
const DAEMON_PID_FILENAME = "paseo.pid";
export const DEFAULT_STOP_TIMEOUT_MS = 15_000
export const DEFAULT_STOP_TIMEOUT_MS = 15_000;
const require = createRequire(import.meta.url)
const require = createRequire(import.meta.url);
const startupReady = (): DetachedStartupResult => ({ exitedEarly: false })
const startupReady = (): DetachedStartupResult => ({ exitedEarly: false });
const startupExited = (details: ProcessExitDetails): DetachedStartupResult => ({
exitedEarly: true,
...details,
})
});
function envWithHome(home?: string): NodeJS.ProcessEnv {
if (!home) {
return process.env
return process.env;
}
return { ...process.env, PASEO_HOME: home }
return { ...process.env, PASEO_HOME: home };
}
function buildRunnerArgs(options: DaemonStartOptions): string[] {
const args: string[] = []
const args: string[] = [];
if (options.relay === false) {
args.push('--no-relay')
args.push("--no-relay");
}
if (options.mcp === false) {
args.push('--no-mcp')
args.push("--no-mcp");
}
return args
return args;
}
function buildChildEnv(options: DaemonStartOptions): NodeJS.ProcessEnv {
const childEnv: NodeJS.ProcessEnv = { ...process.env }
const childEnv: NodeJS.ProcessEnv = { ...process.env };
if (options.home) {
childEnv.PASEO_HOME = options.home
childEnv.PASEO_HOME = options.home;
}
if (options.listen) {
childEnv.PASEO_LISTEN = options.listen
childEnv.PASEO_LISTEN = options.listen;
} else if (options.port) {
childEnv.PASEO_LISTEN = `127.0.0.1:${options.port}`
childEnv.PASEO_LISTEN = `127.0.0.1:${options.port}`;
}
if (options.allowedHosts) {
childEnv.PASEO_ALLOWED_HOSTS = options.allowedHosts
childEnv.PASEO_ALLOWED_HOSTS = options.allowedHosts;
}
return childEnv
return childEnv;
}
function resolveDaemonRunnerEntry(): string {
const serverExportPath = require.resolve('@getpaseo/server')
let currentDir = path.dirname(serverExportPath)
const serverExportPath = require.resolve("@getpaseo/server");
let currentDir = path.dirname(serverExportPath);
while (true) {
const packageJsonPath = path.join(currentDir, 'package.json')
const packageJsonPath = path.join(currentDir, "package.json");
if (existsSync(packageJsonPath)) {
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as { name?: string }
if (packageJson.name === '@getpaseo/server') {
const distRunner = path.join(currentDir, 'dist', 'scripts', 'daemon-runner.js')
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as { name?: string };
if (packageJson.name === "@getpaseo/server") {
const distRunner = path.join(currentDir, "dist", "scripts", "daemon-runner.js");
if (existsSync(distRunner)) {
return distRunner
return distRunner;
}
return path.join(currentDir, 'scripts', 'daemon-runner.ts')
return path.join(currentDir, "scripts", "daemon-runner.ts");
}
} catch {
// Continue searching up if package.json exists but is invalid.
}
}
const parentDir = path.dirname(currentDir)
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir) {
break
break;
}
currentDir = parentDir
currentDir = parentDir;
}
throw new Error('Unable to resolve @getpaseo/server package root for daemon runner')
throw new Error("Unable to resolve @getpaseo/server package root for daemon runner");
}
function pidFilePath(paseoHome: string): string {
return path.join(paseoHome, DAEMON_PID_FILENAME)
return path.join(paseoHome, DAEMON_PID_FILENAME);
}
function readPidFile(pidPath: string): LocalDaemonPidInfo | null {
try {
const parsed = JSON.parse(readFileSync(pidPath, 'utf-8')) as Record<string, unknown>
const pidValue = parsed.pid
if (typeof pidValue !== 'number' || !Number.isInteger(pidValue) || pidValue <= 0) {
return null
const parsed = JSON.parse(readFileSync(pidPath, "utf-8")) as Record<string, unknown>;
const pidValue = parsed.pid;
if (typeof pidValue !== "number" || !Number.isInteger(pidValue) || pidValue <= 0) {
return null;
}
return {
pid: pidValue,
startedAt: typeof parsed.startedAt === 'string' ? parsed.startedAt : undefined,
hostname: typeof parsed.hostname === 'string' ? parsed.hostname : undefined,
uid: typeof parsed.uid === 'number' ? parsed.uid : undefined,
listen: typeof parsed.listen === 'string' ? parsed.listen : typeof parsed.sockPath === 'string' ? parsed.sockPath : undefined,
}
startedAt: typeof parsed.startedAt === "string" ? parsed.startedAt : undefined,
hostname: typeof parsed.hostname === "string" ? parsed.hostname : undefined,
uid: typeof parsed.uid === "number" ? parsed.uid : undefined,
listen:
typeof parsed.listen === "string"
? parsed.listen
: typeof parsed.sockPath === "string"
? parsed.sockPath
: undefined,
};
} catch {
return null
return null;
}
}
function tailFile(filePath: string, lines = 30): string | null {
try {
const content = readFileSync(filePath, 'utf-8')
return content.split('\n').filter(Boolean).slice(-lines).join('\n')
const content = readFileSync(filePath, "utf-8");
return content.split("\n").filter(Boolean).slice(-lines).join("\n");
} catch {
return null
return null;
}
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms)
})
setTimeout(resolve, ms);
});
}
function readNodeErrnoCode(error: unknown): string | undefined {
if (typeof error !== 'object' || error === null || !('code' in error)) {
return undefined
if (typeof error !== "object" || error === null || !("code" in error)) {
return undefined;
}
return typeof error.code === 'string' ? error.code : undefined
return typeof error.code === "string" ? error.code : undefined;
}
function isProcessRunning(pid: number): boolean {
try {
process.kill(pid, 0)
return true
process.kill(pid, 0);
return true;
} catch (err) {
const code = readNodeErrnoCode(err)
if (code === 'EPERM') {
return true
const code = readNodeErrnoCode(err);
if (code === "EPERM") {
return true;
}
return false
return false;
}
}
function signalProcess(pid: number, signal: NodeJS.Signals): boolean {
try {
process.kill(pid, signal)
return true
process.kill(pid, signal);
return true;
} catch (err) {
const code = readNodeErrnoCode(err)
if (code === 'ESRCH') {
return false
const code = readNodeErrnoCode(err);
if (code === "ESRCH") {
return false;
}
throw err
throw err;
}
}
function signalProcessSafely(pid: number, signal: NodeJS.Signals): boolean {
if (!Number.isInteger(pid) || pid <= 1 || pid === process.pid) {
return false
return false;
}
try {
return signalProcess(pid, signal)
return signalProcess(pid, signal);
} catch (err) {
const code = readNodeErrnoCode(err)
if (code === 'EPERM') {
return true
const code = readNodeErrnoCode(err);
if (code === "EPERM") {
return true;
}
throw err
throw err;
}
}
function signalProcessGroupSafely(pid: number, signal: NodeJS.Signals): boolean {
if (!Number.isInteger(pid) || pid <= 1 || pid === process.pid) {
return false
return false;
}
if (process.platform === 'win32') {
return signalProcessSafely(pid, signal)
if (process.platform === "win32") {
return signalProcessSafely(pid, signal);
}
try {
process.kill(-pid, signal)
return true
process.kill(-pid, signal);
return true;
} catch (err) {
const code = readNodeErrnoCode(err)
if (code === 'ESRCH') {
return signalProcessSafely(pid, signal)
const code = readNodeErrnoCode(err);
if (code === "ESRCH") {
return signalProcessSafely(pid, signal);
}
if (code === 'EPERM') {
return true
if (code === "EPERM") {
return true;
}
throw err
throw err;
}
}
async function waitForPidExit(pid: number, timeoutMs: number): Promise<boolean> {
const deadline = Date.now() + timeoutMs
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (!isProcessRunning(pid)) {
return true
return true;
}
await sleep(PID_POLL_INTERVAL_MS)
await sleep(PID_POLL_INTERVAL_MS);
}
return !isProcessRunning(pid)
return !isProcessRunning(pid);
}
type LifecycleShutdownAttempt =
| { requested: true }
| { requested: false; reason: string }
type LifecycleShutdownAttempt = { requested: true } | { requested: false; reason: string };
function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error)
return error instanceof Error ? error.message : String(error);
}
export function resolveLocalPaseoHome(home?: string): string {
return resolvePaseoHome(envWithHome(home))
return resolvePaseoHome(envWithHome(home));
}
export function resolveTcpHostFromListen(listen: string): string | null {
const normalized = listen.trim()
const normalized = listen.trim();
if (!normalized) {
return null
return null;
}
if (
normalized.startsWith('/') ||
normalized.startsWith('unix://') ||
normalized.startsWith('pipe://') ||
normalized.startsWith('\\\\.\\pipe\\')
normalized.startsWith("/") ||
normalized.startsWith("unix://") ||
normalized.startsWith("pipe://") ||
normalized.startsWith("\\\\.\\pipe\\")
) {
return null
return null;
}
if (/^\d+$/.test(normalized)) {
return `127.0.0.1:${normalized}`
return `127.0.0.1:${normalized}`;
}
if (normalized.includes(':')) {
return normalized
if (normalized.includes(":")) {
return normalized;
}
return null
return null;
}
export function resolveLocalDaemonState(options: { home?: string } = {}): LocalDaemonState {
@@ -315,14 +316,14 @@ export function resolveLocalDaemonState(options: { home?: string } = {}): LocalD
// Status should reflect local persisted config + pid file, not inherited daemon env overrides.
PASEO_LISTEN: undefined,
PASEO_ALLOWED_HOSTS: undefined,
}
const home = resolvePaseoHome(env)
const config = loadConfig(home, { env })
const pidPath = pidFilePath(home)
const logPath = path.join(home, DAEMON_LOG_FILENAME)
const pidInfo = existsSync(pidPath) ? readPidFile(pidPath) : null
const running = pidInfo ? isProcessRunning(pidInfo.pid) : false
const listen = pidInfo?.listen ?? config.listen
};
const home = resolvePaseoHome(env);
const config = loadConfig(home, { env });
const pidPath = pidFilePath(home);
const logPath = path.join(home, DAEMON_LOG_FILENAME);
const pidInfo = existsSync(pidPath) ? readPidFile(pidPath) : null;
const running = pidInfo ? isProcessRunning(pidInfo.pid) : false;
const listen = pidInfo?.listen ?? config.listen;
return {
home,
@@ -332,199 +333,197 @@ export function resolveLocalDaemonState(options: { home?: string } = {}): LocalD
pidInfo,
running,
stalePidFile: Boolean(pidInfo) && !running,
}
};
}
export function tailDaemonLog(home?: string, lines = 30): string | null {
const logPath = path.join(resolveLocalPaseoHome(home), DAEMON_LOG_FILENAME)
return tailFile(logPath, lines)
const logPath = path.join(resolveLocalPaseoHome(home), DAEMON_LOG_FILENAME);
return tailFile(logPath, lines);
}
export async function startLocalDaemonDetached(
options: DaemonStartOptions
options: DaemonStartOptions,
): Promise<DetachedStartResult> {
if (options.listen && options.port) {
throw new Error('Cannot use --listen and --port together')
throw new Error("Cannot use --listen and --port together");
}
const childEnv = buildChildEnv(options)
const childEnv = buildChildEnv(options);
const paseoHome = resolvePaseoHome(childEnv)
const logPath = path.join(paseoHome, DAEMON_LOG_FILENAME)
const daemonRunnerEntry = resolveDaemonRunnerEntry()
const paseoHome = resolvePaseoHome(childEnv);
const logPath = path.join(paseoHome, DAEMON_LOG_FILENAME);
const daemonRunnerEntry = resolveDaemonRunnerEntry();
const child = spawn(
process.execPath,
[...process.execArgv, daemonRunnerEntry, ...buildRunnerArgs(options)],
{
detached: true,
env: childEnv,
stdio: ['ignore', 'ignore', 'ignore'],
}
)
stdio: ["ignore", "ignore", "ignore"],
},
);
child.unref()
child.unref();
const startup = await new Promise<DetachedStartupResult>((resolve) => {
let settled = false
let settled = false;
const finish = (value: DetachedStartupResult) => {
if (settled) return
settled = true
resolve(value)
}
if (settled) return;
settled = true;
resolve(value);
};
const timer = setTimeout(() => finish(startupReady()), DETACHED_STARTUP_GRACE_MS)
const timer = setTimeout(() => finish(startupReady()), DETACHED_STARTUP_GRACE_MS);
child.once('error', (error) => {
clearTimeout(timer)
finish(startupExited({ code: null, signal: null, error }))
})
child.once("error", (error) => {
clearTimeout(timer);
finish(startupExited({ code: null, signal: null, error }));
});
child.once('exit', (code, signal) => {
clearTimeout(timer)
finish(startupExited({ code, signal }))
})
})
child.once("exit", (code, signal) => {
clearTimeout(timer);
finish(startupExited({ code, signal }));
});
});
if (startup.exitedEarly) {
const reason = startup.error
? startup.error.message
: `exit code ${startup.code ?? 'unknown'}${startup.signal ? ` (${startup.signal})` : ''}`
const recentLogs = tailFile(logPath)
: `exit code ${startup.code ?? "unknown"}${startup.signal ? ` (${startup.signal})` : ""}`;
const recentLogs = tailFile(logPath);
throw new Error(
[
`Daemon failed to start in background (${reason}).`,
recentLogs ? `Recent daemon logs:\n${recentLogs}` : null,
]
.filter(Boolean)
.join('\n\n')
)
.join("\n\n"),
);
}
return {
pid: child.pid ?? null,
logPath,
}
};
}
export function startLocalDaemonForeground(options: DaemonStartOptions): number {
if (options.listen && options.port) {
throw new Error('Cannot use --listen and --port together')
throw new Error("Cannot use --listen and --port together");
}
const childEnv = buildChildEnv(options)
const daemonRunnerEntry = resolveDaemonRunnerEntry()
const childEnv = buildChildEnv(options);
const daemonRunnerEntry = resolveDaemonRunnerEntry();
const result = spawnSync(
process.execPath,
[...process.execArgv, daemonRunnerEntry, ...buildRunnerArgs(options)],
{
env: childEnv,
stdio: 'inherit',
}
)
stdio: "inherit",
},
);
if (result.error) {
throw result.error
throw result.error;
}
return result.status ?? 1
return result.status ?? 1;
}
async function requestLifecycleShutdown(
state: LocalDaemonState,
timeoutMs: number
timeoutMs: number,
): Promise<LifecycleShutdownAttempt> {
const host = resolveTcpHostFromListen(state.listen)
const host = resolveTcpHostFromListen(state.listen);
if (!host) {
return {
requested: false,
reason: 'daemon listen target is not TCP, falling back to owner PID signal',
}
reason: "daemon listen target is not TCP, falling back to owner PID signal",
};
}
const client = await tryConnectToDaemon({ host, timeout: Math.min(timeoutMs, 5000) })
const client = await tryConnectToDaemon({ host, timeout: Math.min(timeoutMs, 5000) });
if (!client) {
return {
requested: false,
reason: `daemon websocket at ${host} is not reachable, falling back to owner PID signal`,
}
};
}
try {
await client.shutdownServer()
return { requested: true }
await client.shutdownServer();
return { requested: true };
} catch (error) {
return {
requested: false,
reason: `daemon lifecycle shutdown request failed (${getErrorMessage(
error
error,
)}), falling back to owner PID signal`,
}
};
} finally {
await client.close().catch(() => undefined)
await client.close().catch(() => undefined);
}
}
export async function stopLocalDaemon(
options: StopLocalDaemonOptions = {}
options: StopLocalDaemonOptions = {},
): Promise<StopLocalDaemonResult> {
const timeoutMs = options.timeoutMs ?? DEFAULT_STOP_TIMEOUT_MS
const state = resolveLocalDaemonState({ home: options.home })
const timeoutMs = options.timeoutMs ?? DEFAULT_STOP_TIMEOUT_MS;
const state = resolveLocalDaemonState({ home: options.home });
if (!state.pidInfo || !state.running) {
const staleSuffix =
state.stalePidFile && state.pidInfo
? ` (stale PID file for ${state.pidInfo.pid})`
: ''
state.stalePidFile && state.pidInfo ? ` (stale PID file for ${state.pidInfo.pid})` : "";
return {
action: 'not_running',
action: "not_running",
home: state.home,
pid: state.pidInfo?.pid ?? null,
forced: false,
message: `Daemon is not running${staleSuffix}`,
}
};
}
const pid = state.pidInfo.pid
const shutdownAttempt = await requestLifecycleShutdown(state, timeoutMs)
const lifecycleRequested = shutdownAttempt.requested
const fallbackMessage = shutdownAttempt.requested ? null : shutdownAttempt.reason
let forced = false
const pid = state.pidInfo.pid;
const shutdownAttempt = await requestLifecycleShutdown(state, timeoutMs);
const lifecycleRequested = shutdownAttempt.requested;
const fallbackMessage = shutdownAttempt.requested ? null : shutdownAttempt.reason;
let forced = false;
if (!lifecycleRequested) {
const signaled = signalProcessSafely(pid, 'SIGTERM')
const signaled = signalProcessSafely(pid, "SIGTERM");
if (!signaled) {
return {
action: 'not_running',
action: "not_running",
home: state.home,
pid,
forced: false,
message: 'Daemon process was already stopped',
}
message: "Daemon process was already stopped",
};
}
}
let stopped = await waitForPidExit(pid, timeoutMs)
let stopped = await waitForPidExit(pid, timeoutMs);
if (!stopped && options.force) {
forced = true
signalProcessGroupSafely(pid, 'SIGKILL')
stopped = await waitForPidExit(pid, KILL_TIMEOUT_MS)
forced = true;
signalProcessGroupSafely(pid, "SIGKILL");
stopped = await waitForPidExit(pid, KILL_TIMEOUT_MS);
}
if (!stopped) {
throw new Error(
`Timed out waiting for daemon PID ${pid} to stop after ${Math.ceil(timeoutMs / 1000)}s`
)
`Timed out waiting for daemon PID ${pid} to stop after ${Math.ceil(timeoutMs / 1000)}s`,
);
}
return {
action: 'stopped',
action: "stopped",
home: state.home,
pid,
forced,
message: forced
? 'Daemon owner process was force-stopped'
? "Daemon owner process was force-stopped"
: lifecycleRequested
? 'Daemon stopped gracefully'
: fallbackMessage ?? 'Daemon stopped via owner PID signal',
}
? "Daemon stopped gracefully"
: (fallbackMessage ?? "Daemon stopped via owner PID signal"),
};
}
+23 -26
View File
@@ -1,31 +1,28 @@
import { Command } from 'commander'
import chalk from 'chalk'
import { generateLocalPairingOffer, loadConfig, resolvePaseoHome } from '@getpaseo/server'
import { addJsonOption } from '../../utils/command-options.js'
import { Command } from "commander";
import chalk from "chalk";
import { generateLocalPairingOffer, loadConfig, resolvePaseoHome } from "@getpaseo/server";
import { addJsonOption } from "../../utils/command-options.js";
interface PairOptions {
home?: string
json?: boolean
home?: string;
json?: boolean;
}
export function pairCommand(): Command {
return addJsonOption(
new Command('pair')
.description('Print the daemon pairing QR code and link')
)
.option('--home <path>', 'Paseo home directory (default: ~/.paseo)')
return addJsonOption(new Command("pair").description("Print the daemon pairing QR code and link"))
.option("--home <path>", "Paseo home directory (default: ~/.paseo)")
.action(async (_options: PairOptions, command: Command) => {
await runPairCommand(command.optsWithGlobals() as PairOptions)
})
await runPairCommand(command.optsWithGlobals() as PairOptions);
});
}
export async function runPairCommand(options: PairOptions): Promise<void> {
if (options.home) {
process.env.PASEO_HOME = options.home
process.env.PASEO_HOME = options.home;
}
const paseoHome = resolvePaseoHome()
const config = loadConfig(paseoHome)
const paseoHome = resolvePaseoHome();
const config = loadConfig(paseoHome);
const pairing = await generateLocalPairingOffer({
paseoHome,
relayEnabled: config.relayEnabled,
@@ -33,12 +30,12 @@ export async function runPairCommand(options: PairOptions): Promise<void> {
relayPublicEndpoint: config.relayPublicEndpoint,
appBaseUrl: config.appBaseUrl,
includeQr: true,
})
});
if (!pairing.relayEnabled || !pairing.url) {
console.error(chalk.red('Relay pairing is disabled for this daemon config.'))
console.error(chalk.yellow('Enable relay and run this command again.'))
process.exit(1)
console.error(chalk.red("Relay pairing is disabled for this daemon config."));
console.error(chalk.yellow("Enable relay and run this command again."));
process.exit(1);
}
if (options.json) {
@@ -50,12 +47,12 @@ export async function runPairCommand(options: PairOptions): Promise<void> {
qr: pairing.qr,
},
null,
2
)}\n`
)
return
2,
)}\n`,
);
return;
}
const qrBlock = pairing.qr ? `${pairing.qr}\n` : ''
process.stdout.write(`\nScan to pair:\n${qrBlock}${pairing.url}\n`)
const qrBlock = pairing.qr ? `${pairing.qr}\n` : "";
process.stdout.write(`\nScan to pair:\n${qrBlock}${pairing.url}\n`);
}
+62 -56
View File
@@ -1,123 +1,129 @@
import type { Command } from 'commander'
import type { Command } from "commander";
import {
startLocalDaemonDetached,
stopLocalDaemon,
DEFAULT_STOP_TIMEOUT_MS,
type DaemonStartOptions,
} from './local-daemon.js'
import type { CommandOptions, SingleResult, OutputSchema, CommandError } from '../../output/index.js'
} from "./local-daemon.js";
import type {
CommandOptions,
SingleResult,
OutputSchema,
CommandError,
} from "../../output/index.js";
interface RestartResult {
action: 'restarted'
home: string
pid: string
message: string
action: "restarted";
home: string;
pid: string;
message: string;
}
const restartResultSchema: OutputSchema<RestartResult> = {
idField: 'action',
idField: "action",
columns: [
{
header: 'STATUS',
field: 'action',
color: () => 'green',
header: "STATUS",
field: "action",
color: () => "green",
},
{ header: 'HOME', field: 'home' },
{ header: 'PID', field: 'pid' },
{ header: 'MESSAGE', field: 'message' },
{ header: "HOME", field: "home" },
{ header: "PID", field: "pid" },
{ header: "MESSAGE", field: "message" },
],
}
};
export type RestartCommandResult = SingleResult<RestartResult>
export type RestartCommandResult = SingleResult<RestartResult>;
function parseTimeoutMs(raw: unknown): number {
if (typeof raw !== 'string' || raw.trim().length === 0) {
return DEFAULT_STOP_TIMEOUT_MS
if (typeof raw !== "string" || raw.trim().length === 0) {
return DEFAULT_STOP_TIMEOUT_MS;
}
const seconds = Number(raw)
const seconds = Number(raw);
if (!Number.isFinite(seconds) || seconds <= 0) {
const error: CommandError = {
code: 'INVALID_TIMEOUT',
code: "INVALID_TIMEOUT",
message: `Invalid timeout value: ${raw}`,
details: 'Timeout must be a positive number of seconds',
}
throw error
details: "Timeout must be a positive number of seconds",
};
throw error;
}
return Math.ceil(seconds * 1000)
return Math.ceil(seconds * 1000);
}
function toStartOptions(options: CommandOptions): DaemonStartOptions {
const startOptions: DaemonStartOptions = {
home: typeof options.home === 'string' ? options.home : undefined,
listen: typeof options.listen === 'string' ? options.listen : undefined,
port: typeof options.port === 'string' ? options.port : undefined,
relay: typeof options.relay === 'boolean' ? options.relay : undefined,
mcp: typeof options.mcp === 'boolean' ? options.mcp : undefined,
allowedHosts: typeof options.allowedHosts === 'string' ? options.allowedHosts : undefined,
}
home: typeof options.home === "string" ? options.home : undefined,
listen: typeof options.listen === "string" ? options.listen : undefined,
port: typeof options.port === "string" ? options.port : undefined,
relay: typeof options.relay === "boolean" ? options.relay : undefined,
mcp: typeof options.mcp === "boolean" ? options.mcp : undefined,
allowedHosts: typeof options.allowedHosts === "string" ? options.allowedHosts : undefined,
};
if (startOptions.listen && startOptions.port) {
const error: CommandError = {
code: 'INVALID_OPTIONS',
message: 'Cannot use --listen and --port together',
}
throw error
code: "INVALID_OPTIONS",
message: "Cannot use --listen and --port together",
};
throw error;
}
return startOptions
return startOptions;
}
export async function runRestartCommand(
options: CommandOptions,
_command: Command
_command: Command,
): Promise<RestartCommandResult> {
const timeoutMs = parseTimeoutMs(options.timeout)
const force = options.force === true
const startOptions = toStartOptions(options)
const timeoutMs = parseTimeoutMs(options.timeout);
const force = options.force === true;
const startOptions = toStartOptions(options);
try {
let stopResult: Awaited<ReturnType<typeof stopLocalDaemon>>
let stopResult: Awaited<ReturnType<typeof stopLocalDaemon>>;
try {
stopResult = await stopLocalDaemon({
home: startOptions.home,
timeoutMs,
force,
})
});
} catch (err) {
const isTimeout = err instanceof Error && err.message.includes('Timed out waiting for daemon PID')
const isTimeout =
err instanceof Error && err.message.includes("Timed out waiting for daemon PID");
if (!force && isTimeout) {
stopResult = await stopLocalDaemon({
home: startOptions.home,
timeoutMs,
force: true,
})
});
} else {
throw err
throw err;
}
}
const startup = await startLocalDaemonDetached(startOptions)
const before = stopResult.pid === null ? 'not running' : `PID ${stopResult.pid}`
const after = startup.pid === null ? 'unknown PID' : `PID ${startup.pid}`
const startup = await startLocalDaemonDetached(startOptions);
const before = stopResult.pid === null ? "not running" : `PID ${stopResult.pid}`;
const after = startup.pid === null ? "unknown PID" : `PID ${startup.pid}`;
return {
type: 'single',
type: "single",
data: {
action: 'restarted',
action: "restarted",
home: stopResult.home,
pid: startup.pid === null ? '-' : String(startup.pid),
pid: startup.pid === null ? "-" : String(startup.pid),
message: `Local daemon restarted (${before} -> ${after})`,
},
schema: restartResultSchema,
}
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: 'RESTART_FAILED',
code: "RESTART_FAILED",
message: `Failed to restart local daemon: ${message}`,
}
throw error
};
throw error;
}
}
@@ -1,46 +1,45 @@
import { spawnSync } from 'node:child_process'
import { spawnSync } from "node:child_process";
export interface NodePathFromPidResult {
nodePath: string | null
error?: string
nodePath: string | null;
error?: string;
}
function normalizeError(error: unknown): string {
if (error instanceof Error) {
return error.message
return error.message;
}
return String(error)
return String(error);
}
export function resolveNodePathFromPid(pid: number): NodePathFromPidResult {
const result = spawnSync('ps', ['-o', 'comm=', '-p', String(pid)], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
})
const result = spawnSync("ps", ["-o", "comm=", "-p", String(pid)], {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
});
if (result.error) {
return {
nodePath: null,
error: `ps failed: ${normalizeError(result.error)}`,
}
};
}
if ((result.status ?? 1) !== 0) {
const details = result.stderr?.trim()
const details = result.stderr?.trim();
return {
nodePath: null,
error: details ? `ps failed: ${details}` : `ps exited with code ${result.status ?? 1}`,
}
};
}
const resolved = result.stdout.trim()
const resolved = result.stdout.trim();
if (!resolved) {
return {
nodePath: null,
error: 'ps returned an empty command path',
}
error: "ps returned an empty command path",
};
}
return { nodePath: resolved }
return { nodePath: resolved };
}
+30 -30
View File
@@ -1,58 +1,58 @@
import { Command } from 'commander'
import chalk from 'chalk'
import { Command } from "commander";
import chalk from "chalk";
import {
startLocalDaemonForeground,
startLocalDaemonDetached,
type DaemonStartOptions as StartOptions,
} from './local-daemon.js'
import { getErrorMessage } from '../../utils/errors.js'
} from "./local-daemon.js";
import { getErrorMessage } from "../../utils/errors.js";
export type { DaemonStartOptions as StartOptions } from './local-daemon.js'
export type { DaemonStartOptions as StartOptions } from "./local-daemon.js";
export function startCommand(): Command {
return new Command('start')
.description('Start the local Paseo daemon')
.option('--listen <listen>', 'Listen target (host:port, port, or unix socket path)')
.option('--port <port>', 'Port to listen on (default: 6767)')
.option('--home <path>', 'Paseo home directory (default: ~/.paseo)')
.option('--foreground', 'Run in foreground (don\'t daemonize)')
.option('--no-relay', 'Disable relay connection')
.option('--no-mcp', 'Disable the Agent MCP HTTP endpoint')
return new Command("start")
.description("Start the local Paseo daemon")
.option("--listen <listen>", "Listen target (host:port, port, or unix socket path)")
.option("--port <port>", "Port to listen on (default: 6767)")
.option("--home <path>", "Paseo home directory (default: ~/.paseo)")
.option("--foreground", "Run in foreground (don't daemonize)")
.option("--no-relay", "Disable relay connection")
.option("--no-mcp", "Disable the Agent MCP HTTP endpoint")
.option(
'--allowed-hosts <hosts>',
'Comma-separated Host allowlist values (example: "localhost,.example.com" or "true")'
"--allowed-hosts <hosts>",
'Comma-separated Host allowlist values (example: "localhost,.example.com" or "true")',
)
.action(async (options: StartOptions) => {
await runStart(options)
})
await runStart(options);
});
}
export async function runStart(options: StartOptions): Promise<void> {
if (options.listen && options.port) {
console.error(chalk.red('Cannot use --listen and --port together'))
process.exit(1)
console.error(chalk.red("Cannot use --listen and --port together"));
process.exit(1);
}
if (!options.foreground) {
try {
const startup = await startLocalDaemonDetached(options)
console.log(chalk.green(`Daemon starting in background (PID ${startup.pid ?? 'unknown'}).`))
console.log(chalk.dim(`Logs: ${startup.logPath}`))
const startup = await startLocalDaemonDetached(options);
console.log(chalk.green(`Daemon starting in background (PID ${startup.pid ?? "unknown"}).`));
console.log(chalk.dim(`Logs: ${startup.logPath}`));
} catch (err) {
exitWithError(getErrorMessage(err))
exitWithError(getErrorMessage(err));
}
return
return;
}
try {
const status = startLocalDaemonForeground(options)
process.exit(status)
const status = startLocalDaemonForeground(options);
process.exit(status);
} catch (err) {
const message = getErrorMessage(err)
exitWithError(`Failed to start daemon: ${message}`)
const message = getErrorMessage(err);
exitWithError(`Failed to start daemon: ${message}`);
}
}
function exitWithError(message: string): never {
console.error(chalk.red(message))
process.exit(1)
console.error(chalk.red(message));
process.exit(1);
}
+139 -139
View File
@@ -1,260 +1,260 @@
import type { Command } from 'commander'
import { execFileSync } from 'node:child_process'
import { createRequire } from 'node:module'
import { getOrCreateServerId, findExecutable, applyProviderEnv } 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'
import type { Command } from "commander";
import { execFileSync } from "node:child_process";
import { createRequire } from "node:module";
import { getOrCreateServerId, findExecutable, applyProviderEnv } 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
label: string;
path: string | null;
version: string | null;
}
interface DaemonStatus {
serverId: string | null
status: 'running' | 'stopped' | 'unresponsive'
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
providers: ProviderBinaryStatus[]
note?: string
serverId: string | null;
status: "running" | "stopped" | "unresponsive";
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;
providers: ProviderBinaryStatus[];
note?: string;
}
interface StatusRow {
key: string
value: string
key: string;
value: string;
}
type CliPackageJson = {
version?: unknown
}
version?: unknown;
};
const require = createRequire(import.meta.url)
const require = createRequire(import.meta.url);
function normalizeError(error: unknown): string {
if (error instanceof Error) {
return error.message
return error.message;
}
return String(error)
return String(error);
}
function shortenMessage(message: string, max = 120): string {
const normalized = message.replace(/\s+/g, ' ').trim()
const normalized = message.replace(/\s+/g, " ").trim();
if (normalized.length <= max) {
return normalized
return normalized;
}
return `${normalized.slice(0, max - 3)}...`
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}`
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()
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'
return "unknown";
}
function createStatusSchema(status: DaemonStatus): OutputSchema<StatusRow> {
return {
idField: 'key',
idField: "key",
columns: [
{ header: 'KEY', field: 'key' },
{ header: "KEY", field: "key" },
{
header: 'VALUE',
field: 'value',
header: "VALUE",
field: "value",
color: (_, item) => {
if (item.key === 'Status') {
if (item.value === 'running') return 'green'
if (item.value === 'unresponsive') return 'yellow'
return 'red'
if (item.key === "Status") {
if (item.value === "running") return "green";
if (item.value === "unresponsive") 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'
if (item.key.startsWith(" ")) {
if (item.value === "not found") return "red";
if (item.value.endsWith("(--version failed)")) return "yellow";
return "green";
}
return undefined
return undefined;
},
},
],
serialize: () => status,
}
};
}
function toStatusRows(status: DaemonStatus): StatusRow[] {
const rows: StatusRow[] = [
{ key: 'Server ID', value: status.serverId ?? '-' },
{ key: 'Status', value: status.status },
{ 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: "Server ID", value: status.serverId ?? "-" },
{ key: "Status", value: status.status },
{ 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 },
];
if (status.runningAgents !== null && status.idleAgents !== null) {
rows.push({
key: 'Agents',
key: "Agents",
value: `${status.runningAgents} running, ${status.idleAgents} idle`,
})
});
} else {
rows.push({
key: 'Agents',
value: 'Unavailable (daemon API not reachable)',
})
key: "Agents",
value: "Unavailable (daemon API not reachable)",
});
}
if (status.note) {
rows.push({ key: 'Note', value: status.note })
rows.push({ key: "Note", value: status.note });
}
rows.push({ key: '', value: '' })
rows.push({ key: 'Providers', value: '' })
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' })
rows.push({ key: ` ${provider.label}`, value: "not found" });
} else if (!provider.version) {
rows.push({ key: ` ${provider.label}`, value: `${provider.path} (--version failed)` })
rows.push({ key: ` ${provider.label}`, value: `${provider.path} (--version failed)` });
} else {
rows.push({ key: ` ${provider.label}`, value: `${provider.path} (${provider.version})` })
rows.push({ key: ` ${provider.label}`, value: `${provider.path} (${provider.version})` });
}
}
return rows
return rows;
}
const PROVIDER_BINARIES: { label: string; binary: string }[] = [
{ label: 'Claude', binary: 'claude' },
{ label: 'Codex', binary: 'codex' },
{ label: 'OpenCode', binary: 'opencode' },
]
{ label: "Claude", binary: "claude" },
{ label: "Codex", binary: "codex" },
{ label: "OpenCode", binary: "opencode" },
];
function checkProviderBinary(binary: string): { path: string | null; version: string | null } {
const binaryPath = findExecutable(binary)
const binaryPath = findExecutable(binary);
if (!binaryPath) {
return { path: null, version: null }
return { path: null, version: null };
}
const env = applyProviderEnv(process.env)
const env = applyProviderEnv(process.env);
try {
const output = execFileSync(binaryPath, ['--version'], {
encoding: 'utf8',
const output = execFileSync(binaryPath, ["--version"], {
encoding: "utf8",
timeout: 5000,
stdio: ['ignore', 'pipe', 'pipe'],
stdio: ["ignore", "pipe", "pipe"],
env,
}).trim()
return { path: binaryPath, version: output || null }
}).trim();
return { path: binaryPath, version: output || null };
} catch {
return { path: binaryPath, version: null }
return { path: binaryPath, version: null };
}
}
function checkProviderBinaries(): ProviderBinaryStatus[] {
return PROVIDER_BINARIES.map(({ label, binary }) => {
const result = checkProviderBinary(binary)
return { label, ...result }
})
const result = checkProviderBinary(binary);
return { label, ...result };
});
}
function resolveOwnerLabel(uid: number | undefined, hostname: string | undefined): string | null {
if (uid === undefined && !hostname) {
return null
return null;
}
const uidPart = uid === undefined ? '?' : String(uid)
const hostPart = hostname ?? 'unknown-host'
return `${uidPart}@${hostPart}`
const uidPart = uid === undefined ? "?" : String(uid);
const hostPart = hostname ?? "unknown-host";
return `${uidPart}@${hostPart}`;
}
export type StatusResult = ListResult<StatusRow>
export type StatusResult = ListResult<StatusRow>;
export async function runStatusCommand(
options: CommandOptions,
_command: Command
_command: Command,
): Promise<StatusResult> {
const home = typeof options.home === 'string' ? options.home : undefined
const state = resolveLocalDaemonState({ home })
const home = typeof options.home === "string" ? options.home : undefined;
const state = resolveLocalDaemonState({ home });
const owner = resolveOwnerLabel(state.pidInfo?.uid, state.pidInfo?.hostname)
let daemonNode: string
const owner = resolveOwnerLabel(state.pidInfo?.uid, state.pidInfo?.hostname);
let daemonNode: string;
if (!state.running) {
daemonNode = '-'
daemonNode = "-";
} else if (state.pidInfo?.pid) {
const fromPid = resolveNodePathFromPid(state.pidInfo.pid)
daemonNode = fromPid.nodePath ?? `unknown (${fromPid.error ?? 'could not resolve from PID'})`
const fromPid = resolveNodePathFromPid(state.pidInfo.pid);
daemonNode = fromPid.nodePath ?? `unknown (${fromPid.error ?? "could not resolve from PID"})`;
} else {
daemonNode = 'unknown (no PID available)'
daemonNode = "unknown (no PID available)";
}
const cliNode = process.execPath
let status: DaemonStatus['status'] = state.running ? 'running' : 'stopped'
let runningAgents: number | null = null
let idleAgents: number | null = null
let note: string | undefined
const cliNode = process.execPath;
let status: DaemonStatus["status"] = state.running ? "running" : "stopped";
let runningAgents: number | null = null;
let idleAgents: number | null = null;
let note: string | undefined;
if (!state.running && state.stalePidFile && state.pidInfo) {
note = `Stale PID file found for PID ${state.pidInfo.pid}`
note = `Stale PID file found for PID ${state.pidInfo.pid}`;
}
if (state.running) {
const host = resolveTcpHostFromListen(state.listen)
const host = resolveTcpHostFromListen(state.listen);
if (host) {
const client = await tryConnectToDaemon({ host, timeout: 1500 })
const client = await tryConnectToDaemon({ host, timeout: 1500 });
if (client) {
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
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;
} catch {
status = 'unresponsive'
note = appendNote(note, `Daemon PID is running but API requests to ${host} failed`)
status = "unresponsive";
note = appendNote(note, `Daemon PID is running but API requests to ${host} failed`);
} finally {
await client.close().catch(() => {})
await client.close().catch(() => {});
}
} else {
status = 'unresponsive'
note = appendNote(note, `Daemon PID is running but websocket at ${host} is not reachable`)
status = "unresponsive";
note = appendNote(note, `Daemon PID is running but websocket at ${host} is not reachable`);
}
} else {
note = appendNote(note, 'Daemon is configured for unix socket listen; API probe skipped')
note = appendNote(note, "Daemon is configured for unix socket listen; API probe skipped");
}
}
const cliVersion = resolveCliVersion()
const cliVersion = resolveCliVersion();
let serverId: string | null = null
let serverId: string | null = null;
try {
serverId = getOrCreateServerId(state.home)
serverId = getOrCreateServerId(state.home);
} catch (error) {
note = appendNote(note, `serverId unavailable: ${shortenMessage(normalizeError(error))}`)
note = appendNote(note, `serverId unavailable: ${shortenMessage(normalizeError(error))}`);
}
const providers = checkProviderBinaries()
const providers = checkProviderBinaries();
const daemonStatus: DaemonStatus = {
serverId,
@@ -273,11 +273,11 @@ export async function runStatusCommand(
cliVersion,
providers,
note,
}
};
return {
type: 'list',
type: "list",
data: toStatusRows(daemonStatus),
schema: createStatusSchema(daemonStatus),
}
};
}
+41 -36
View File
@@ -1,74 +1,79 @@
import type { Command } from 'commander'
import { stopLocalDaemon, DEFAULT_STOP_TIMEOUT_MS } from './local-daemon.js'
import type { CommandOptions, SingleResult, OutputSchema, CommandError } from '../../output/index.js'
import type { Command } from "commander";
import { stopLocalDaemon, DEFAULT_STOP_TIMEOUT_MS } from "./local-daemon.js";
import type {
CommandOptions,
SingleResult,
OutputSchema,
CommandError,
} from "../../output/index.js";
interface StopResult {
action: 'stopped' | 'not_running'
home: string
pid: string
message: string
action: "stopped" | "not_running";
home: string;
pid: string;
message: string;
}
const stopResultSchema: OutputSchema<StopResult> = {
idField: 'action',
idField: "action",
columns: [
{
header: 'STATUS',
field: 'action',
color: (value) => (value === 'stopped' ? 'green' : 'yellow'),
header: "STATUS",
field: "action",
color: (value) => (value === "stopped" ? "green" : "yellow"),
},
{ header: 'HOME', field: 'home' },
{ header: 'PID', field: 'pid' },
{ header: 'MESSAGE', field: 'message' },
{ header: "HOME", field: "home" },
{ header: "PID", field: "pid" },
{ header: "MESSAGE", field: "message" },
],
}
};
export type StopCommandResult = SingleResult<StopResult>
export type StopCommandResult = SingleResult<StopResult>;
function parseTimeoutMs(raw: unknown): number {
if (typeof raw !== 'string' || raw.trim().length === 0) {
return DEFAULT_STOP_TIMEOUT_MS
if (typeof raw !== "string" || raw.trim().length === 0) {
return DEFAULT_STOP_TIMEOUT_MS;
}
const seconds = Number(raw)
const seconds = Number(raw);
if (!Number.isFinite(seconds) || seconds <= 0) {
const error: CommandError = {
code: 'INVALID_TIMEOUT',
code: "INVALID_TIMEOUT",
message: `Invalid timeout value: ${raw}`,
details: 'Timeout must be a positive number of seconds',
}
throw error
details: "Timeout must be a positive number of seconds",
};
throw error;
}
return Math.ceil(seconds * 1000)
return Math.ceil(seconds * 1000);
}
export async function runStopCommand(
options: CommandOptions,
_command: Command
_command: Command,
): Promise<StopCommandResult> {
const home = typeof options.home === 'string' ? options.home : undefined
const force = options.force === true
const timeoutMs = parseTimeoutMs(options.timeout)
const home = typeof options.home === "string" ? options.home : undefined;
const force = options.force === true;
const timeoutMs = parseTimeoutMs(options.timeout);
try {
const result = await stopLocalDaemon({ home, force, timeoutMs })
const result = await stopLocalDaemon({ home, force, timeoutMs });
return {
type: 'single',
type: "single",
data: {
action: result.action,
home: result.home,
pid: result.pid === null ? '-' : String(result.pid),
pid: result.pid === null ? "-" : String(result.pid),
message: result.message,
},
schema: stopResultSchema,
}
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
const message = err instanceof Error ? err.message : String(err);
const error: CommandError = {
code: 'STOP_FAILED',
code: "STOP_FAILED",
message: `Failed to stop local daemon: ${message}`,
}
throw error
};
throw error;
}
}