* test: Bootstrapped sdk/ as TypeScript ESM package with full GSD-1 PLAN.… - "sdk/package.json" - "sdk/tsconfig.json" - "sdk/vitest.config.ts" - "sdk/src/types.ts" - "sdk/src/plan-parser.ts" - "sdk/src/plan-parser.test.ts" GSD-Task: S01/T01 * test: Implemented config reader and gsd-tools bridge with 25 unit tests… - "sdk/src/config.ts" - "sdk/src/config.test.ts" - "sdk/src/gsd-tools.ts" - "sdk/src/gsd-tools.test.ts" GSD-Task: S01/T02 * test: Built prompt-builder, session-runner, and GSD class — 85 total un… - "sdk/src/prompt-builder.ts" - "sdk/src/prompt-builder.test.ts" - "sdk/src/session-runner.ts" - "sdk/src/index.ts" - "sdk/src/types.ts" GSD-Task: S01/T03 * test: Created E2E integration test with fixtures proving full SDK pipel… - "sdk/src/e2e.integration.test.ts" - "sdk/test-fixtures/sample-plan.md" - "sdk/test-fixtures/.planning/config.json" - "sdk/test-fixtures/.planning/STATE.md" - "vitest.config.ts" - "tsconfig.json" GSD-Task: S01/T04 * test: Added PhaseType/GSDEventType enums, 16-variant GSDEvent union, GS… - "sdk/src/types.ts" - "sdk/src/event-stream.ts" - "sdk/src/logger.ts" - "sdk/src/event-stream.test.ts" - "sdk/src/logger.test.ts" GSD-Task: S02/T01 * test: Built ContextEngine for phase-aware context file resolution, getT… - "sdk/src/context-engine.ts" - "sdk/src/tool-scoping.ts" - "sdk/src/phase-prompt.ts" - "sdk/src/context-engine.test.ts" - "sdk/src/tool-scoping.test.ts" - "sdk/src/phase-prompt.test.ts" GSD-Task: S02/T02 * test: Wired event stream into session runner, added onEvent()/addTransp… - "sdk/src/session-runner.ts" - "sdk/src/index.ts" - "sdk/src/e2e.integration.test.ts" GSD-Task: S02/T03 * feat: Added PhaseStepType enum, PhaseOpInfo interface, phase lifecycle… - "sdk/src/types.ts" - "sdk/src/gsd-tools.ts" - "sdk/src/session-runner.ts" - "sdk/src/index.ts" - "sdk/src/phase-runner-types.test.ts" GSD-Task: S03/T01 * test: Implemented PhaseRunner state machine with 39 unit tests covering… - "sdk/src/phase-runner.ts" - "sdk/src/phase-runner.test.ts" GSD-Task: S03/T02 * test: Wired PhaseRunner into GSD.runPhase() public API with full re-exp… - "sdk/src/index.ts" - "sdk/src/phase-runner.integration.test.ts" - "sdk/src/phase-runner.ts" GSD-Task: S03/T03 * test: Expanded runVerifyStep with full gap closure cycle (plan → execut… - "sdk/src/types.ts" - "sdk/src/phase-runner.ts" - "sdk/src/phase-runner.test.ts" GSD-Task: S04/T02 * fix: Added 3 integration tests proving phasePlanIndex returns correct t… - "sdk/src/phase-runner.integration.test.ts" - "sdk/src/index.ts" GSD-Task: S04/T03 * test: Add milestone-level types, typed roadmapAnalyze(), GSD.run() orch… - "sdk/src/types.ts" - "sdk/src/gsd-tools.ts" - "sdk/src/index.ts" - "sdk/src/milestone-runner.test.ts" GSD-Task: S05/T01 * test: Added CLITransport (structured stdout log lines) and WSTransport… - "sdk/src/cli-transport.ts" - "sdk/src/cli-transport.test.ts" - "sdk/src/ws-transport.ts" - "sdk/src/ws-transport.test.ts" - "sdk/src/index.ts" - "sdk/package.json" GSD-Task: S05/T02 * test: Added gsd-sdk CLI entry point with argument parsing, bin field, p… - "sdk/src/cli.ts" - "sdk/src/cli.test.ts" - "sdk/package.json" GSD-Task: S05/T03 * feat: Add InitNewProjectInfo type, initNewProject()/configSet() GSDTool… - "sdk/src/types.ts" - "sdk/src/gsd-tools.ts" - "sdk/src/cli.ts" - "sdk/src/cli.test.ts" - "sdk/src/gsd-tools.test.ts" GSD-Task: S01/T01 * chore: Created InitRunner orchestrator with setup → config → PROJECT.md… - "sdk/src/init-runner.ts" - "sdk/src/types.ts" - "sdk/src/index.ts" GSD-Task: S01/T02 * test: Wired InitRunner into CLI main() for full gsd-sdk init dispatch a… - "sdk/src/cli.ts" - "sdk/src/init-runner.test.ts" - "sdk/src/cli.test.ts" GSD-Task: S01/T03 * test: Add PlanCheck step, AI self-discuss, and retryOnce wrapper to Pha… - "sdk/src/types.ts" - "sdk/src/phase-runner.ts" - "sdk/src/session-runner.ts" - "sdk/src/phase-runner.test.ts" - "sdk/src/phase-runner-types.test.ts" GSD-Task: S02/T01 * feat: Rewrite CLITransport with ANSI colors, phase banners, spawn indic… - "sdk/src/cli-transport.ts" - "sdk/src/cli-transport.test.ts" GSD-Task: S02/T02 * test: Add `gsd-sdk auto` command with autoMode config override, USAGE t… - "sdk/src/cli.ts" - "sdk/src/cli.test.ts" - "sdk/src/index.ts" - "sdk/src/types.ts" GSD-Task: S02/T03 * fix: CLI shebang + gsd-tools non-JSON output handling Three bugs found during first real gsd-sdk run: 1. cli.ts shebang was commented out — shell executed JS as bash, triggering ImageMagick's import command instead of Node 2. configSet() called exec() which JSON.parse()d the output, but gsd-tools config-set returns 'key=value' text, not JSON. Added execRaw() method for commands that return plain text. 3. Same JSON parse bug affected commit() (returns git SHA), stateLoad(), verifySummary(), initExecutePhase(), stateBeginPhase(), and phaseComplete(). All switched to execRaw(). Tests updated to match real gsd-tools output format (plain text instead of mocked JSON). 376/376 tests pass.
131 lines
5.4 KiB
TypeScript
131 lines
5.4 KiB
TypeScript
/**
|
|
* CLI Transport — renders GSD events as rich ANSI-colored output to a Writable stream.
|
|
*
|
|
* Implements TransportHandler with colored banners, step indicators, spawn markers,
|
|
* and running cost totals. No external dependencies — ANSI codes are inline constants.
|
|
*/
|
|
|
|
import type { Writable } from 'node:stream';
|
|
import { GSDEventType, type GSDEvent, type TransportHandler } from './types.js';
|
|
|
|
// ─── ANSI escape constants (no dependency per D021) ──────────────────────────
|
|
|
|
const BOLD = '\x1b[1m';
|
|
const RESET = '\x1b[0m';
|
|
const GREEN = '\x1b[32m';
|
|
const RED = '\x1b[31m';
|
|
const YELLOW = '\x1b[33m';
|
|
const CYAN = '\x1b[36m';
|
|
const DIM = '\x1b[90m';
|
|
|
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
|
|
/** Extract HH:MM:SS from an ISO-8601 timestamp. */
|
|
function formatTime(ts: string): string {
|
|
try {
|
|
const d = new Date(ts);
|
|
if (Number.isNaN(d.getTime())) return '??:??:??';
|
|
return d.toISOString().slice(11, 19);
|
|
} catch {
|
|
return '??:??:??';
|
|
}
|
|
}
|
|
|
|
/** Truncate a string to `max` characters, appending '…' if truncated. */
|
|
function truncate(s: string, max: number): string {
|
|
if (s.length <= max) return s;
|
|
return s.slice(0, max - 1) + '…';
|
|
}
|
|
|
|
/** Format a USD amount. */
|
|
function usd(n: number): string {
|
|
return `$${n.toFixed(2)}`;
|
|
}
|
|
|
|
// ─── CLITransport ────────────────────────────────────────────────────────────
|
|
|
|
export class CLITransport implements TransportHandler {
|
|
private readonly out: Writable;
|
|
private runningCostUsd = 0;
|
|
|
|
constructor(out?: Writable) {
|
|
this.out = out ?? process.stdout;
|
|
}
|
|
|
|
/** Format and write a GSD event as a rich ANSI-colored line. Never throws. */
|
|
onEvent(event: GSDEvent): void {
|
|
try {
|
|
const line = this.formatEvent(event);
|
|
this.out.write(line + '\n');
|
|
} catch {
|
|
// TransportHandler contract: onEvent must never throw
|
|
}
|
|
}
|
|
|
|
/** No-op — stdout doesn't need cleanup. */
|
|
close(): void {
|
|
// Nothing to clean up
|
|
}
|
|
|
|
// ─── Private formatting ────────────────────────────────────────────
|
|
|
|
private formatEvent(event: GSDEvent): string {
|
|
const time = formatTime(event.timestamp);
|
|
|
|
switch (event.type) {
|
|
case GSDEventType.SessionInit:
|
|
return `[${time}] [INIT] Session started — model: ${event.model}, tools: ${event.tools.length}, cwd: ${event.cwd}`;
|
|
|
|
case GSDEventType.SessionComplete:
|
|
return `[${time}] ${GREEN}✓ Session complete — cost: ${usd(event.totalCostUsd)}, turns: ${event.numTurns}, duration: ${(event.durationMs / 1000).toFixed(1)}s${RESET}`;
|
|
|
|
case GSDEventType.SessionError:
|
|
return `[${time}] ${RED}✗ Session failed — subtype: ${event.errorSubtype}, errors: [${event.errors.join(', ')}]${RESET}`;
|
|
|
|
case GSDEventType.ToolCall:
|
|
return `[${time}] [TOOL] ${event.toolName}(${truncate(JSON.stringify(event.input), 80)})`;
|
|
|
|
case GSDEventType.PhaseStart:
|
|
return `${BOLD}${CYAN}━━━ GSD ► PHASE ${event.phaseNumber}: ${event.phaseName} ━━━${RESET}`;
|
|
|
|
case GSDEventType.PhaseComplete:
|
|
return `[${time}] [PHASE] Phase ${event.phaseNumber} complete — success: ${event.success}, cost: ${usd(event.totalCostUsd)}, running: ${usd(this.runningCostUsd)}`;
|
|
|
|
case GSDEventType.PhaseStepStart:
|
|
return `${CYAN}◆ ${event.step}${RESET}`;
|
|
|
|
case GSDEventType.PhaseStepComplete:
|
|
return event.success
|
|
? `${GREEN}✓ ${event.step}${RESET} ${DIM}${event.durationMs}ms${RESET}`
|
|
: `${RED}✗ ${event.step}${RESET} ${DIM}${event.durationMs}ms${RESET}`;
|
|
|
|
case GSDEventType.WaveStart:
|
|
return `${YELLOW}⟫ Wave ${event.waveNumber} (${event.planCount} plans)${RESET}`;
|
|
|
|
case GSDEventType.WaveComplete:
|
|
return `[${time}] [WAVE] Wave ${event.waveNumber} complete — ${GREEN}${event.successCount} success${RESET}, ${RED}${event.failureCount} failed${RESET}, ${event.durationMs}ms`;
|
|
|
|
case GSDEventType.CostUpdate: {
|
|
this.runningCostUsd += event.sessionCostUsd;
|
|
return `${DIM}[${time}] Cost: session ${usd(event.sessionCostUsd)}, running ${usd(this.runningCostUsd)}${RESET}`;
|
|
}
|
|
|
|
case GSDEventType.MilestoneStart:
|
|
return `${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}\n${BOLD} GSD Milestone — ${event.phaseCount} phases${RESET}\n${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`;
|
|
|
|
case GSDEventType.MilestoneComplete:
|
|
return `${BOLD}━━━ Milestone complete — success: ${event.success}, cost: ${usd(event.totalCostUsd)}, running: ${usd(this.runningCostUsd)} ━━━${RESET}`;
|
|
|
|
case GSDEventType.AssistantText:
|
|
return `${DIM}[${time}] ${truncate(event.text, 200)}${RESET}`;
|
|
|
|
case GSDEventType.InitResearchSpawn:
|
|
return `${CYAN}◆ Spawning ${event.sessionCount} researchers...${RESET}`;
|
|
|
|
// Generic fallback for event types without specific formatting
|
|
default:
|
|
return `[${time}] [EVENT] ${event.type}`;
|
|
}
|
|
}
|
|
}
|