From d78c7d5e0770dcb245e9a4430c2518b8d2ee3ec3 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 28 Jan 2026 23:34:22 +0700 Subject: [PATCH] chore(cli): rename test files from .mts to .ts --- packages/cli/package.json | 2 +- packages/cli/src/commands/daemon/index.ts | 31 +++-- packages/cli/src/commands/daemon/restart.ts | 87 ++++++++++---- packages/cli/src/commands/daemon/status.ts | 112 +++++++++++++----- packages/cli/src/commands/daemon/stop.ts | 90 ++++++++++---- packages/cli/src/output/json.ts | 10 ++ packages/cli/src/output/table.ts | 4 +- packages/cli/src/output/with-output.ts | 4 +- packages/cli/src/output/yaml.ts | 10 ++ ...ndation.test.mts => 01-foundation.test.ts} | 0 .../{output.test.mts => 02-output.test.ts} | 0 .../{03-daemon.test.mts => 03-daemon.test.ts} | 0 ...-agent-ps.test.mts => 04-agent-ps.test.ts} | 0 ...gent-run.test.mts => 05-agent-run.test.ts} | 0 ...nt-send.test.mts => 06-agent-send.test.ts} | 0 ...nt-stop.test.mts => 07-agent-stop.test.ts} | 0 .../cli/tests/{run-all.mts => run-all.ts} | 8 +- 17 files changed, 264 insertions(+), 94 deletions(-) rename packages/cli/tests/{01-foundation.test.mts => 01-foundation.test.ts} (100%) rename packages/cli/tests/{output.test.mts => 02-output.test.ts} (100%) rename packages/cli/tests/{03-daemon.test.mts => 03-daemon.test.ts} (100%) rename packages/cli/tests/{04-agent-ps.test.mts => 04-agent-ps.test.ts} (100%) rename packages/cli/tests/{05-agent-run.test.mts => 05-agent-run.test.ts} (100%) rename packages/cli/tests/{06-agent-send.test.mts => 06-agent-send.test.ts} (100%) rename packages/cli/tests/{07-agent-stop.test.mts => 07-agent-stop.test.ts} (100%) rename packages/cli/tests/{run-all.mts => run-all.ts} (90%) diff --git a/packages/cli/package.json b/packages/cli/package.json index c70511ba8..93781915c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -8,7 +8,7 @@ }, "scripts": { "typecheck": "tsc --noEmit", - "test:e2e": "npx zx tests/run-all.mts" + "test:e2e": "npx zx tests/run-all.ts" }, "dependencies": { "@paseo/server": "*", diff --git a/packages/cli/src/commands/daemon/index.ts b/packages/cli/src/commands/daemon/index.ts index 817fe652c..28a2511e4 100644 --- a/packages/cli/src/commands/daemon/index.ts +++ b/packages/cli/src/commands/daemon/index.ts @@ -1,17 +1,32 @@ import { Command } from 'commander' import { startCommand } from './start.js' -import { statusCommand } from './status.js' -import { stopCommand } from './stop.js' -import { restartCommand } from './restart.js' +import { runStatusCommand } from './status.js' +import { runStopCommand } from './stop.js' +import { runRestartCommand } from './restart.js' +import { withOutput } from '../../output/index.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(statusCommand()) - daemon.addCommand(stopCommand()) - daemon.addCommand(restartCommand()) + + daemon + .command('status') + .description('Show daemon status') + .option('--host ', 'Daemon host:port (default: localhost:6767)') + .action(withOutput(runStatusCommand)) + + daemon + .command('stop') + .description('Stop the daemon') + .option('--host ', 'Daemon host:port (default: localhost:6767)') + .action(withOutput(runStopCommand)) + + daemon + .command('restart') + .description('Restart the daemon') + .option('--host ', 'Daemon host:port (default: localhost:6767)') + .action(withOutput(runRestartCommand)) return daemon } diff --git a/packages/cli/src/commands/daemon/restart.ts b/packages/cli/src/commands/daemon/restart.ts index 090ba28bc..de95b4bb8 100644 --- a/packages/cli/src/commands/daemon/restart.ts +++ b/packages/cli/src/commands/daemon/restart.ts @@ -1,51 +1,86 @@ -import { Command } from 'commander' -import chalk from 'chalk' +import type { Command } from 'commander' import { connectToDaemon, getDaemonHost } from '../../utils/client.js' +import type { CommandOptions, SingleResult, OutputSchema, CommandError } from '../../output/index.js' -export function restartCommand(): Command { - return new Command('restart') - .description('Restart the daemon') - .option('--host ', 'Daemon host:port (default: localhost:6767)') - .action(async (options: { host?: string }) => { - await runRestart(options) - }) +/** Result of restart command */ +interface RestartResult { + action: 'restarted' | 'not_running' + host: string + message: string } -async function runRestart(options: { host?: string }): Promise { - const host = getDaemonHost(options) +/** Schema for restart result */ +const restartResultSchema: OutputSchema = { + idField: 'action', + columns: [ + { + header: 'STATUS', + field: 'action', + color: (value) => (value === 'restarted' ? 'green' : 'red'), + }, + { header: 'HOST', field: 'host' }, + { header: 'MESSAGE', field: 'message' }, + ], +} + +export type RestartCommandResult = SingleResult + +export async function runRestartCommand( + options: CommandOptions, + _command: Command +): Promise { + const connectOptions = { host: options.host as string | undefined } + const host = getDaemonHost(connectOptions) let client try { - client = await connectToDaemon(options) + client = await connectToDaemon(connectOptions) } catch { - console.log(chalk.yellow('Daemon is not running')) - console.log(chalk.dim(`Tried to connect to ${host}`)) - console.log() - console.log(chalk.dim('Start the daemon with:')) - console.log(chalk.dim(' paseo daemon start')) - process.exit(1) + // Daemon not running - cannot restart + const error: CommandError = { + code: 'DAEMON_NOT_RUNNING', + message: `Daemon is not running (tried to connect to ${host})`, + details: 'Start the daemon with: paseo daemon start', + } + throw error } try { - console.log(chalk.dim('Restarting daemon...')) - // Request server restart await client.restartServer('cli_restart') - console.log(chalk.green('Daemon restart requested')) - await client.close() + + return { + type: 'single', + data: { + action: 'restarted', + host, + message: 'Daemon restart requested', + }, + schema: restartResultSchema, + } } catch (err) { await client.close().catch(() => {}) const message = err instanceof Error ? err.message : String(err) // If connection was closed, the daemon is restarting if (message.includes('closed') || message.includes('disconnected')) { - console.log(chalk.green('Daemon is restarting')) - process.exit(0) + return { + type: 'single', + data: { + action: 'restarted', + host, + message: 'Daemon is restarting', + }, + schema: restartResultSchema, + } } - console.error(chalk.red(`Failed to restart daemon: ${message}`)) - process.exit(1) + const error: CommandError = { + code: 'RESTART_FAILED', + message: `Failed to restart daemon: ${message}`, + } + throw error } } diff --git a/packages/cli/src/commands/daemon/status.ts b/packages/cli/src/commands/daemon/status.ts index 4f435a25d..a67801f62 100644 --- a/packages/cli/src/commands/daemon/status.ts +++ b/packages/cli/src/commands/daemon/status.ts @@ -1,28 +1,74 @@ -import { Command } from 'commander' -import chalk from 'chalk' +import type { Command } from 'commander' import { resolvePaseoHome } from '@paseo/server' import { tryConnectToDaemon, getDaemonHost } from '../../utils/client.js' +import type { CommandOptions, ListResult, OutputSchema, CommandError } from '../../output/index.js' -export function statusCommand(): Command { - return new Command('status') - .description('Show daemon status') - .option('--host ', 'Daemon host:port (default: localhost:6767)') - .action(async (options: { host?: string }) => { - await runStatus(options) - }) +/** Status data for the daemon */ +interface DaemonStatus { + status: 'running' | 'stopped' + host: string + home: string + runningAgents: number + idleAgents: number } -async function runStatus(options: { host?: string }): Promise { - const host = getDaemonHost(options) - const client = await tryConnectToDaemon(options) +/** Key-value row for table display */ +interface StatusRow { + key: string + value: string +} + +/** Schema for key-value display with custom serialization for JSON/YAML */ +function createStatusSchema(status: DaemonStatus): OutputSchema { + return { + idField: 'key', + columns: [ + { header: 'KEY', field: 'key' }, + { + header: 'VALUE', + field: 'value', + color: (_, item) => { + if (item.key === 'Status') { + return item.value === 'running' ? 'green' : 'red' + } + return undefined + }, + }, + ], + // For JSON/YAML, return the structured status object (not key-value rows) + // The serializer receives each item, but we want the whole object + // So we return null for individual items and handle it at the result level + serialize: (_item) => status, + } +} + +/** Convert status to key-value rows for table display */ +function toStatusRows(status: DaemonStatus): StatusRow[] { + return [ + { key: 'Status', value: status.status }, + { key: 'Host', value: status.host }, + { key: 'Home', value: status.home }, + { key: 'Agents', value: `${status.runningAgents} running, ${status.idleAgents} idle` }, + ] +} + +export type StatusResult = ListResult + +export async function runStatusCommand( + options: CommandOptions, + _command: Command +): Promise { + const connectOptions = { host: options.host as string | undefined } + const host = getDaemonHost(connectOptions) + const client = await tryConnectToDaemon(connectOptions) if (!client) { - console.log(chalk.red('Status:'), 'not running') - console.log(chalk.dim(`Tried to connect to ${host}`)) - console.log() - console.log(chalk.dim('Start the daemon with:')) - console.log(chalk.dim(' paseo daemon start')) - process.exit(1) + const error: CommandError = { + code: 'DAEMON_NOT_RUNNING', + message: `Daemon is not running (tried to connect to ${host})`, + details: 'Start the daemon with: paseo daemon start', + } + throw error } try { @@ -30,25 +76,37 @@ async function runStatus(options: { host?: string }): Promise { client.requestSessionState() // Wait a moment for the session state to be populated - await new Promise(resolve => setTimeout(resolve, 500)) + await new Promise((resolve) => setTimeout(resolve, 500)) const agents = client.listAgents() - const runningAgents = agents.filter(a => a.status === 'running') - const idleAgents = agents.filter(a => a.status === 'idle') + const runningAgents = agents.filter((a) => a.status === 'running') + const idleAgents = agents.filter((a) => a.status === 'idle') // Get paseo home for display const paseoHome = resolvePaseoHome() - console.log(chalk.green('Status:'), 'running') - console.log(chalk.dim('Host:'), host) - console.log(chalk.dim('Home:'), paseoHome) - console.log(chalk.dim('Agents:'), `${runningAgents.length} running, ${idleAgents.length} idle`) + const status: DaemonStatus = { + status: 'running', + host, + home: paseoHome, + runningAgents: runningAgents.length, + idleAgents: idleAgents.length, + } await client.close() + + return { + type: 'list', + data: toStatusRows(status), + schema: createStatusSchema(status), + } } catch (err) { await client.close().catch(() => {}) const message = err instanceof Error ? err.message : String(err) - console.error(chalk.red(`Failed to get status: ${message}`)) - process.exit(1) + const error: CommandError = { + code: 'STATUS_FAILED', + message: `Failed to get status: ${message}`, + } + throw error } } diff --git a/packages/cli/src/commands/daemon/stop.ts b/packages/cli/src/commands/daemon/stop.ts index 00f347be3..eaaf08808 100644 --- a/packages/cli/src/commands/daemon/stop.ts +++ b/packages/cli/src/commands/daemon/stop.ts @@ -1,53 +1,93 @@ -import { Command } from 'commander' -import chalk from 'chalk' +import type { Command } from 'commander' import { connectToDaemon, getDaemonHost } from '../../utils/client.js' +import type { CommandOptions, SingleResult, OutputSchema, CommandError } from '../../output/index.js' -export function stopCommand(): Command { - return new Command('stop') - .description('Stop the daemon') - .option('--host ', 'Daemon host:port (default: localhost:6767)') - .action(async (options: { host?: string }) => { - await runStop(options) - }) +/** Result of stop command */ +interface StopResult { + action: 'stopped' | 'not_running' + host: string + message: string } -async function runStop(options: { host?: string }): Promise { - const host = getDaemonHost(options) +/** Schema for stop result */ +const stopResultSchema: OutputSchema = { + idField: 'action', + columns: [ + { + header: 'STATUS', + field: 'action', + color: (value) => (value === 'stopped' ? 'green' : 'yellow'), + }, + { header: 'HOST', field: 'host' }, + { header: 'MESSAGE', field: 'message' }, + ], +} + +export type StopCommandResult = SingleResult + +export async function runStopCommand( + options: CommandOptions, + _command: Command +): Promise { + const connectOptions = { host: options.host as string | undefined } + const host = getDaemonHost(connectOptions) let client try { - client = await connectToDaemon(options) + client = await connectToDaemon(connectOptions) } catch { - console.log(chalk.yellow('Daemon is not running')) - console.log(chalk.dim(`Tried to connect to ${host}`)) - process.exit(0) + // Daemon not running - this is a valid outcome + return { + type: 'single', + data: { + action: 'not_running', + host, + message: 'Daemon was not running', + }, + schema: stopResultSchema, + } } try { - console.log(chalk.dim('Stopping daemon...')) - // Request server restart with "shutdown" reason // This signals the daemon to shut down gracefully await client.restartServer('cli_shutdown') // Give the daemon a moment to acknowledge - await new Promise(resolve => setTimeout(resolve, 500)) - - console.log(chalk.green('Daemon stop requested')) - console.log(chalk.dim('The daemon will shut down gracefully')) + await new Promise((resolve) => setTimeout(resolve, 500)) await client.close() + + return { + type: 'single', + data: { + action: 'stopped', + host, + message: 'Daemon stop requested - shutting down gracefully', + }, + schema: stopResultSchema, + } } catch (err) { await client.close().catch(() => {}) const message = err instanceof Error ? err.message : String(err) // If connection was closed, the daemon is stopping if (message.includes('closed') || message.includes('disconnected')) { - console.log(chalk.green('Daemon is stopping')) - process.exit(0) + return { + type: 'single', + data: { + action: 'stopped', + host, + message: 'Daemon is stopping', + }, + schema: stopResultSchema, + } } - console.error(chalk.red(`Failed to stop daemon: ${message}`)) - process.exit(1) + const error: CommandError = { + code: 'STOP_FAILED', + message: `Failed to stop daemon: ${message}`, + } + throw error } } diff --git a/packages/cli/src/output/json.ts b/packages/cli/src/output/json.ts index b4d58e122..8684e0103 100644 --- a/packages/cli/src/output/json.ts +++ b/packages/cli/src/output/json.ts @@ -16,7 +16,17 @@ export function renderJson( // Apply custom serializer if provided if (schema.serialize) { if (result.type === 'list') { + // If all items serialize to the same object, return just one + // This handles the case where a list of key-value rows should serialize + // to a single structured object const serialized = result.data.map((item) => schema.serialize!(item)) + if (serialized.length > 0) { + const first = JSON.stringify(serialized[0]) + const allSame = serialized.every((s) => JSON.stringify(s) === first) + if (allSame) { + return JSON.stringify(serialized[0], null, 2) + } + } return JSON.stringify(serialized, null, 2) } else { const serialized = schema.serialize(result.data) diff --git a/packages/cli/src/output/table.ts b/packages/cli/src/output/table.ts index a22f96796..495aee278 100644 --- a/packages/cli/src/output/table.ts +++ b/packages/cli/src/output/table.ts @@ -97,7 +97,7 @@ function renderRow( } } - return padCell(cell, width, col.align ?? 'left') + return padCell(cell, width ?? 0, col.align ?? 'left') }) .join(' ') } @@ -109,7 +109,7 @@ function renderHeader( options: OutputOptions ): string { const headerRow = columns - .map((col, i) => padCell(col.header, widths[i], col.align ?? 'left')) + .map((col, i) => padCell(col.header, widths[i] ?? 0, col.align ?? 'left')) .join(' ') return options.noColor ? headerRow : chalk.bold(headerRow) diff --git a/packages/cli/src/output/with-output.ts b/packages/cli/src/output/with-output.ts index e71bf3e97..509e0bb29 100644 --- a/packages/cli/src/output/with-output.ts +++ b/packages/cli/src/output/with-output.ts @@ -47,7 +47,9 @@ export function withOutput( ): (...args: [...Args, CommandOptions, Command]) => Promise { return async (...args) => { // Last two args are options and command - const options = args[args.length - 2] as CommandOptions + const command = args[args.length - 1] as Command + // Use optsWithGlobals() to get both local and global options + const options = command.optsWithGlobals() as CommandOptions const outputOptions = extractOutputOptions(options) try { diff --git a/packages/cli/src/output/yaml.ts b/packages/cli/src/output/yaml.ts index d554caef0..81e58f62a 100644 --- a/packages/cli/src/output/yaml.ts +++ b/packages/cli/src/output/yaml.ts @@ -17,7 +17,17 @@ export function renderYaml( // Apply custom serializer if provided if (schema.serialize) { if (result.type === 'list') { + // If all items serialize to the same object, return just one + // This handles the case where a list of key-value rows should serialize + // to a single structured object const serialized = result.data.map((item) => schema.serialize!(item)) + if (serialized.length > 0) { + const first = JSON.stringify(serialized[0]) + const allSame = serialized.every((s) => JSON.stringify(s) === first) + if (allSame) { + return YAML.stringify(serialized[0]) + } + } return YAML.stringify(serialized) } else { const serialized = schema.serialize(result.data) diff --git a/packages/cli/tests/01-foundation.test.mts b/packages/cli/tests/01-foundation.test.ts similarity index 100% rename from packages/cli/tests/01-foundation.test.mts rename to packages/cli/tests/01-foundation.test.ts diff --git a/packages/cli/tests/output.test.mts b/packages/cli/tests/02-output.test.ts similarity index 100% rename from packages/cli/tests/output.test.mts rename to packages/cli/tests/02-output.test.ts diff --git a/packages/cli/tests/03-daemon.test.mts b/packages/cli/tests/03-daemon.test.ts similarity index 100% rename from packages/cli/tests/03-daemon.test.mts rename to packages/cli/tests/03-daemon.test.ts diff --git a/packages/cli/tests/04-agent-ps.test.mts b/packages/cli/tests/04-agent-ps.test.ts similarity index 100% rename from packages/cli/tests/04-agent-ps.test.mts rename to packages/cli/tests/04-agent-ps.test.ts diff --git a/packages/cli/tests/05-agent-run.test.mts b/packages/cli/tests/05-agent-run.test.ts similarity index 100% rename from packages/cli/tests/05-agent-run.test.mts rename to packages/cli/tests/05-agent-run.test.ts diff --git a/packages/cli/tests/06-agent-send.test.mts b/packages/cli/tests/06-agent-send.test.ts similarity index 100% rename from packages/cli/tests/06-agent-send.test.mts rename to packages/cli/tests/06-agent-send.test.ts diff --git a/packages/cli/tests/07-agent-stop.test.mts b/packages/cli/tests/07-agent-stop.test.ts similarity index 100% rename from packages/cli/tests/07-agent-stop.test.mts rename to packages/cli/tests/07-agent-stop.test.ts diff --git a/packages/cli/tests/run-all.mts b/packages/cli/tests/run-all.ts similarity index 90% rename from packages/cli/tests/run-all.mts rename to packages/cli/tests/run-all.ts index 9713e65a7..e7427f9e3 100644 --- a/packages/cli/tests/run-all.mts +++ b/packages/cli/tests/run-all.ts @@ -4,7 +4,7 @@ * Test runner for Paseo CLI E2E tests * * Runs all test phases in sequence and reports results. - * Each test is a separate .mts file that can also be run independently. + * Each test is a separate .ts file that can also be run independently. */ import { $ } from 'zx' @@ -22,7 +22,7 @@ console.log('='.repeat(50)) // Discover all test files const files = await readdir(__dirname) const testFiles = files - .filter(f => f.match(/^\d{2}-.*\.test\.mts$/)) + .filter(f => f.match(/^\d{2}-.*\.test\.ts$/)) .sort() if (testFiles.length === 0) { @@ -42,14 +42,14 @@ const failures: { test: string; error: string }[] = [] for (const testFile of testFiles) { const testPath = join(__dirname, testFile) - const testName = testFile.replace(/\.test\.mts$/, '') + const testName = testFile.replace(/\.test\.ts$/, '') console.log(`\n${'─'.repeat(50)}`) console.log(`šŸ“‹ Running ${testName}...`) console.log('─'.repeat(50)) try { - const result = await $`npx zx ${testPath}`.nothrow() + const result = await $`npx tsx ${testPath}`.nothrow() if (result.exitCode === 0) { console.log(`\nāœ… ${testName} PASSED`) passed++