import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs' import { tmpdir } from 'os' import { dirname, join, resolve } from 'path' export type FlowDraftStep = Record export interface FlowCandidate { step: FlowDraftStep summary: string source: string recordedAt: string } export interface FlowSessionState { version: 1 startedAt: string updatedAt: string steps: FlowDraftStep[] candidate: FlowCandidate | null } const FLOW_SESSION_VERSION = 1 as const export function getFlowSessionPath() { return resolve( process.env.RNX_FLOW_SESSION_PATH || join(tmpdir(), 'rnx-flow-session.json'), ) } function writeFlowSession(state: FlowSessionState) { const filepath = getFlowSessionPath() mkdirSync(dirname(filepath), { recursive: true }) writeFileSync(filepath, JSON.stringify(state, null, 2) + '\n') } export function readFlowSession(): FlowSessionState | null { const filepath = getFlowSessionPath() if (!existsSync(filepath)) return null try { const parsed = JSON.parse(readFileSync(filepath, 'utf8')) as Partial if ( parsed.version !== FLOW_SESSION_VERSION || !Array.isArray(parsed.steps) || typeof parsed.startedAt !== 'string' || typeof parsed.updatedAt !== 'string' ) { rmSync(filepath, { force: true }) return null } return { version: FLOW_SESSION_VERSION, startedAt: parsed.startedAt, updatedAt: parsed.updatedAt, steps: parsed.steps as FlowDraftStep[], candidate: parsed.candidate ?? null, } } catch { rmSync(filepath, { force: true }) return null } } export function hasActiveFlowSession() { return readFlowSession() !== null } export function startFlowSession() { const now = new Date().toISOString() const state: FlowSessionState = { version: FLOW_SESSION_VERSION, startedAt: now, updatedAt: now, steps: [], candidate: null, } writeFlowSession(state) return { path: getFlowSessionPath(), state } } export function clearFlowSession() { rmSync(getFlowSessionPath(), { force: true }) } export function finalizeFlowSession() { clearFlowSession() } export function rememberFlowCandidate(input: { step: FlowDraftStep summary: string source: string }) { const state = readFlowSession() if (!state) return { active: false as const } // a pending candidate that was never `maestro keep`'d is silently dropped // when a new action arrives — that's the "skip mistakes" model. surface // the replaced action so the caller can warn (a kept action is never // lost; an unkept one is). const replaced = state.candidate const next: FlowSessionState = { ...state, updatedAt: new Date().toISOString(), candidate: { step: input.step, summary: input.summary, source: input.source, recordedAt: new Date().toISOString(), }, } writeFlowSession(next) return { active: true as const, candidate: next.candidate, replaced } } export function keepFlowCandidate() { const state = readFlowSession() if (!state) return { active: false as const, kept: false as const, reason: 'no-session' } if (!state.candidate) { return { active: true as const, kept: false as const, reason: 'no-candidate' } } const keptCandidate = state.candidate const next: FlowSessionState = { ...state, updatedAt: new Date().toISOString(), steps: [...state.steps, keptCandidate.step], candidate: null, } writeFlowSession(next) return { active: true as const, kept: true as const, candidate: keptCandidate, stepCount: next.steps.length, } }