From 6fd421446674c5a9a9cefd22d9179da2306efde0 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 28 Jan 2026 22:47:08 +0700 Subject: [PATCH] test(cli): add zx TDD infrastructure and phase 1 tests - Install zx as devDependency for E2E testing - Create tests/ directory with setup utilities - Add setup.ts with test context helpers: - Random port generation (never 6767) - Temp directory creation for PASEO_HOME and workDir - Daemon startup/waitForDaemon helper (WebSocket only) - Cleanup function - paseo() helper to run CLI commands against test daemon - Add Phase 1 foundation tests (01-foundation.test.mts): - Test paseo --version outputs version - Test paseo --help shows commands - Add test runner (run-all.mts) that discovers and runs all tests - Add test:e2e script to package.json --- package-lock.json | 20 ++- packages/cli/package.json | 10 +- packages/cli/src/cli.ts | 11 +- packages/cli/src/commands/daemon/index.ts | 17 +++ packages/cli/src/commands/daemon/restart.ts | 51 ++++++++ packages/cli/src/commands/daemon/start.ts | 94 +++++++++++++ packages/cli/src/commands/daemon/status.ts | 54 ++++++++ packages/cli/src/commands/daemon/stop.ts | 53 ++++++++ packages/cli/src/utils/client.ts | 75 +++++++++++ packages/cli/tests/01-foundation.test.mts | 53 ++++++++ packages/cli/tests/run-all.mts | 92 +++++++++++++ packages/cli/tests/setup.ts | 138 ++++++++++++++++++++ packages/server/package.json | 1 + packages/server/src/server/exports.ts | 7 + 14 files changed, 664 insertions(+), 12 deletions(-) create mode 100644 packages/cli/src/commands/daemon/index.ts create mode 100644 packages/cli/src/commands/daemon/restart.ts create mode 100644 packages/cli/src/commands/daemon/start.ts create mode 100644 packages/cli/src/commands/daemon/status.ts create mode 100644 packages/cli/src/commands/daemon/stop.ts create mode 100644 packages/cli/src/utils/client.ts create mode 100644 packages/cli/tests/01-foundation.test.mts create mode 100644 packages/cli/tests/run-all.mts create mode 100644 packages/cli/tests/setup.ts create mode 100644 packages/server/src/server/exports.ts diff --git a/package-lock.json b/package-lock.json index bade96865..5acabcf6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26240,6 +26240,19 @@ } } }, + "node_modules/zx": { + "version": "8.8.5", + "resolved": "https://registry.npmjs.org/zx/-/zx-8.8.5.tgz", + "integrity": "sha512-SNgDF5L0gfN7FwVOdEFguY3orU5AkfFZm9B5YSHog/UDHv+lvmd82ZAsOenOkQixigwH2+yyH198AwNdKhj+RA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "zx": "build/cli.js" + }, + "engines": { + "node": ">= 12.17.0" + } + }, "packages/app": { "name": "@paseo/app", "version": "1.0.0", @@ -26355,14 +26368,17 @@ "dependencies": { "@paseo/server": "*", "chalk": "^5.3.0", - "commander": "^12.0.0" + "commander": "^12.0.0", + "ws": "^8.14.2" }, "bin": { "paseo": "bin/paseo" }, "devDependencies": { + "@types/ws": "^8.5.8", "tsx": "^4.6.0", - "typescript": "^5.2.2" + "typescript": "^5.2.2", + "zx": "^8.8.5" } }, "packages/cli/node_modules/chalk": { diff --git a/packages/cli/package.json b/packages/cli/package.json index 014cbcc31..7a6004fee 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -7,15 +7,19 @@ "paseo": "./bin/paseo" }, "scripts": { - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "test:e2e": "npx zx tests/run-all.mts" }, "dependencies": { "@paseo/server": "*", + "chalk": "^5.3.0", "commander": "^12.0.0", - "chalk": "^5.3.0" + "ws": "^8.14.2" }, "devDependencies": { + "@types/ws": "^8.5.8", + "tsx": "^4.6.0", "typescript": "^5.2.2", - "tsx": "^4.6.0" + "zx": "^8.8.5" } } diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index c776b8d67..2a4339934 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -1,4 +1,5 @@ import { Command } from 'commander' +import { createDaemonCommand } from './commands/daemon/index.js' const VERSION = '0.1.0' @@ -10,7 +11,7 @@ export function createCli(): Command { .description('Paseo CLI - control your AI coding agents from the command line') .version(VERSION, '-v, --version', 'output the version number') - // Placeholder subcommands for Phase 1 + // Placeholder subcommands program .command('agent') .description('Manage agents') @@ -18,12 +19,8 @@ export function createCli(): Command { console.log('agent command (not yet implemented)') }) - program - .command('daemon') - .description('Manage the Paseo daemon') - .action(() => { - console.log('daemon command (not yet implemented)') - }) + // Real daemon command + program.addCommand(createDaemonCommand()) program .command('permit') diff --git a/packages/cli/src/commands/daemon/index.ts b/packages/cli/src/commands/daemon/index.ts new file mode 100644 index 000000000..817fe652c --- /dev/null +++ b/packages/cli/src/commands/daemon/index.ts @@ -0,0 +1,17 @@ +import { Command } from 'commander' +import { startCommand } from './start.js' +import { statusCommand } from './status.js' +import { stopCommand } from './stop.js' +import { restartCommand } from './restart.js' + +export function createDaemonCommand(): Command { + const daemon = new Command('daemon') + .description('Manage the Paseo daemon') + + daemon.addCommand(startCommand()) + daemon.addCommand(statusCommand()) + daemon.addCommand(stopCommand()) + daemon.addCommand(restartCommand()) + + return daemon +} diff --git a/packages/cli/src/commands/daemon/restart.ts b/packages/cli/src/commands/daemon/restart.ts new file mode 100644 index 000000000..090ba28bc --- /dev/null +++ b/packages/cli/src/commands/daemon/restart.ts @@ -0,0 +1,51 @@ +import { Command } from 'commander' +import chalk from 'chalk' +import { connectToDaemon, getDaemonHost } from '../../utils/client.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) + }) +} + +async function runRestart(options: { host?: string }): Promise { + const host = getDaemonHost(options) + + let client + try { + client = await connectToDaemon(options) + } 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) + } + + 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() + } 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) + } + + console.error(chalk.red(`Failed to restart daemon: ${message}`)) + process.exit(1) + } +} diff --git a/packages/cli/src/commands/daemon/start.ts b/packages/cli/src/commands/daemon/start.ts new file mode 100644 index 000000000..60234b95e --- /dev/null +++ b/packages/cli/src/commands/daemon/start.ts @@ -0,0 +1,94 @@ +import { Command } from 'commander' +import chalk from 'chalk' +import { + createPaseoDaemon, + loadConfig, + resolvePaseoHome, + createRootLogger, + loadPersistedConfig, +} from '@paseo/server' + +interface StartOptions { + port?: string + home?: string + foreground?: boolean + noRelay?: boolean +} + +export function startCommand(): Command { + return new Command('start') + .description('Start the Paseo daemon') + .option('--port ', 'Port to listen on (default: 6767)') + .option('--home ', 'Paseo home directory (default: ~/.paseo)') + .option('--foreground', 'Run in foreground (don\'t daemonize)') + .option('--no-relay', 'Disable relay connection') + .action(async (options: StartOptions) => { + await runStart(options) + }) +} + +async function runStart(options: StartOptions): Promise { + // Set environment variables based on CLI options + if (options.home) { + process.env.PASEO_HOME = options.home + } + if (options.port) { + process.env.PASEO_LISTEN = `127.0.0.1:${options.port}` + } + + const paseoHome = resolvePaseoHome() + const persistedConfig = loadPersistedConfig(paseoHome) + const logger = createRootLogger(persistedConfig) + const config = loadConfig(paseoHome) + + // Apply CLI overrides + if (options.noRelay) { + config.relayEnabled = false + } + + // For now, only foreground mode is supported + // TODO: Implement daemonization in a future phase + if (!options.foreground) { + console.log(chalk.yellow('Note: Background daemon mode not yet implemented. Running in foreground.')) + } + + const daemon = await createPaseoDaemon(config, logger) + + // Handle graceful shutdown + let shuttingDown = false + const handleShutdown = async (signal: string) => { + if (shuttingDown) { + logger.info('Forcing exit...') + process.exit(1) + } + shuttingDown = true + logger.info(`${signal} received, shutting down gracefully... (press Ctrl+C again to force exit)`) + + const forceExit = setTimeout(() => { + logger.warn('Forcing shutdown - HTTP server didn\'t close in time') + process.exit(1) + }, 10000) + + try { + await daemon.stop() + clearTimeout(forceExit) + logger.info('Server closed') + process.exit(0) + } catch (err) { + clearTimeout(forceExit) + logger.error({ err }, 'Shutdown failed') + process.exit(1) + } + } + + process.on('SIGTERM', () => handleShutdown('SIGTERM')) + process.on('SIGINT', () => handleShutdown('SIGINT')) + + try { + await daemon.start() + } catch (err) { + const message = err instanceof Error ? err.message : String(err) + console.error(chalk.red(`Failed to start daemon: ${message}`)) + process.exit(1) + } +} diff --git a/packages/cli/src/commands/daemon/status.ts b/packages/cli/src/commands/daemon/status.ts new file mode 100644 index 000000000..4f435a25d --- /dev/null +++ b/packages/cli/src/commands/daemon/status.ts @@ -0,0 +1,54 @@ +import { Command } from 'commander' +import chalk from 'chalk' +import { resolvePaseoHome } from '@paseo/server' +import { tryConnectToDaemon, getDaemonHost } from '../../utils/client.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) + }) +} + +async function runStatus(options: { host?: string }): Promise { + const host = getDaemonHost(options) + const client = await tryConnectToDaemon(options) + + 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) + } + + try { + // Request session state to get agent information + client.requestSessionState() + + // Wait a moment for the session state to be populated + 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') + + // 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`) + + await client.close() + } 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) + } +} diff --git a/packages/cli/src/commands/daemon/stop.ts b/packages/cli/src/commands/daemon/stop.ts new file mode 100644 index 000000000..00f347be3 --- /dev/null +++ b/packages/cli/src/commands/daemon/stop.ts @@ -0,0 +1,53 @@ +import { Command } from 'commander' +import chalk from 'chalk' +import { connectToDaemon, getDaemonHost } from '../../utils/client.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) + }) +} + +async function runStop(options: { host?: string }): Promise { + const host = getDaemonHost(options) + + let client + try { + client = await connectToDaemon(options) + } catch { + console.log(chalk.yellow('Daemon is not running')) + console.log(chalk.dim(`Tried to connect to ${host}`)) + process.exit(0) + } + + 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 client.close() + } 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) + } + + console.error(chalk.red(`Failed to stop daemon: ${message}`)) + process.exit(1) + } +} diff --git a/packages/cli/src/utils/client.ts b/packages/cli/src/utils/client.ts new file mode 100644 index 000000000..a7926e30d --- /dev/null +++ b/packages/cli/src/utils/client.ts @@ -0,0 +1,75 @@ +import { DaemonClientV2 } from '@paseo/server' +import WebSocket from 'ws' + +export interface ConnectOptions { + host?: string + timeout?: number +} + +const DEFAULT_HOST = 'localhost:6767' +const DEFAULT_TIMEOUT = 5000 + +/** + * Get the daemon host from environment or options + */ +export function getDaemonHost(options?: ConnectOptions): string { + return options?.host ?? process.env.PASEO_HOST ?? DEFAULT_HOST +} + +/** + * Create a WebSocket factory that works in Node.js + */ +function createNodeWebSocketFactory() { + return (url: string, options?: { headers?: Record }) => { + return new WebSocket(url, { headers: options?.headers }) as unknown as { + readyState: number + send: (data: string) => void + close: (code?: number, reason?: string) => void + on: (event: string, listener: (...args: unknown[]) => void) => void + off: (event: string, listener: (...args: unknown[]) => void) => void + } + } +} + +/** + * Create and connect a daemon client + * Returns the connected client or throws if connection fails + */ +export async function connectToDaemon(options?: ConnectOptions): Promise { + const host = getDaemonHost(options) + const timeout = options?.timeout ?? DEFAULT_TIMEOUT + const url = `ws://${host}/ws` + + const client = new DaemonClientV2({ + url, + webSocketFactory: createNodeWebSocketFactory(), + reconnect: { enabled: false }, + }) + + // Connect with timeout + const connectPromise = client.connect() + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + reject(new Error(`Connection timeout after ${timeout}ms`)) + }, timeout) + }) + + try { + await Promise.race([connectPromise, timeoutPromise]) + return client + } catch (err) { + await client.close().catch(() => {}) + throw err + } +} + +/** + * Try to connect to the daemon, returns null if connection fails + */ +export async function tryConnectToDaemon(options?: ConnectOptions): Promise { + try { + return await connectToDaemon(options) + } catch { + return null + } +} diff --git a/packages/cli/tests/01-foundation.test.mts b/packages/cli/tests/01-foundation.test.mts new file mode 100644 index 000000000..1b5c509cd --- /dev/null +++ b/packages/cli/tests/01-foundation.test.mts @@ -0,0 +1,53 @@ +#!/usr/bin/env npx zx + +/** + * Phase 1: Foundation Tests + * + * Tests basic CLI functionality that doesn't require a daemon: + * - paseo --version outputs version + * - paseo --help shows commands + */ + +import { $ } from 'zx' + +$.verbose = false + +console.log('๐Ÿ“‹ Phase 1: Foundation Tests\n') + +// Test 1.1: --version outputs version +console.log(' Testing paseo --version...') +const versionResult = await $`paseo --version`.nothrow() +if (versionResult.exitCode !== 0) { + console.error(' โŒ paseo --version failed with exit code', versionResult.exitCode) + console.error(' stderr:', versionResult.stderr) + process.exit(1) +} +const versionOutput = versionResult.stdout.trim() +if (!versionOutput.match(/\d+\.\d+\.\d+/)) { + console.error(' โŒ paseo --version output does not contain version number') + console.error(' output:', versionOutput) + process.exit(1) +} +console.log(' โœ… paseo --version outputs:', versionOutput) + +// Test 1.2: --help shows commands +console.log(' Testing paseo --help...') +const helpResult = await $`paseo --help`.nothrow() +if (helpResult.exitCode !== 0) { + console.error(' โŒ paseo --help failed with exit code', helpResult.exitCode) + console.error(' stderr:', helpResult.stderr) + process.exit(1) +} +const helpOutput = helpResult.stdout + +// Check for expected sections in help output +const expectedTerms = ['agent', 'daemon', 'Usage', 'Options', 'Commands'] +const missingTerms = expectedTerms.filter(term => !helpOutput.includes(term)) +if (missingTerms.length > 0) { + console.error(' โŒ paseo --help missing expected terms:', missingTerms.join(', ')) + console.error(' output:', helpOutput) + process.exit(1) +} +console.log(' โœ… paseo --help shows commands') + +console.log('\nโœ… Phase 1: Foundation Tests PASSED') diff --git a/packages/cli/tests/run-all.mts b/packages/cli/tests/run-all.mts new file mode 100644 index 000000000..9713e65a7 --- /dev/null +++ b/packages/cli/tests/run-all.mts @@ -0,0 +1,92 @@ +#!/usr/bin/env npx zx + +/** + * 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. + */ + +import { $ } from 'zx' +import { readdir } from 'fs/promises' +import { join, dirname } from 'path' +import { fileURLToPath } from 'url' + +const __dirname = dirname(fileURLToPath(import.meta.url)) + +$.verbose = false + +console.log('๐Ÿงช Paseo CLI E2E Test Runner\n') +console.log('='.repeat(50)) + +// Discover all test files +const files = await readdir(__dirname) +const testFiles = files + .filter(f => f.match(/^\d{2}-.*\.test\.mts$/)) + .sort() + +if (testFiles.length === 0) { + console.log('โš ๏ธ No test files found') + process.exit(0) +} + +console.log(`Found ${testFiles.length} test file(s):\n`) +for (const file of testFiles) { + console.log(` - ${file}`) +} +console.log() + +let passed = 0 +let failed = 0 +const failures: { test: string; error: string }[] = [] + +for (const testFile of testFiles) { + const testPath = join(__dirname, testFile) + const testName = testFile.replace(/\.test\.mts$/, '') + + console.log(`\n${'โ”€'.repeat(50)}`) + console.log(`๐Ÿ“‹ Running ${testName}...`) + console.log('โ”€'.repeat(50)) + + try { + const result = await $`npx zx ${testPath}`.nothrow() + if (result.exitCode === 0) { + console.log(`\nโœ… ${testName} PASSED`) + passed++ + } else { + console.log(`\nโŒ ${testName} FAILED (exit code: ${result.exitCode})`) + if (result.stderr) { + console.log('stderr:', result.stderr) + } + failed++ + failures.push({ test: testName, error: result.stderr || `Exit code: ${result.exitCode}` }) + } + } catch (e) { + const error = e instanceof Error ? e.message : String(e) + console.log(`\nโŒ ${testName} FAILED`) + console.log('Error:', error) + failed++ + failures.push({ test: testName, error }) + } +} + +// Summary +console.log('\n' + '='.repeat(50)) +console.log('๐Ÿ“Š Test Results') +console.log('='.repeat(50)) +console.log(` โœ… Passed: ${passed}`) +console.log(` โŒ Failed: ${failed}`) +console.log(` ๐Ÿ“ Total: ${passed + failed}`) + +if (failures.length > 0) { + console.log('\nโŒ Failed tests:') + for (const { test, error } of failures) { + console.log(` - ${test}`) + if (error) { + console.log(` ${error.split('\n')[0]}`) + } + } +} + +console.log() +process.exit(failed > 0 ? 1 : 0) diff --git a/packages/cli/tests/setup.ts b/packages/cli/tests/setup.ts new file mode 100644 index 000000000..7aac061ad --- /dev/null +++ b/packages/cli/tests/setup.ts @@ -0,0 +1,138 @@ +/** + * Test setup utilities for Paseo CLI E2E tests + * + * Critical rules from design doc: + * 1. Port: Random port via 10000 + Math.floor(Math.random() * 50000) - NEVER 6767 + * 2. Protocol: WebSocket ONLY - daemon has no HTTP endpoints + * 3. Temp dirs: Create temp directories for PASEO_HOME and agent --cwd + * 4. Model: Always --provider claude with haiku model for agent tests + * 5. Cleanup: Kill daemon and remove temp dirs after each test + */ + +import { $, ProcessPromise, sleep } from 'zx' +import { mkdtemp, rm } from 'fs/promises' +import { tmpdir } from 'os' +import { join } from 'path' + +export interface TestContext { + /** Random port for test daemon (never 6767) */ + port: number + /** Temp directory for PASEO_HOME */ + paseoHome: string + /** Temp directory for agent working directory */ + workDir: string + /** Running daemon process */ + daemon: ProcessPromise | null + /** Run a paseo CLI command against the test daemon */ + paseo: (args: string[]) => ProcessPromise + /** Clean up all resources */ + cleanup: () => Promise +} + +/** + * Generate a random port for test daemon + * NEVER uses 6767 (user's running daemon) + */ +export function getRandomPort(): number { + return 10000 + Math.floor(Math.random() * 50000) +} + +/** + * Create isolated temp directories for testing + */ +export async function createTempDirs(): Promise<{ paseoHome: string; workDir: string }> { + const paseoHome = await mkdtemp(join(tmpdir(), 'paseo-test-home-')) + const workDir = await mkdtemp(join(tmpdir(), 'paseo-test-work-')) + return { paseoHome, workDir } +} + +/** + * Wait for daemon to be ready by testing WebSocket connection + * Uses `paseo agent ps` which connects via WebSocket + */ +export async function waitForDaemon(port: number, timeout = 30000): Promise { + const start = Date.now() + while (Date.now() - start < timeout) { + try { + const result = await $`PASEO_HOST=localhost:${port} paseo agent ps`.nothrow() + if (result.exitCode === 0) return + } catch { + // Connection failed, keep trying + } + await sleep(100) + } + throw new Error(`Daemon failed to start on port ${port} within ${timeout}ms`) +} + +/** + * Start an isolated test daemon + */ +export async function startDaemon( + port: number, + paseoHome: string +): Promise { + $.verbose = false + const daemon = $`PASEO_HOME=${paseoHome} PASEO_PORT=${port} paseo daemon start --foreground`.nothrow() + return daemon +} + +/** + * Create a full test context with daemon, temp dirs, and helpers + */ +export async function createTestContext(): Promise { + const port = getRandomPort() + const { paseoHome, workDir } = await createTempDirs() + + // Helper to run CLI commands against test daemon + const paseo = (args: string[]): ProcessPromise => { + $.verbose = false + return $`PASEO_HOST=localhost:${port} paseo ${args}`.nothrow() + } + + // Cleanup function + const cleanup = async (): Promise => { + if (ctx.daemon) { + ctx.daemon.kill() + } + await rm(paseoHome, { recursive: true, force: true }) + await rm(workDir, { recursive: true, force: true }) + } + + const ctx: TestContext = { + port, + paseoHome, + workDir, + daemon: null, + paseo, + cleanup, + } + + return ctx +} + +/** + * Create a test context and start the daemon + * Use this for tests that need a running daemon + */ +export async function createTestContextWithDaemon(): Promise { + const ctx = await createTestContext() + ctx.daemon = await startDaemon(ctx.port, ctx.paseoHome) + await waitForDaemon(ctx.port) + return ctx +} + +/** + * Register cleanup handlers for process exit + */ +export function registerCleanupHandlers(cleanup: () => Promise): void { + const handler = async () => { + await cleanup() + process.exit(0) + } + + process.on('exit', () => { + // Can't await in exit handler, but at least try to kill daemon + }) + process.on('SIGINT', handler) + process.on('SIGTERM', handler) +} diff --git a/packages/server/package.json b/packages/server/package.json index 0d5739424..24782c288 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -4,6 +4,7 @@ "description": "Paseo backend server", "type": "module", "exports": { + ".": "./src/server/exports.ts", "./utils/tool-call-parsers": "./src/utils/tool-call-parsers.ts" }, "scripts": { diff --git a/packages/server/src/server/exports.ts b/packages/server/src/server/exports.ts new file mode 100644 index 000000000..e077d74d5 --- /dev/null +++ b/packages/server/src/server/exports.ts @@ -0,0 +1,7 @@ +// CLI exports for @paseo/server +export { createPaseoDaemon, type PaseoDaemon, type PaseoDaemonConfig } from "./bootstrap.js"; +export { loadConfig } from "./config.js"; +export { resolvePaseoHome } from "./paseo-home.js"; +export { createRootLogger, type LogLevel, type LogFormat } from "./logger.js"; +export { loadPersistedConfig, type PersistedConfig } from "./persisted-config.js"; +export { DaemonClientV2, type DaemonClientV2Config, type ConnectionState, type DaemonEvent } from "../client/daemon-client-v2.js";