Files
oh-my-opencode/packages/boulder-state/src/storage/read-state.ts
T
2026-05-21 12:49:33 +09:00

170 lines
5.1 KiB
TypeScript

import { existsSync, readFileSync } from "node:fs"
import type { BoulderState, BoulderWorkResumeOption, BoulderWorkState, TaskSessionState } from "../types"
import { getBoulderFilePath, resolveBoulderPlanPathForWork } from "./path"
import { getPlanProgress } from "./plan-progress"
import { buildWorkFromMirror, isValidWorkStatus, parseIsoToMs, projectWorkToMirror, selectMirrorWork } from "./shared"
export function readBoulderState(directory: string): BoulderState | null {
const filePath = getBoulderFilePath(directory)
if (!existsSync(filePath)) {
return null
}
try {
const content = readFileSync(filePath, "utf-8")
const parsed = JSON.parse(content)
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
return null
}
normalizeState(parsed)
const state = parsed as BoulderState
const mirrorWork = selectMirrorWork(state)
if (mirrorWork) {
state.active_work_id = mirrorWork.work_id
projectWorkToMirror(state, mirrorWork)
}
return state
} catch {
return null
}
}
function normalizeState(state: Record<string, unknown>): void {
const sessionIds = Array.isArray(state.session_ids) ? state.session_ids : []
state.session_ids = sessionIds
const sessionOrigins = state.session_origins && typeof state.session_origins === "object" && !Array.isArray(state.session_origins)
? (state.session_origins as Record<string, unknown>)
: {}
state.session_origins = sessionOrigins
if (sessionIds.length === 1) {
const soleSessionId = sessionIds[0]
if (
typeof soleSessionId === "string"
&& sessionOrigins[soleSessionId] !== "appended"
&& sessionOrigins[soleSessionId] !== "direct"
) {
sessionOrigins[soleSessionId] = "direct"
}
}
if (!state.task_sessions || typeof state.task_sessions !== "object" || Array.isArray(state.task_sessions)) {
state.task_sessions = {}
}
}
export function getBoulderWorks(state: BoulderState): BoulderWorkState[] {
if (state.works && typeof state.works === "object") {
return Object.values(state.works)
}
if (!state.active_plan || !state.plan_name || !state.started_at) {
return []
}
return [buildWorkFromMirror(state)]
}
export function getActiveWorks(directory: string): BoulderWorkState[] {
const state = readBoulderState(directory)
if (!state) {
return []
}
return getBoulderWorks(state).filter((work) => work.status !== "completed" && work.status !== "abandoned")
}
export function getWorkById(directory: string, workId: string): BoulderWorkState | null {
const state = readBoulderState(directory)
if (!state) {
return null
}
return getBoulderWorks(state).find((work) => work.work_id === workId) ?? null
}
export function getWorkByPlanName(
directory: string,
planName: string,
options?: { worktreePath?: string },
): BoulderWorkState | null {
const state = readBoulderState(directory)
if (!state) {
return null
}
const worktreePath = options?.worktreePath
return getBoulderWorks(state).find((work) => {
if (work.plan_name !== planName) {
return false
}
return worktreePath ? work.worktree_path === worktreePath : true
}) ?? null
}
export function getWorkForSession(directory: string, sessionId: string): BoulderWorkState | null {
const state = readBoulderState(directory)
if (!state) {
return null
}
const works = getBoulderWorks(state)
.filter((work) => work.session_ids.includes(sessionId))
.sort((left, right) => (parseIsoToMs(right.updated_at ?? right.started_at) ?? 0) - (parseIsoToMs(left.updated_at ?? left.started_at) ?? 0))
if (works.length > 0) {
return works[0] ?? null
}
return state.session_ids.includes(sessionId) ? buildWorkFromMirror(state) : null
}
export function getWorkResumeOptions(directory: string): BoulderWorkResumeOption[] {
const state = readBoulderState(directory)
if (!state) {
return []
}
return getBoulderWorks(state)
.filter((work) => work.status !== "completed" && work.status !== "abandoned")
.map((work) => {
const progress = getPlanProgress(resolveBoulderPlanPathForWork(directory, work))
return {
work_id: work.work_id,
plan_name: work.plan_name,
active_plan: work.active_plan,
worktree_path: work.worktree_path,
status: work.status && isValidWorkStatus(work.status) ? work.status : "active",
started_at: work.started_at,
updated_at: work.updated_at ?? work.started_at,
ended_at: work.ended_at,
elapsed_ms: work.elapsed_ms,
session_count: work.session_ids.length,
progress,
is_current_mirror: state.active_work_id === work.work_id,
}
})
}
export function getTaskSessionState(directory: string, taskKey: string): TaskSessionState | null {
const state = readBoulderState(directory)
if (state?.active_work_id) {
const work = state.works?.[state.active_work_id]
const taskSession = work?.task_sessions?.[taskKey]
if (taskSession) {
return taskSession
}
}
if (!state?.task_sessions) {
return null
}
return state.task_sessions[taskKey] ?? null
}