chore(cli): rename test files from .mts to .ts

This commit is contained in:
Mohamed Boudra
2026-01-28 23:34:22 +07:00
parent d51d70a6fd
commit d78c7d5e07
17 changed files with 264 additions and 94 deletions
+1 -1
View File
@@ -8,7 +8,7 @@
}, },
"scripts": { "scripts": {
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"test:e2e": "npx zx tests/run-all.mts" "test:e2e": "npx zx tests/run-all.ts"
}, },
"dependencies": { "dependencies": {
"@paseo/server": "*", "@paseo/server": "*",
+23 -8
View File
@@ -1,17 +1,32 @@
import { Command } from 'commander' import { Command } from 'commander'
import { startCommand } from './start.js' import { startCommand } from './start.js'
import { statusCommand } from './status.js' import { runStatusCommand } from './status.js'
import { stopCommand } from './stop.js' import { runStopCommand } from './stop.js'
import { restartCommand } from './restart.js' import { runRestartCommand } from './restart.js'
import { withOutput } from '../../output/index.js'
export function createDaemonCommand(): Command { export function createDaemonCommand(): Command {
const daemon = new Command('daemon') const daemon = new Command('daemon').description('Manage the Paseo daemon')
.description('Manage the Paseo daemon')
daemon.addCommand(startCommand()) daemon.addCommand(startCommand())
daemon.addCommand(statusCommand())
daemon.addCommand(stopCommand()) daemon
daemon.addCommand(restartCommand()) .command('status')
.description('Show daemon status')
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
.action(withOutput(runStatusCommand))
daemon
.command('stop')
.description('Stop the daemon')
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
.action(withOutput(runStopCommand))
daemon
.command('restart')
.description('Restart the daemon')
.option('--host <host>', 'Daemon host:port (default: localhost:6767)')
.action(withOutput(runRestartCommand))
return daemon return daemon
} }
+61 -26
View File
@@ -1,51 +1,86 @@
import { Command } from 'commander' import type { Command } from 'commander'
import chalk from 'chalk'
import { connectToDaemon, getDaemonHost } from '../../utils/client.js' import { connectToDaemon, getDaemonHost } from '../../utils/client.js'
import type { CommandOptions, SingleResult, OutputSchema, CommandError } from '../../output/index.js'
export function restartCommand(): Command { /** Result of restart command */
return new Command('restart') interface RestartResult {
.description('Restart the daemon') action: 'restarted' | 'not_running'
.option('--host <host>', 'Daemon host:port (default: localhost:6767)') host: string
.action(async (options: { host?: string }) => { message: string
await runRestart(options)
})
} }
async function runRestart(options: { host?: string }): Promise<void> { /** Schema for restart result */
const host = getDaemonHost(options) const restartResultSchema: OutputSchema<RestartResult> = {
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<RestartResult>
export async function runRestartCommand(
options: CommandOptions,
_command: Command
): Promise<RestartCommandResult> {
const connectOptions = { host: options.host as string | undefined }
const host = getDaemonHost(connectOptions)
let client let client
try { try {
client = await connectToDaemon(options) client = await connectToDaemon(connectOptions)
} catch { } catch {
console.log(chalk.yellow('Daemon is not running')) // Daemon not running - cannot restart
console.log(chalk.dim(`Tried to connect to ${host}`)) const error: CommandError = {
console.log() code: 'DAEMON_NOT_RUNNING',
console.log(chalk.dim('Start the daemon with:')) message: `Daemon is not running (tried to connect to ${host})`,
console.log(chalk.dim(' paseo daemon start')) details: 'Start the daemon with: paseo daemon start',
process.exit(1) }
throw error
} }
try { try {
console.log(chalk.dim('Restarting daemon...'))
// Request server restart // Request server restart
await client.restartServer('cli_restart') await client.restartServer('cli_restart')
console.log(chalk.green('Daemon restart requested'))
await client.close() await client.close()
return {
type: 'single',
data: {
action: 'restarted',
host,
message: 'Daemon restart requested',
},
schema: restartResultSchema,
}
} catch (err) { } catch (err) {
await client.close().catch(() => {}) await client.close().catch(() => {})
const message = err instanceof Error ? err.message : String(err) const message = err instanceof Error ? err.message : String(err)
// If connection was closed, the daemon is restarting // If connection was closed, the daemon is restarting
if (message.includes('closed') || message.includes('disconnected')) { if (message.includes('closed') || message.includes('disconnected')) {
console.log(chalk.green('Daemon is restarting')) return {
process.exit(0) type: 'single',
data: {
action: 'restarted',
host,
message: 'Daemon is restarting',
},
schema: restartResultSchema,
}
} }
console.error(chalk.red(`Failed to restart daemon: ${message}`)) const error: CommandError = {
process.exit(1) code: 'RESTART_FAILED',
message: `Failed to restart daemon: ${message}`,
}
throw error
} }
} }
+85 -27
View File
@@ -1,28 +1,74 @@
import { Command } from 'commander' import type { Command } from 'commander'
import chalk from 'chalk'
import { resolvePaseoHome } from '@paseo/server' import { resolvePaseoHome } from '@paseo/server'
import { tryConnectToDaemon, getDaemonHost } from '../../utils/client.js' import { tryConnectToDaemon, getDaemonHost } from '../../utils/client.js'
import type { CommandOptions, ListResult, OutputSchema, CommandError } from '../../output/index.js'
export function statusCommand(): Command { /** Status data for the daemon */
return new Command('status') interface DaemonStatus {
.description('Show daemon status') status: 'running' | 'stopped'
.option('--host <host>', 'Daemon host:port (default: localhost:6767)') host: string
.action(async (options: { host?: string }) => { home: string
await runStatus(options) runningAgents: number
}) idleAgents: number
} }
async function runStatus(options: { host?: string }): Promise<void> { /** Key-value row for table display */
const host = getDaemonHost(options) interface StatusRow {
const client = await tryConnectToDaemon(options) key: string
value: string
}
/** Schema for key-value display with custom serialization for JSON/YAML */
function createStatusSchema(status: DaemonStatus): OutputSchema<StatusRow> {
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<StatusRow>
export async function runStatusCommand(
options: CommandOptions,
_command: Command
): Promise<StatusResult> {
const connectOptions = { host: options.host as string | undefined }
const host = getDaemonHost(connectOptions)
const client = await tryConnectToDaemon(connectOptions)
if (!client) { if (!client) {
console.log(chalk.red('Status:'), 'not running') const error: CommandError = {
console.log(chalk.dim(`Tried to connect to ${host}`)) code: 'DAEMON_NOT_RUNNING',
console.log() message: `Daemon is not running (tried to connect to ${host})`,
console.log(chalk.dim('Start the daemon with:')) details: 'Start the daemon with: paseo daemon start',
console.log(chalk.dim(' paseo daemon start')) }
process.exit(1) throw error
} }
try { try {
@@ -30,25 +76,37 @@ async function runStatus(options: { host?: string }): Promise<void> {
client.requestSessionState() client.requestSessionState()
// Wait a moment for the session state to be populated // 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 agents = client.listAgents()
const runningAgents = agents.filter(a => a.status === 'running') const runningAgents = agents.filter((a) => a.status === 'running')
const idleAgents = agents.filter(a => a.status === 'idle') const idleAgents = agents.filter((a) => a.status === 'idle')
// Get paseo home for display // Get paseo home for display
const paseoHome = resolvePaseoHome() const paseoHome = resolvePaseoHome()
console.log(chalk.green('Status:'), 'running') const status: DaemonStatus = {
console.log(chalk.dim('Host:'), host) status: 'running',
console.log(chalk.dim('Home:'), paseoHome) host,
console.log(chalk.dim('Agents:'), `${runningAgents.length} running, ${idleAgents.length} idle`) home: paseoHome,
runningAgents: runningAgents.length,
idleAgents: idleAgents.length,
}
await client.close() await client.close()
return {
type: 'list',
data: toStatusRows(status),
schema: createStatusSchema(status),
}
} catch (err) { } catch (err) {
await client.close().catch(() => {}) await client.close().catch(() => {})
const message = err instanceof Error ? err.message : String(err) const message = err instanceof Error ? err.message : String(err)
console.error(chalk.red(`Failed to get status: ${message}`)) const error: CommandError = {
process.exit(1) code: 'STATUS_FAILED',
message: `Failed to get status: ${message}`,
}
throw error
} }
} }
+65 -25
View File
@@ -1,53 +1,93 @@
import { Command } from 'commander' import type { Command } from 'commander'
import chalk from 'chalk'
import { connectToDaemon, getDaemonHost } from '../../utils/client.js' import { connectToDaemon, getDaemonHost } from '../../utils/client.js'
import type { CommandOptions, SingleResult, OutputSchema, CommandError } from '../../output/index.js'
export function stopCommand(): Command { /** Result of stop command */
return new Command('stop') interface StopResult {
.description('Stop the daemon') action: 'stopped' | 'not_running'
.option('--host <host>', 'Daemon host:port (default: localhost:6767)') host: string
.action(async (options: { host?: string }) => { message: string
await runStop(options)
})
} }
async function runStop(options: { host?: string }): Promise<void> { /** Schema for stop result */
const host = getDaemonHost(options) const stopResultSchema: OutputSchema<StopResult> = {
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<StopResult>
export async function runStopCommand(
options: CommandOptions,
_command: Command
): Promise<StopCommandResult> {
const connectOptions = { host: options.host as string | undefined }
const host = getDaemonHost(connectOptions)
let client let client
try { try {
client = await connectToDaemon(options) client = await connectToDaemon(connectOptions)
} catch { } catch {
console.log(chalk.yellow('Daemon is not running')) // Daemon not running - this is a valid outcome
console.log(chalk.dim(`Tried to connect to ${host}`)) return {
process.exit(0) type: 'single',
data: {
action: 'not_running',
host,
message: 'Daemon was not running',
},
schema: stopResultSchema,
}
} }
try { try {
console.log(chalk.dim('Stopping daemon...'))
// Request server restart with "shutdown" reason // Request server restart with "shutdown" reason
// This signals the daemon to shut down gracefully // This signals the daemon to shut down gracefully
await client.restartServer('cli_shutdown') await client.restartServer('cli_shutdown')
// Give the daemon a moment to acknowledge // Give the daemon a moment to acknowledge
await new Promise(resolve => setTimeout(resolve, 500)) 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 client.close() await client.close()
return {
type: 'single',
data: {
action: 'stopped',
host,
message: 'Daemon stop requested - shutting down gracefully',
},
schema: stopResultSchema,
}
} catch (err) { } catch (err) {
await client.close().catch(() => {}) await client.close().catch(() => {})
const message = err instanceof Error ? err.message : String(err) const message = err instanceof Error ? err.message : String(err)
// If connection was closed, the daemon is stopping // If connection was closed, the daemon is stopping
if (message.includes('closed') || message.includes('disconnected')) { if (message.includes('closed') || message.includes('disconnected')) {
console.log(chalk.green('Daemon is stopping')) return {
process.exit(0) type: 'single',
data: {
action: 'stopped',
host,
message: 'Daemon is stopping',
},
schema: stopResultSchema,
}
} }
console.error(chalk.red(`Failed to stop daemon: ${message}`)) const error: CommandError = {
process.exit(1) code: 'STOP_FAILED',
message: `Failed to stop daemon: ${message}`,
}
throw error
} }
} }
+10
View File
@@ -16,7 +16,17 @@ export function renderJson<T>(
// Apply custom serializer if provided // Apply custom serializer if provided
if (schema.serialize) { if (schema.serialize) {
if (result.type === 'list') { 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)) 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) return JSON.stringify(serialized, null, 2)
} else { } else {
const serialized = schema.serialize(result.data) const serialized = schema.serialize(result.data)
+2 -2
View File
@@ -97,7 +97,7 @@ function renderRow<T>(
} }
} }
return padCell(cell, width, col.align ?? 'left') return padCell(cell, width ?? 0, col.align ?? 'left')
}) })
.join(' ') .join(' ')
} }
@@ -109,7 +109,7 @@ function renderHeader<T>(
options: OutputOptions options: OutputOptions
): string { ): string {
const headerRow = columns 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(' ') .join(' ')
return options.noColor ? headerRow : chalk.bold(headerRow) return options.noColor ? headerRow : chalk.bold(headerRow)
+3 -1
View File
@@ -47,7 +47,9 @@ export function withOutput<T, Args extends unknown[]>(
): (...args: [...Args, CommandOptions, Command]) => Promise<void> { ): (...args: [...Args, CommandOptions, Command]) => Promise<void> {
return async (...args) => { return async (...args) => {
// Last two args are options and command // 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) const outputOptions = extractOutputOptions(options)
try { try {
+10
View File
@@ -17,7 +17,17 @@ export function renderYaml<T>(
// Apply custom serializer if provided // Apply custom serializer if provided
if (schema.serialize) { if (schema.serialize) {
if (result.type === 'list') { 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)) 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) return YAML.stringify(serialized)
} else { } else {
const serialized = schema.serialize(result.data) const serialized = schema.serialize(result.data)
@@ -4,7 +4,7 @@
* Test runner for Paseo CLI E2E tests * Test runner for Paseo CLI E2E tests
* *
* Runs all test phases in sequence and reports results. * 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' import { $ } from 'zx'
@@ -22,7 +22,7 @@ console.log('='.repeat(50))
// Discover all test files // Discover all test files
const files = await readdir(__dirname) const files = await readdir(__dirname)
const testFiles = files const testFiles = files
.filter(f => f.match(/^\d{2}-.*\.test\.mts$/)) .filter(f => f.match(/^\d{2}-.*\.test\.ts$/))
.sort() .sort()
if (testFiles.length === 0) { if (testFiles.length === 0) {
@@ -42,14 +42,14 @@ const failures: { test: string; error: string }[] = []
for (const testFile of testFiles) { for (const testFile of testFiles) {
const testPath = join(__dirname, testFile) const testPath = join(__dirname, testFile)
const testName = testFile.replace(/\.test\.mts$/, '') const testName = testFile.replace(/\.test\.ts$/, '')
console.log(`\n${'─'.repeat(50)}`) console.log(`\n${'─'.repeat(50)}`)
console.log(`📋 Running ${testName}...`) console.log(`📋 Running ${testName}...`)
console.log('─'.repeat(50)) console.log('─'.repeat(50))
try { try {
const result = await $`npx zx ${testPath}`.nothrow() const result = await $`npx tsx ${testPath}`.nothrow()
if (result.exitCode === 0) { if (result.exitCode === 0) {
console.log(`\n✅ ${testName} PASSED`) console.log(`\n✅ ${testName} PASSED`)
passed++ passed++