Add boulder-state feature for persisting workflow state: - storage.ts: File I/O operations for state persistence - types.ts: State interfaces - Includes test coverage 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
151 lines
3.7 KiB
TypeScript
151 lines
3.7 KiB
TypeScript
/**
|
|
* Boulder State Storage
|
|
*
|
|
* Handles reading/writing boulder.json for active plan tracking.
|
|
*/
|
|
|
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs"
|
|
import { dirname, join, basename } from "node:path"
|
|
import type { BoulderState, PlanProgress } from "./types"
|
|
import { BOULDER_DIR, BOULDER_FILE, PROMETHEUS_PLANS_DIR } from "./constants"
|
|
|
|
export function getBoulderFilePath(directory: string): string {
|
|
return join(directory, BOULDER_DIR, BOULDER_FILE)
|
|
}
|
|
|
|
export function readBoulderState(directory: string): BoulderState | null {
|
|
const filePath = getBoulderFilePath(directory)
|
|
|
|
if (!existsSync(filePath)) {
|
|
return null
|
|
}
|
|
|
|
try {
|
|
const content = readFileSync(filePath, "utf-8")
|
|
return JSON.parse(content) as BoulderState
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export function writeBoulderState(directory: string, state: BoulderState): boolean {
|
|
const filePath = getBoulderFilePath(directory)
|
|
|
|
try {
|
|
const dir = dirname(filePath)
|
|
if (!existsSync(dir)) {
|
|
mkdirSync(dir, { recursive: true })
|
|
}
|
|
|
|
writeFileSync(filePath, JSON.stringify(state, null, 2), "utf-8")
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
export function appendSessionId(directory: string, sessionId: string): BoulderState | null {
|
|
const state = readBoulderState(directory)
|
|
if (!state) return null
|
|
|
|
if (!state.session_ids.includes(sessionId)) {
|
|
state.session_ids.push(sessionId)
|
|
if (writeBoulderState(directory, state)) {
|
|
return state
|
|
}
|
|
}
|
|
|
|
return state
|
|
}
|
|
|
|
export function clearBoulderState(directory: string): boolean {
|
|
const filePath = getBoulderFilePath(directory)
|
|
|
|
try {
|
|
if (existsSync(filePath)) {
|
|
const { unlinkSync } = require("node:fs")
|
|
unlinkSync(filePath)
|
|
}
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find Prometheus plan files for this project.
|
|
* Prometheus stores plans at: {project}/.sisyphus/plans/{name}.md
|
|
*/
|
|
export function findPrometheusPlans(directory: string): string[] {
|
|
const plansDir = join(directory, PROMETHEUS_PLANS_DIR)
|
|
|
|
if (!existsSync(plansDir)) {
|
|
return []
|
|
}
|
|
|
|
try {
|
|
const files = readdirSync(plansDir)
|
|
return files
|
|
.filter((f) => f.endsWith(".md"))
|
|
.map((f) => join(plansDir, f))
|
|
.sort((a, b) => {
|
|
// Sort by modification time, newest first
|
|
const aStat = require("node:fs").statSync(a)
|
|
const bStat = require("node:fs").statSync(b)
|
|
return bStat.mtimeMs - aStat.mtimeMs
|
|
})
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse a plan file and count checkbox progress.
|
|
*/
|
|
export function getPlanProgress(planPath: string): PlanProgress {
|
|
if (!existsSync(planPath)) {
|
|
return { total: 0, completed: 0, isComplete: true }
|
|
}
|
|
|
|
try {
|
|
const content = readFileSync(planPath, "utf-8")
|
|
|
|
// Match markdown checkboxes: - [ ] or - [x] or - [X]
|
|
const uncheckedMatches = content.match(/^[-*]\s*\[\s*\]/gm) || []
|
|
const checkedMatches = content.match(/^[-*]\s*\[[xX]\]/gm) || []
|
|
|
|
const total = uncheckedMatches.length + checkedMatches.length
|
|
const completed = checkedMatches.length
|
|
|
|
return {
|
|
total,
|
|
completed,
|
|
isComplete: total === 0 || completed === total,
|
|
}
|
|
} catch {
|
|
return { total: 0, completed: 0, isComplete: true }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract plan name from file path.
|
|
*/
|
|
export function getPlanName(planPath: string): string {
|
|
return basename(planPath, ".md")
|
|
}
|
|
|
|
/**
|
|
* Create a new boulder state for a plan.
|
|
*/
|
|
export function createBoulderState(
|
|
planPath: string,
|
|
sessionId: string
|
|
): BoulderState {
|
|
return {
|
|
active_plan: planPath,
|
|
started_at: new Date().toISOString(),
|
|
session_ids: [sessionId],
|
|
plan_name: getPlanName(planPath),
|
|
}
|
|
}
|