From 8f9100f6a9cef2ec4d66179bfbd1292d73895bbf Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 5 Feb 2026 14:35:55 +0700 Subject: [PATCH] chore: remove packages/cli/docs --- packages/cli/docs/output-architecture.md | 544 ----------------------- packages/cli/docs/type-audit.md | 155 ------- 2 files changed, 699 deletions(-) delete mode 100644 packages/cli/docs/output-architecture.md delete mode 100644 packages/cli/docs/type-audit.md diff --git a/packages/cli/docs/output-architecture.md b/packages/cli/docs/output-architecture.md deleted file mode 100644 index a3778f29b..000000000 --- a/packages/cli/docs/output-architecture.md +++ /dev/null @@ -1,544 +0,0 @@ -# Output Architecture Design - -This document describes the output abstraction layer for the Paseo CLI, enabling structured data output with multiple format options. - -## Overview - -Commands should return **structured data objects**, not formatted strings. A separate rendering layer transforms this data into the requested output format. This separation enables: - -1. **Testability** - Tests verify structured data without parsing strings -2. **Flexibility** - Easy to add new output formats -3. **Consistency** - Uniform formatting across all commands - -### Inspiration from Existing CLIs - -This design draws from patterns in established CLIs: - -- **Docker CLI** - Uses Go templates with `--format` flag, provides `table` and `json` directives -- **kubectl** - Supports `-o json`, `-o yaml`, `-o wide`, and custom columns -- **GitHub CLI** - Uses `--json` with field selection, plus `--jq` and `--template` post-processors - -Sources: -- [Docker CLI Formatting](https://docs.docker.com/engine/cli/formatting/) -- [kubectl Output Formatting](https://www.baeldung.com/ops/kubectl-output-format) -- [GitHub CLI Formatting](https://cli.github.com/manual/gh_help_formatting) - -## Architecture - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Command Execution │ -│ │ -│ parseArgs() → executeCommand() → CommandResult │ -└─────────────────────────────────┬───────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ Output Renderer │ -│ │ -│ CommandResult + OutputOptions → formatted string │ -│ │ -│ Renderers: │ -│ - TableRenderer (default, human-readable) │ -│ - JsonRenderer (machine-readable) │ -│ - YamlRenderer (machine-readable) │ -│ - QuietRenderer (minimal, IDs only) │ -└─────────────────────────────────┬───────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ stdout/stderr │ -└─────────────────────────────────────────────────────────────┘ -``` - -## Type Definitions - -### Output Options - -```typescript -type OutputFormat = 'table' | 'json' | 'yaml' - -interface OutputOptions { - format: OutputFormat - quiet: boolean // Minimal output (IDs only) - noHeaders: boolean // Omit table headers - noColor: boolean // Disable color output -} -``` - -### Command Result - -Commands return a `CommandResult` that contains structured data plus metadata for formatting: - -```typescript -interface CommandResult { - /** The structured data to render */ - data: T - - /** Schema describing how to render this data */ - schema: OutputSchema -} - -interface OutputSchema { - /** Field to use for quiet mode (--quiet outputs just this) */ - idField: keyof T | ((item: T) => string) - - /** Column definitions for table output */ - columns: ColumnDef[] - - /** Optional: transform data before JSON/YAML output */ - serialize?: (data: T) => unknown -} - -interface ColumnDef { - /** Header text for the column */ - header: string - - /** Field key or accessor function */ - field: keyof T | ((item: T) => unknown) - - /** Optional width hint (characters) */ - width?: number - - /** Optional alignment */ - align?: 'left' | 'right' | 'center' - - /** Optional color function */ - color?: (value: unknown, item: T) => string | undefined -} -``` - -### Single vs List Results - -Commands may return either a single item or a list: - -```typescript -// For commands returning a single item (e.g., `agent show `) -interface SingleResult extends CommandResult { - type: 'single' - data: T -} - -// For commands returning a list (e.g., `agent list`) -interface ListResult extends CommandResult { - type: 'list' - data: T[] -} - -// Union type for command handlers -type AnyCommandResult = SingleResult | ListResult -``` - -## Example: Agent List Command - -### Data Type - -```typescript -interface AgentListItem { - id: string - title: string - status: 'running' | 'idle' | 'error' - provider: string - cwd: string - createdAt: string -} -``` - -### Schema Definition - -```typescript -const agentListSchema: OutputSchema = { - idField: 'id', - - columns: [ - { - header: 'ID', - field: 'id', - width: 8, - }, - { - header: 'TITLE', - field: 'title', - width: 30, - }, - { - header: 'STATUS', - field: 'status', - color: (value) => { - switch (value) { - case 'running': return 'green' - case 'idle': return 'dim' - case 'error': return 'red' - default: return undefined - } - }, - }, - { - header: 'PROVIDER', - field: 'provider', - }, - { - header: 'CWD', - field: 'cwd', - }, - ], -} -``` - -### Command Implementation - -```typescript -async function agentListCommand(options: CommandOptions): Promise> { - const client = await connectToDaemon(options) - const agents = client.listAgents() - - const data = agents.map(agent => ({ - id: agent.agentId, - title: agent.title ?? '(untitled)', - status: mapLifecycleStatus(agent.lifecycle), - provider: agent.agentType, - cwd: agent.cwd, - createdAt: agent.createdAt, - })) - - return { - type: 'list', - data, - schema: agentListSchema, - } -} -``` - -## Renderer Implementations - -### Table Renderer - -The default renderer for human-readable output: - -```typescript -function renderTable(result: ListResult, options: OutputOptions): string { - const { data, schema } = result - - if (data.length === 0) { - return '' // Or a "no items" message - } - - const rows: string[][] = [] - - // Add header row (unless noHeaders) - if (!options.noHeaders) { - rows.push(schema.columns.map(col => col.header)) - } - - // Add data rows - for (const item of data) { - const row = schema.columns.map(col => { - const value = typeof col.field === 'function' - ? col.field(item) - : item[col.field] - return String(value ?? '') - }) - rows.push(row) - } - - // Calculate column widths - const widths = schema.columns.map((col, i) => { - const maxContent = Math.max(...rows.map(row => stripAnsi(row[i]).length)) - return col.width ? Math.max(col.width, maxContent) : maxContent - }) - - // Format and join - return rows.map((row, rowIndex) => { - return row.map((cell, colIndex) => { - const col = schema.columns[colIndex] - const width = widths[colIndex] - let formatted = padCell(cell, width, col.align ?? 'left') - - // Apply color (skip header row) - if (rowIndex > 0 && col.color && !options.noColor) { - const colorName = col.color(cell, data[rowIndex - 1]) - if (colorName) { - formatted = applyColor(formatted, colorName) - } - } - - return formatted - }).join(' ') - }).join('\n') -} -``` - -### JSON Renderer - -```typescript -function renderJson(result: AnyCommandResult, options: OutputOptions): string { - const { data, schema } = result - const output = schema.serialize ? schema.serialize(data) : data - return JSON.stringify(output, null, 2) -} -``` - -### YAML Renderer - -```typescript -import YAML from 'yaml' - -function renderYaml(result: AnyCommandResult, options: OutputOptions): string { - const { data, schema } = result - const output = schema.serialize ? schema.serialize(data) : data - return YAML.stringify(output) -} -``` - -### Quiet Renderer - -Returns only the ID field(s): - -```typescript -function renderQuiet(result: AnyCommandResult, options: OutputOptions): string { - const { data, schema } = result - const getId = typeof schema.idField === 'function' - ? schema.idField - : (item: T) => String(item[schema.idField as keyof T]) - - if (result.type === 'single') { - return getId(data as T) - } - - return (data as T[]).map(getId).join('\n') -} -``` - -## Error Output - -Errors are handled separately from success output and always go to stderr: - -```typescript -interface CommandError { - code: string // Machine-readable error code - message: string // Human-readable message - details?: unknown // Additional context -} - -function renderError(error: CommandError, options: OutputOptions): string { - if (options.format === 'json') { - return JSON.stringify({ error }, null, 2) - } - - if (options.format === 'yaml') { - return YAML.stringify({ error }) - } - - // Table/default format - return chalk.red(`Error: ${error.message}`) -} -``` - -## Streaming Output - -For commands like `logs -f` and `attach`, streaming requires a different approach: - -```typescript -interface StreamingResult { - type: 'stream' - schema: OutputSchema - - /** Async iterator yielding items as they arrive */ - stream: AsyncIterable -} -``` - -### Streaming Renderer - -```typescript -async function renderStream( - result: StreamingResult, - options: OutputOptions, - write: (chunk: string) => void -): Promise { - const { stream, schema } = result - - // For JSON, output newline-delimited JSON (NDJSON) - if (options.format === 'json') { - for await (const item of stream) { - write(JSON.stringify(item) + '\n') - } - return - } - - // For table format, render each item as a row - let headerWritten = false - for await (const item of stream) { - if (!headerWritten && !options.noHeaders) { - write(renderTableHeader(schema) + '\n') - headerWritten = true - } - write(renderTableRow(item, schema, options) + '\n') - } -} -``` - -### NDJSON for Streaming - -When `--json` (or `--format json`) is used with streaming commands, output is newline-delimited JSON (NDJSON) for easy parsing: - -``` -{"timestamp":"2024-01-15T10:30:00Z","type":"stdout","content":"Hello"} -{"timestamp":"2024-01-15T10:30:01Z","type":"stdout","content":"World"} -``` - -This allows consumers to process output line-by-line without buffering the entire stream. - -## Testing - -### Testing Structured Data - -Tests can directly verify the structured data without parsing formatted output: - -```typescript -describe('agent list', () => { - it('returns agents with correct structure', async () => { - const result = await agentListCommand({ host: testHost }) - - expect(result.type).toBe('list') - expect(result.data).toHaveLength(2) - expect(result.data[0]).toMatchObject({ - id: expect.any(String), - title: 'Test Agent', - status: 'running', - }) - }) - - it('uses correct schema for table output', async () => { - const result = await agentListCommand({ host: testHost }) - - expect(result.schema.idField).toBe('id') - expect(result.schema.columns.map(c => c.header)).toEqual([ - 'ID', 'TITLE', 'STATUS', 'PROVIDER', 'CWD' - ]) - }) -}) -``` - -### Testing Renderers - -Renderer tests verify formatting independently: - -```typescript -describe('table renderer', () => { - it('formats data as aligned table', () => { - const result: ListResult = { - type: 'list', - data: [ - { id: 'abc123', title: 'Agent 1', status: 'running', ... }, - { id: 'def456', title: 'Agent 2', status: 'idle', ... }, - ], - schema: agentListSchema, - } - - const output = renderTable(result, { format: 'table', quiet: false, ... }) - - expect(output).toContain('ID') - expect(output).toContain('abc123') - expect(output).toContain('Agent 1') - }) -}) -``` - -### E2E Tests - -E2E tests can verify both structured data (for correctness) and formatted output (for UX): - -```typescript -// Verify JSON output is valid and contains expected data -test('agent list --json', async () => { - const output = await ctx.paseo('agent list --json') - const data = JSON.parse(output.stdout) - - expect(data).toBeInstanceOf(Array) - expect(data[0]).toHaveProperty('id') -}) - -// Verify table output looks correct -test('agent list shows table headers', async () => { - const output = await ctx.paseo('agent list') - - expect(output.stdout).toMatch(/ID\s+TITLE\s+STATUS/) -}) -``` - -## Integration with Command Framework - -### Global Options - -Add output options to the root command: - -```typescript -program - .option('-o, --format ', 'Output format: table, json, yaml', 'table') - .option('--json', 'Output in JSON format (alias for --format json)') - .option('-q, --quiet', 'Minimal output (IDs only)') - .option('--no-headers', 'Omit table headers') - .option('--no-color', 'Disable colored output') -``` - -### Command Handler Wrapper - -A wrapper function handles the rendering: - -```typescript -function withOutput( - handler: (options: CommandOptions) => Promise> -) { - return async (options: CommandOptions) => { - try { - const result = await handler(options) - const output = render(result, options) - process.stdout.write(output + '\n') - } catch (error) { - const errorOutput = renderError(toCommandError(error), options) - process.stderr.write(errorOutput + '\n') - process.exit(1) - } - } -} - -// Usage -program - .command('list') - .description('List agents') - .action(withOutput(agentListCommand)) -``` - -## Implementation Plan - -1. **Phase 1: Core Types** - - Define `CommandResult`, `OutputSchema`, `ColumnDef` types - - Implement basic table renderer - - Implement JSON renderer - -2. **Phase 2: Integration** - - Add global output options to CLI - - Create `withOutput` wrapper - - Migrate `daemon status` command as proof of concept - -3. **Phase 3: Full Coverage** - - Add YAML renderer - - Add quiet renderer - - Migrate all existing commands - -4. **Phase 4: Streaming** - - Implement `StreamingResult` type - - Add streaming renderers - - Apply to `logs` and `attach` commands - -## Open Questions - -1. **Should we support Go templates like Docker/gh?** This adds flexibility but also complexity. For v1, predefined formats are likely sufficient. - -2. **How to handle nested data in tables?** Options: - - Flatten (e.g., `config.timeout` becomes `TIMEOUT` column) - - Skip in table, include in JSON/YAML - - Use nested tables for detail views - -3. **Should quiet mode support custom fields?** e.g., `--quiet=title` to output titles instead of IDs. diff --git a/packages/cli/docs/type-audit.md b/packages/cli/docs/type-audit.md deleted file mode 100644 index a25dc97f3..000000000 --- a/packages/cli/docs/type-audit.md +++ /dev/null @@ -1,155 +0,0 @@ -# CLI Type Audit (commands) - -## Scope -- Audited `packages/cli/src/commands/**` for inline type/interface definitions. -- Checked `@getpaseo/server` exports from `packages/server/src/server/exports.ts`. -- Note: `packages/server/src/index.ts` does **not** exist in this repo; the package export entrypoint is `./src/server/exports.ts` per `packages/server/package.json`. - -## Server Exports (current) -`packages/server/src/server/exports.ts` exports: -- `createPaseoDaemon`, `PaseoDaemon`, `PaseoDaemonConfig` -- `loadConfig`, `resolvePaseoHome` -- `createRootLogger`, `LogLevel`, `LogFormat` -- `loadPersistedConfig`, `PersistedConfig` -- `DaemonClient`, `DaemonClientConfig`, `ConnectionState`, `DaemonEvent` - -No agent snapshot/timeline/permission/message types are exported. - -## Findings by File - -### `packages/cli/src/commands/agent/run.ts` -Inline types: -- `AgentSnapshot` (id/provider/cwd/createdAt/status/title) - -Recommended server type: -- `AgentSnapshotPayload` from `packages/server/src/shared/messages.ts` (daemon client returns this shape). **Not exported** from `@getpaseo/server` today. - -Notes: -- `AgentRunResult` is CLI output; no server type expected. - ---- - -### `packages/cli/src/commands/agent/ps.ts` -Inline types: -- `AgentSnapshot` (id/provider/cwd/createdAt/status/title/archivedAt?) - -Recommended server type: -- `AgentSnapshotPayload` (includes `archivedAt` and full snapshot fields). **Not exported**. - -Notes: -- `AgentListItem` is CLI output; no server type expected. - ---- - -### `packages/cli/src/commands/agent/send.ts` -Inline types: -- `AgentSnapshot` (id/provider/cwd/createdAt/status/title) - -Recommended server type: -- `AgentSnapshotPayload`. **Not exported**. - -Notes: -- `AgentSendResult` is CLI output; no server type expected. - ---- - -### `packages/cli/src/commands/agent/inspect.ts` -Inline types: -- `AgentSnapshotLike` (snapshot fields + `lastUsage`, `capabilities`, `availableModes`, `pendingPermissions`) - -Recommended server types: -- `AgentSnapshotPayload` (overall snapshot shape). **Not exported**. -- `AgentUsage` for `lastUsage`. **Not exported** (in `packages/server/src/server/agent/agent-sdk-types.ts`). -- `AgentCapabilityFlags` for `capabilities`. **Not exported**. -- `AgentMode` for `availableModes`. **Not exported**. -- `AgentPermissionRequest` for `pendingPermissions`. **Not exported**. - -Notes: -- `pendingPermissions` uses `{ id, tool?: string }` but server type is `AgentPermissionRequest` with `{ name, kind, ... }`; current CLI projection is lossy and field names don’t match (`tool` vs `name`). -- `AgentInspect` and `InspectRow` are CLI output types. - ---- - -### `packages/cli/src/commands/agent/logs.ts` -Inline types: -- `AgentStreamSnapshotMessage` -- `AgentStreamMessage` -- Timeline item shape in `formatTimelineItem` and `extractTimelineFrom*` helpers (`{ type: string; ... }`) - -Recommended server types: -- `AgentStreamSnapshotMessage` from `packages/server/src/shared/messages.ts`. **Not exported**. -- `AgentStreamMessage` from `packages/server/src/shared/messages.ts`. **Not exported**. -- `AgentStreamEventPayload` from `packages/server/src/shared/messages.ts` (for `event` typing). **Not exported**. -- `AgentTimelineItem` from `packages/server/src/server/agent/agent-sdk-types.ts` (for timeline item shape). **Not exported**. - -Notes: -- These are WebSocket message types; they should come from shared message definitions to avoid drift. -- `LogEntry` is CLI output. - ---- - -### `packages/cli/src/commands/agent/mode.ts` -Inline types: -- `ModeListItem` (id/label/description) -- `SetModeResult` (agentId/mode) - -Recommended server type: -- `ModeListItem` duplicates the shape of `AgentMode` (id/label/description) from `packages/server/src/server/agent/agent-sdk-types.ts`. **Not exported**. - -Notes: -- `SetModeResult` is CLI output. - ---- - -### `packages/cli/src/commands/daemon/start.ts` -Inline types: -- `StartOptions` (CLI flags) - -Server type usage: -- CLI-only; no server type expected. - ---- - -### `packages/cli/src/commands/daemon/status.ts` -Inline types: -- `DaemonStatus` -- `StatusRow` - -Server type usage: -- CLI-only; no server type expected. - ---- - -### `packages/cli/src/commands/daemon/restart.ts` -Inline types: -- `RestartResult` - -Server type usage: -- CLI-only; no server type expected. - ---- - -### `packages/cli/src/commands/daemon/stop.ts` -Inline types: -- `StopResult` - -Server type usage: -- CLI-only; no server type expected. - -## Gaps in `@getpaseo/server` Exports (needed for CLI cleanup) -To replace inline types in CLI commands, `@getpaseo/server` would need to export (directly or re-export): -- From `packages/server/src/shared/messages.ts`: - - `AgentSnapshotPayload` - - `AgentStreamEventPayload` - - `AgentStreamMessage` - - `AgentStreamSnapshotMessage` - - (optionally) `AgentStateMessage`, `SessionStateMessage`, `SessionOutboundMessage` if CLI starts typing daemon event queues more strictly -- From `packages/server/src/server/agent/agent-sdk-types.ts`: - - `AgentMode` - - `AgentUsage` - - `AgentCapabilityFlags` - - `AgentPermissionRequest` - - `AgentTimelineItem` - -## Summary -Primary inline types that should become server imports are the agent snapshot/timeline/message/permission/mode shapes in `agent/*` commands. All are defined in server shared or agent SDK types today but are not exported through `@getpaseo/server`.