feat(cli/boulder): add types and formatter for boulder subcommand

This commit is contained in:
YeonGyu-Kim
2026-05-11 13:41:09 +09:00
parent fb2f696b47
commit 30984939eb
3 changed files with 170 additions and 0 deletions
+62
View File
@@ -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)
})
})
+75
View File
@@ -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."
}
+33
View File
@@ -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[]
}