diff --git a/packages/cli/src/commands/daemon/status.ts b/packages/cli/src/commands/daemon/status.ts index 87edfe4b7..60a07cc2c 100644 --- a/packages/cli/src/commands/daemon/status.ts +++ b/packages/cli/src/commands/daemon/status.ts @@ -1,11 +1,18 @@ import type { Command } from 'commander' +import { execFileSync } from 'node:child_process' import { createRequire } from 'node:module' -import { getOrCreateServerId } from '@getpaseo/server' +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 +} + interface DaemonStatus { serverId: string | null status: 'running' | 'stopped' | 'unresponsive' @@ -21,6 +28,7 @@ interface DaemonStatus { daemonNode: string cliNode: string cliVersion: string + providers: ProviderBinaryStatus[] note?: string } @@ -77,16 +85,17 @@ function createStatusSchema(status: DaemonStatus): OutputSchema { header: 'VALUE', field: 'value', color: (_, item) => { - if (item.key !== 'Status') { - return undefined + if (item.key === 'Status') { + if (item.value === 'running') return 'green' + if (item.value === 'unresponsive') return 'yellow' + return 'red' } - if (item.value === 'running') { + if (item.key.startsWith(' ')) { + if (item.value === 'not found') return 'red' + if (item.value.endsWith('(--version failed)')) return 'yellow' return 'green' } - if (item.value === 'unresponsive') { - return 'yellow' - } - return 'red' + return undefined }, }, ], @@ -126,9 +135,53 @@ function toStatusRows(status: DaemonStatus): StatusRow[] { 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' }, +] + +function checkProviderBinary(binary: string): { path: string | null; version: string | null } { + const binaryPath = findExecutable(binary) + if (!binaryPath) { + return { path: null, version: null } + } + const env = applyProviderEnv(process.env) + try { + const output = execFileSync(binaryPath, ['--version'], { + encoding: 'utf8', + timeout: 5000, + stdio: ['ignore', 'pipe', 'pipe'], + env, + }).trim() + return { path: binaryPath, version: output || null } + } catch { + return { path: binaryPath, version: null } + } +} + +function checkProviderBinaries(): ProviderBinaryStatus[] { + return PROVIDER_BINARIES.map(({ label, binary }) => { + const result = checkProviderBinary(binary) + return { label, ...result } + }) +} + function resolveOwnerLabel(uid: number | undefined, hostname: string | undefined): string | null { if (uid === undefined && !hostname) { return null @@ -201,6 +254,8 @@ export async function runStatusCommand( note = appendNote(note, `serverId unavailable: ${shortenMessage(normalizeError(error))}`) } + const providers = checkProviderBinaries() + const daemonStatus: DaemonStatus = { serverId, status, @@ -216,6 +271,7 @@ export async function runStatusCommand( daemonNode, cliNode, cliVersion, + providers, note, } diff --git a/packages/server/src/server/agent/provider-launch-config.test.ts b/packages/server/src/server/agent/provider-launch-config.test.ts index e415b714f..0113f9294 100644 --- a/packages/server/src/server/agent/provider-launch-config.test.ts +++ b/packages/server/src/server/agent/provider-launch-config.test.ts @@ -92,6 +92,25 @@ describe("applyProviderEnv", () => { expect(env.FOO).toBe("bar"); expect(Object.keys(env).length).toBeGreaterThanOrEqual(3); }); + + test("shell env PATH wins over base env PATH", () => { + const base = { PATH: "/usr/bin:/bin" }; + const shellEnv = { PATH: "/usr/local/bin:/usr/bin:/bin:/home/user/.nvm/bin" }; + + const env = applyProviderEnv(base, undefined, shellEnv); + + expect(env.PATH).toBe("/usr/local/bin:/usr/bin:/bin:/home/user/.nvm/bin"); + }); + + test("runtimeSettings env wins over shell env", () => { + const base = { PATH: "/usr/bin" }; + const shellEnv = { PATH: "/usr/local/bin:/usr/bin" }; + const runtime: ProviderRuntimeSettings = { env: { PATH: "/custom/path" } }; + + const env = applyProviderEnv(base, runtime, shellEnv); + + expect(env.PATH).toBe("/custom/path"); + }); }); describe("findExecutable", () => { diff --git a/packages/server/src/server/agent/provider-launch-config.ts b/packages/server/src/server/agent/provider-launch-config.ts index b50f714e3..b3018bb6a 100644 --- a/packages/server/src/server/agent/provider-launch-config.ts +++ b/packages/server/src/server/agent/provider-launch-config.ts @@ -131,8 +131,8 @@ export function applyProviderEnv( shellEnv?: Record ): Record { return { - ...(shellEnv ?? resolveShellEnv()), ...baseEnv, + ...(shellEnv ?? resolveShellEnv()), ...(runtimeSettings?.env ?? {}), }; } diff --git a/packages/server/src/server/exports.ts b/packages/server/src/server/exports.ts index df7ea3411..9c4637073 100644 --- a/packages/server/src/server/exports.ts +++ b/packages/server/src/server/exports.ts @@ -24,6 +24,9 @@ export { type SherpaLoaderEnvResolution, } from "./speech/providers/local/sherpa/sherpa-runtime-env.js"; +// Provider binary resolution +export { findExecutable, applyProviderEnv } from "./agent/provider-launch-config.js"; + // Agent SDK types for CLI commands export type { AgentMode,