diff --git a/src/cli/boulder/formatter.test.ts b/src/cli/boulder/formatter.test.ts new file mode 100644 index 000000000..cd787bf91 --- /dev/null +++ b/src/cli/boulder/formatter.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it } from "bun:test" + +import { formatJsonOutput, formatTextOutput } from "./formatter" +import type { BoulderCliResult } from "./types" + +describe("boulder formatter", () => { + it("renders text output with statuses and progress", () => { + const result: BoulderCliResult = { + works: [ + { + work_id: "w1", + plan_name: "alpha", + active_plan: "/tmp/alpha.md", + status: "active", + started_at: "2026-05-10T00:00:00.000Z", + elapsed_human: "30m 0s", + total_tasks: 2, + completed_tasks: 1, + remaining_tasks: 1, + percentage: 50, + session_count: 2, + current_task: { + task_key: "todo:2", + task_title: "Alpha task", + elapsed_human: "1m 0s", + }, + }, + ], + } + + const textOutput = formatTextOutput(result) + expect(textOutput).toContain("boulder progress") + expect(textOutput).toContain("plan: alpha") + expect(textOutput).toContain("status: active") + expect(textOutput).toContain("progress: 50% (1/2)") + expect(textOutput).toContain("elapsed: 30m 0s") + }) + + it("renders parseable json output", () => { + const result: BoulderCliResult = { + works: [ + { + work_id: "w1", + plan_name: "alpha", + active_plan: "/tmp/alpha.md", + status: "completed", + started_at: "2026-05-10T00:00:00.000Z", + ended_at: "2026-05-10T00:01:00.000Z", + elapsed_ms: 60_000, + total_tasks: 2, + completed_tasks: 2, + remaining_tasks: 0, + percentage: 100, + session_count: 1, + }, + ], + } + + const jsonOutput = formatJsonOutput(result) + expect(JSON.parse(jsonOutput)).toEqual(result) + }) +}) diff --git a/src/cli/boulder/formatter.ts b/src/cli/boulder/formatter.ts new file mode 100644 index 000000000..94af0b603 --- /dev/null +++ b/src/cli/boulder/formatter.ts @@ -0,0 +1,75 @@ +import color from "picocolors" + +import type { BoulderWorkStatus } from "../../features/boulder-state" +import type { BoulderCliResult, BoulderCliWork } from "./types" + +function colorizeStatus(status: BoulderWorkStatus): string { + if (status === "active") { + return color.cyan(status) + } + + if (status === "completed") { + return color.green(status) + } + + if (status === "paused") { + return color.yellow(status) + } + + return color.red(status) +} + +function formatCurrentTask(work: BoulderCliWork): string { + if (!work.current_task) { + return "-" + } + + const elapsed = work.current_task.elapsed_human + ? ` (${work.current_task.elapsed_human})` + : "" + return `${work.current_task.task_title}${elapsed}` +} + +function formatWorkBlock(work: BoulderCliWork): string { + const elapsed = work.elapsed_human ?? "-" + const progress = `${work.percentage}% (${work.completed_tasks}/${work.total_tasks})` + + return [ + `plan: ${work.plan_name}`, + `status: ${colorizeStatus(work.status)}`, + `progress: ${progress}`, + `elapsed: ${elapsed}`, + `sessions: ${work.session_count}`, + `current task: ${formatCurrentTask(work)}`, + ].join("\n") +} + +export function formatTextOutput(result: BoulderCliResult): string { + const separator = color.dim("----------------------------------------") + const blocks = result.works.map((work) => formatWorkBlock(work)) + return ["boulder progress", ...blocks].join(`\n${separator}\n`) +} + +export function formatJsonOutput(result: BoulderCliResult): string { + return JSON.stringify(result, null, 2) +} + +export function formatNoBoulderMessage(isJson: boolean | undefined): string { + if (isJson) { + return JSON.stringify({ + error: "No boulder state found.", + }) + } + + return "No boulder state found." +} + +export function formatReadErrorMessage(isJson: boolean | undefined): string { + if (isJson) { + return JSON.stringify({ + error: "Failed to read boulder state.", + }) + } + + return "Failed to read boulder state." +} diff --git a/src/cli/boulder/types.ts b/src/cli/boulder/types.ts new file mode 100644 index 000000000..adefc72c5 --- /dev/null +++ b/src/cli/boulder/types.ts @@ -0,0 +1,33 @@ +import type { BoulderWorkStatus } from "../../features/boulder-state" + +export interface BoulderOptions { + directory?: string + workId?: string + json?: boolean +} + +export interface BoulderCliWork { + work_id: string + plan_name: string + active_plan: string + worktree_path?: string + status: BoulderWorkStatus + started_at: string + ended_at?: string + elapsed_human?: string + elapsed_ms?: number + total_tasks: number + completed_tasks: number + remaining_tasks: number + percentage: number + session_count: number + current_task?: { + task_key: string + task_title: string + elapsed_human?: string + } +} + +export interface BoulderCliResult { + works: BoulderCliWork[] +}