diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index d89703b3c..03e5a90d7 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -9,7 +9,6 @@ import { createWorktreeCommand } from './commands/worktree/index.js' import { startCommand as daemonStartCommand } from './commands/daemon/start.js' import { runStatusCommand as runDaemonStatusCommand } from './commands/daemon/status.js' import { runRestartCommand as runDaemonRestartCommand } from './commands/daemon/restart.js' -import { runDaemonUpdateCommandOrExit } from './commands/daemon/update.js' import { runLsCommand } from './commands/agent/ls.js' import { runRunCommand } from './commands/agent/run.js' import { runLogsCommand } from './commands/agent/logs.js' @@ -158,15 +157,6 @@ export function createCli(): Command { program.addCommand(onboardCommand()) program.addCommand(daemonStartCommand()) - program - .command('update') - .description('Update local daemon package (alias for "paseo daemon update")') - .option('--home ', 'Paseo home directory (default: ~/.paseo)') - .option('-y, --yes', 'Restart automatically after update') - .action(async (options: { home?: string; yes?: boolean }) => { - await runDaemonUpdateCommandOrExit(options) - }) - addJsonOption( program .command('status') diff --git a/packages/cli/src/commands/daemon/index.ts b/packages/cli/src/commands/daemon/index.ts index 2e66369af..0aa09338b 100644 --- a/packages/cli/src/commands/daemon/index.ts +++ b/packages/cli/src/commands/daemon/index.ts @@ -4,7 +4,6 @@ import { runStatusCommand } from './status.js' import { runStopCommand } from './stop.js' import { runRestartCommand } from './restart.js' import { pairCommand } from './pair.js' -import { updateCommand } from './update.js' import { withOutput } from '../../output/index.js' import { addJsonOption } from '../../utils/command-options.js' @@ -13,7 +12,6 @@ export function createDaemonCommand(): Command { daemon.addCommand(startCommand()) daemon.addCommand(pairCommand()) - daemon.addCommand(updateCommand()) addJsonOption( daemon diff --git a/packages/cli/src/commands/daemon/runtime-toolchain.ts b/packages/cli/src/commands/daemon/runtime-toolchain.ts index e4406b7f4..0d64733b3 100644 --- a/packages/cli/src/commands/daemon/runtime-toolchain.ts +++ b/packages/cli/src/commands/daemon/runtime-toolchain.ts @@ -1,27 +1,10 @@ import { spawnSync } from 'node:child_process' -import { existsSync } from 'node:fs' -import path from 'node:path' - -export type NodePathSource = 'daemon_pid' | 'current_process' - -export interface NodePathResolution { - nodePath: string - source: NodePathSource - note?: string -} export interface NodePathFromPidResult { nodePath: string | null error?: string } -export interface NpmInvocation { - nodePath: string - npmPath: string - command: string - argsPrefix: string[] -} - function normalizeError(error: unknown): string { if (error instanceof Error) { return error.message @@ -61,71 +44,3 @@ export function resolveNodePathFromPid(pid: number): NodePathFromPidResult { return { nodePath: resolved } } -export function resolvePreferredNodePath(args: { - daemonPid?: number | null - fallbackNodePath?: string -}): NodePathResolution { - const fallback = args.fallbackNodePath ?? process.execPath - const daemonPid = args.daemonPid - - if (typeof daemonPid === 'number' && Number.isInteger(daemonPid) && daemonPid > 0) { - const fromPid = resolveNodePathFromPid(daemonPid) - if (fromPid.nodePath) { - return { - nodePath: fromPid.nodePath, - source: 'daemon_pid', - } - } - - return { - nodePath: fallback, - source: 'current_process', - note: `Could not resolve node from daemon PID ${daemonPid}; using current process node (${fromPid.error ?? 'unknown error'})`, - } - } - - return { - nodePath: fallback, - source: 'current_process', - } -} - -export function resolveNpmInvocationFromNode(nodePath: string): NpmInvocation { - const binDir = path.dirname(nodePath) - const prefix = path.dirname(binDir) - const npmBinary = path.join(binDir, process.platform === 'win32' ? 'npm.cmd' : 'npm') - - if (existsSync(npmBinary)) { - return { - nodePath, - npmPath: npmBinary, - command: npmBinary, - argsPrefix: [], - } - } - - const npmCliCandidates = [ - path.join(prefix, 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'), - path.join(prefix, 'node_modules', 'npm', 'bin', 'npm-cli.js'), - ] - - for (const candidate of npmCliCandidates) { - if (existsSync(candidate)) { - return { - nodePath, - npmPath: candidate, - command: nodePath, - argsPrefix: [candidate], - } - } - } - - throw new Error(`Unable to resolve npm for node executable: ${nodePath}`) -} - -export function formatNpmInvocation(invocation: NpmInvocation): string { - if (invocation.argsPrefix.length === 0) { - return invocation.command - } - return `${invocation.command} ${invocation.argsPrefix.join(' ')}` -} diff --git a/packages/cli/src/commands/daemon/status.ts b/packages/cli/src/commands/daemon/status.ts index 1b27cf85f..87edfe4b7 100644 --- a/packages/cli/src/commands/daemon/status.ts +++ b/packages/cli/src/commands/daemon/status.ts @@ -1,16 +1,10 @@ import type { Command } from 'commander' -import { spawnSync } from 'node:child_process' import { createRequire } from 'node:module' import { getOrCreateServerId } 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 { - formatNpmInvocation, - resolveNpmInvocationFromNode, - resolvePreferredNodePath, - type NpmInvocation, -} from './runtime-toolchain.js' +import { resolveNodePathFromPid } from './runtime-toolchain.js' interface DaemonStatus { serverId: string | null @@ -24,11 +18,9 @@ interface DaemonStatus { logPath: string runningAgents: number | null idleAgents: number | null - runtimeNode: string - runtimeNpm: string + daemonNode: string + cliNode: string cliVersion: string - latestCliVersion: string - updateStatus: string note?: string } @@ -41,13 +33,7 @@ type CliPackageJson = { version?: unknown } -type LatestCliVersionResult = { - version: string | null - note?: string -} - const require = createRequire(import.meta.url) -const CLI_UPDATE_CHECK_TIMEOUT_MS = 3000 function normalizeError(error: unknown): string { if (error instanceof Error) { @@ -82,95 +68,6 @@ function resolveCliVersion(): string { return 'unknown' } -function parseVersionFromNpmOutput(raw: string): string | null { - const trimmed = raw.trim() - if (!trimmed) { - return null - } - - try { - const parsed = JSON.parse(trimmed) as unknown - if (typeof parsed === 'string' && parsed.trim().length > 0) { - return parsed.trim() - } - } catch { - // Fall back to plain output. - } - - return trimmed.replace(/^"+|"+$/g, '').trim() || null -} - -function parseSemver(version: string): [number, number, number] | null { - const match = version.trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/) - if (!match) { - return null - } - - const major = Number(match[1]) - const minor = Number(match[2]) - const patch = Number(match[3]) - if (![major, minor, patch].every((part) => Number.isInteger(part) && part >= 0)) { - return null - } - - return [major, minor, patch] -} - -function compareSemver(left: string, right: string): number | null { - const leftParts = parseSemver(left) - const rightParts = parseSemver(right) - if (!leftParts || !rightParts) { - return null - } - - for (let index = 0; index < leftParts.length; index += 1) { - if (leftParts[index]! < rightParts[index]!) return -1 - if (leftParts[index]! > rightParts[index]!) return 1 - } - - return 0 -} - -function fetchLatestCliVersion(npm: NpmInvocation): LatestCliVersionResult { - const result = spawnSync( - npm.command, - [...npm.argsPrefix, 'view', '@getpaseo/cli', 'version', '--json'], - { - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'pipe'], - env: process.env, - timeout: CLI_UPDATE_CHECK_TIMEOUT_MS, - } - ) - - if (result.error) { - return { - version: null, - note: `update check failed: ${shortenMessage(normalizeError(result.error))}`, - } - } - - if ((result.status ?? 1) !== 0) { - const stderr = result.stderr?.trim() - return { - version: null, - note: stderr - ? `update check failed: ${shortenMessage(stderr)}` - : `update check failed: npm exited with code ${result.status ?? 1}`, - } - } - - const version = parseVersionFromNpmOutput(result.stdout) - if (!version) { - return { - version: null, - note: 'update check failed: empty npm response', - } - } - - return { version } -} - function createStatusSchema(status: DaemonStatus): OutputSchema { return { idField: 'key', @@ -208,11 +105,9 @@ function toStatusRows(status: DaemonStatus): StatusRow[] { { key: 'Started', value: status.startedAt ?? '-' }, { key: 'Owner', value: status.owner ?? '-' }, { key: 'Logs', value: status.logPath }, - { key: 'Node', value: status.runtimeNode }, - { key: 'npm', value: status.runtimeNpm }, + { key: 'Daemon Node', value: status.daemonNode }, + { key: 'CLI Node', value: status.cliNode }, { key: 'CLI', value: status.cliVersion }, - { key: 'Latest CLI', value: status.latestCliVersion }, - { key: 'Update', value: status.updateStatus }, ] if (status.runningAgents !== null && status.idleAgents !== null) { @@ -253,22 +148,24 @@ export async function runStatusCommand( const state = resolveLocalDaemonState({ home }) const owner = resolveOwnerLabel(state.pidInfo?.uid, state.pidInfo?.hostname) - const resolvedNode = resolvePreferredNodePath({ - daemonPid: state.running ? state.pidInfo?.pid : null, - fallbackNodePath: process.execPath, - }) + let daemonNode: string + if (!state.running) { + daemonNode = '-' + } else if (state.pidInfo?.pid) { + const fromPid = 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 status: DaemonStatus['status'] = state.running ? 'running' : 'stopped' let runningAgents: number | null = null let idleAgents: number | null = null let note: string | undefined - let runtimeNpm = '-' - let latestCliVersion = 'unknown' - let updateStatus = 'unknown' if (!state.running && state.stalePidFile && state.pidInfo) { note = `Stale PID file found for PID ${state.pidInfo.pid}` } - note = appendNote(note, resolvedNode.note) if (state.running) { const host = resolveTcpHostFromListen(state.listen) @@ -295,38 +192,7 @@ export async function runStatusCommand( } } - let npmInvocation: NpmInvocation | null = null - try { - npmInvocation = resolveNpmInvocationFromNode(resolvedNode.nodePath) - runtimeNpm = formatNpmInvocation(npmInvocation) - } catch (err) { - runtimeNpm = `unresolved (${shortenMessage(normalizeError(err))})` - } - const cliVersion = resolveCliVersion() - if (npmInvocation) { - const latest = fetchLatestCliVersion(npmInvocation) - if (latest.version) { - latestCliVersion = latest.version - if (cliVersion === 'unknown') { - updateStatus = 'unknown (local CLI version unavailable)' - } else { - const comparison = compareSemver(cliVersion, latest.version) - if (comparison === null) { - updateStatus = 'unknown (version format not comparable)' - } else if (comparison < 0) { - updateStatus = `update available (${cliVersion} -> ${latest.version})` - } else { - updateStatus = 'up to date' - } - } - } else { - latestCliVersion = 'unknown' - updateStatus = latest.note ?? 'unknown' - } - } else { - updateStatus = 'unknown (npm unresolved)' - } let serverId: string | null = null try { @@ -347,11 +213,9 @@ export async function runStatusCommand( logPath: state.logPath, runningAgents, idleAgents, - runtimeNode: `${resolvedNode.nodePath} (${resolvedNode.source})`, - runtimeNpm, + daemonNode, + cliNode, cliVersion, - latestCliVersion, - updateStatus, note, } diff --git a/packages/cli/src/commands/daemon/update.ts b/packages/cli/src/commands/daemon/update.ts deleted file mode 100644 index 5fc46d768..000000000 --- a/packages/cli/src/commands/daemon/update.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { confirm, isCancel } from '@clack/prompts' -import { spawn } from 'node:child_process' -import { Command } from 'commander' -import chalk from 'chalk' -import { runRestartCommand } from './restart.js' -import { resolveLocalDaemonState } from './local-daemon.js' -import { - resolveNpmInvocationFromNode, - resolvePreferredNodePath, - type NpmInvocation, -} from './runtime-toolchain.js' -import { getErrorMessage } from '../../utils/errors.js' - -export interface DaemonUpdateOptions { - home?: string - yes?: boolean -} - -function runCommand(command: string, args: string[]): Promise { - return new Promise((resolve, reject) => { - const child = spawn(command, args, { - stdio: 'inherit', - env: process.env, - }) - - child.once('error', reject) - child.once('exit', (code, signal) => { - if (signal) { - reject(new Error(`Command exited via signal ${signal}`)) - return - } - resolve(code ?? 1) - }) - }) -} - -export async function runDaemonUpdateCommand(options: DaemonUpdateOptions): Promise { - const daemonState = resolveLocalDaemonState({ home: options.home }) - const resolvedNode = resolvePreferredNodePath({ - daemonPid: daemonState.running ? daemonState.pidInfo?.pid : null, - fallbackNodePath: process.execPath, - }) - const npm: NpmInvocation = resolveNpmInvocationFromNode(resolvedNode.nodePath) - const args = [...npm.argsPrefix, 'install', '-g', '@getpaseo/cli@latest'] - - const exitCode = await runCommand(npm.command, args) - if (exitCode !== 0) { - throw new Error(`Update command failed with exit code ${exitCode}`) - } - - let shouldRestart = options.yes === true - if (!shouldRestart) { - if (!process.stdin.isTTY || !process.stdout.isTTY) { - console.log(chalk.yellow('Update complete. Restart skipped (non-interactive terminal).')) - console.log(chalk.dim('Run `paseo daemon restart` when ready.')) - return - } - - console.log( - chalk.yellow( - 'Restart will stop running agents, but they can continue from persisted state after restart.' - ) - ) - const answer = await confirm({ - message: 'Restart daemon now?', - active: 'Restart now', - inactive: 'Later', - initialValue: true, - }) - - if (isCancel(answer)) { - console.log(chalk.yellow('Update complete. Restart skipped.')) - return - } - - shouldRestart = answer - } - - if (!shouldRestart) { - console.log(chalk.yellow('Update complete. Restart skipped.')) - return - } - - const restartResult = await runRestartCommand({ home: options.home }, new Command()) - console.log(chalk.green(restartResult.data.message)) -} - -export async function runDaemonUpdateCommandOrExit(options: DaemonUpdateOptions): Promise { - try { - await runDaemonUpdateCommand(options) - } catch (err) { - console.error(chalk.red(`Failed to update daemon: ${getErrorMessage(err)}`)) - process.exit(1) - } -} - -export function updateCommand(): Command { - return new Command('update') - .description('Update local daemon package') - .option('--home ', 'Paseo home directory (default: ~/.paseo)') - .option('-y, --yes', 'Restart automatically after update') - .action(async (options: DaemonUpdateOptions) => { - await runDaemonUpdateCommandOrExit(options) - }) -} diff --git a/packages/server/src/server/logger.ts b/packages/server/src/server/logger.ts index 46da3d0d3..0035ac23b 100644 --- a/packages/server/src/server/logger.ts +++ b/packages/server/src/server/logger.ts @@ -234,6 +234,7 @@ export function createRootLogger( path: path.dirname(config.file.path), size: toRotatingFileStreamSize(config.file.rotate.maxSize), maxFiles: config.file.rotate.maxFiles, + initialRotation: true, }); return pino(