feat(cli/boulder): add types and formatter for boulder subcommand
This commit is contained in:
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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[]
|
||||
}
|
||||
Reference in New Issue
Block a user