refactor(cli): remove daemon update command and simplify status runtime info

This commit is contained in:
Mohamed Boudra
2026-03-12 11:19:33 +07:00
parent 355e56db53
commit 9e6b45e2f0
6 changed files with 18 additions and 355 deletions
-10
View File
@@ -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 <path>', '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')
@@ -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
@@ -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(' ')}`
}
+17 -153
View File
@@ -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<StatusRow> {
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,
}
-105
View File
@@ -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<number> {
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<void> {
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<void> {
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 <path>', 'Paseo home directory (default: ~/.paseo)')
.option('-y, --yes', 'Restart automatically after update')
.action(async (options: DaemonUpdateOptions) => {
await runDaemonUpdateCommandOrExit(options)
})
}
+1
View File
@@ -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(