chore: remove packages/cli/docs

This commit is contained in:
Mohamed Boudra
2026-02-05 14:35:55 +07:00
parent 8d1d7b7603
commit 8f9100f6a9
2 changed files with 0 additions and 699 deletions

View File

@@ -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<T> │
└─────────────────────────────────┬───────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Output Renderer │
│ │
│ CommandResult<T> + 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<T>` that contains structured data plus metadata for formatting:
```typescript
interface CommandResult<T> {
/** The structured data to render */
data: T
/** Schema describing how to render this data */
schema: OutputSchema<T>
}
interface OutputSchema<T> {
/** Field to use for quiet mode (--quiet outputs just this) */
idField: keyof T | ((item: T) => string)
/** Column definitions for table output */
columns: ColumnDef<T>[]
/** Optional: transform data before JSON/YAML output */
serialize?: (data: T) => unknown
}
interface ColumnDef<T> {
/** 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 <id>`)
interface SingleResult<T> extends CommandResult<T> {
type: 'single'
data: T
}
// For commands returning a list (e.g., `agent list`)
interface ListResult<T> extends CommandResult<T[]> {
type: 'list'
data: T[]
}
// Union type for command handlers
type AnyCommandResult<T> = SingleResult<T> | ListResult<T>
```
## 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<AgentListItem> = {
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<ListResult<AgentListItem>> {
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<T>(result: ListResult<T>, 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<T>(result: AnyCommandResult<T>, 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<T>(result: AnyCommandResult<T>, 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<T>(result: AnyCommandResult<T>, 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<T> {
type: 'stream'
schema: OutputSchema<T>
/** Async iterator yielding items as they arrive */
stream: AsyncIterable<T>
}
```
### Streaming Renderer
```typescript
async function renderStream<T>(
result: StreamingResult<T>,
options: OutputOptions,
write: (chunk: string) => void
): Promise<void> {
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<AgentListItem> = {
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 <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<T>(
handler: (options: CommandOptions) => Promise<AnyCommandResult<T>>
) {
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.

View File

@@ -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 dont 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`.