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