singularity-forge/src/headless-status.ts
Mikael Hugo 362af3d6a4
Some checks failed
CI / detect-changes (push) Has been cancelled
CI / docs-check (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / build (push) Has been cancelled
CI / integration-tests (push) Has been cancelled
CI / windows-portability (push) Has been cancelled
CI / rtk-portability (linux, blacksmith-4vcpu-ubuntu-2404) (push) Has been cancelled
CI / rtk-portability (macos, macos-15) (push) Has been cancelled
CI / rtk-portability (windows, blacksmith-4vcpu-windows-2025) (push) Has been cancelled
fix(headless): bypass rpc for status
2026-05-15 17:32:21 +02:00

108 lines
3.4 KiB
TypeScript

/**
* headless-status.ts — direct `sf headless status` implementation.
*
* Purpose: keep the headless status machine surface read-only and
* TTY-independent instead of routing through the interactive `/status` overlay
* command or the long-lived RPC/v2 session handshake.
*/
import { buildQuerySnapshot, type QuerySnapshot } from "./headless-query.js";
export interface HeadlessStatusResult {
exitCode: number;
data?: QuerySnapshot;
}
/**
* Render a compact text status from the query snapshot.
*
* Purpose: provide the same operator value as `/status` in terminals where no
* interactive overlay can be displayed.
*
* Consumer: handleHeadlessStatus for text-mode `sf headless status`.
*/
export function formatHeadlessStatus(snapshot: QuerySnapshot): string {
const { next, runtime, uokDiagnostics, schedule } = snapshot;
const state = snapshot.state as any;
const lines = ["SF Status", ""];
lines.push(`Phase: ${state.phase}`);
if (state.activeMilestone) {
lines.push(
`Active milestone: ${state.activeMilestone.id} - ${state.activeMilestone.title}`,
);
}
if (state.activeSlice) {
lines.push(
`Active slice: ${state.activeSlice.id} - ${state.activeSlice.title}`,
);
}
if (state.activeTask) {
lines.push(
`Active task: ${state.activeTask.id} - ${state.activeTask.title}`,
);
}
const progress = state.progress;
if (progress) {
const parts = [
`milestones ${progress.milestones.done}/${progress.milestones.total}`,
];
if (progress.slices)
parts.push(`slices ${progress.slices.done}/${progress.slices.total}`);
if (progress.tasks)
parts.push(`tasks ${progress.tasks.done}/${progress.tasks.total}`);
lines.push(`Progress: ${parts.join(", ")}`);
}
if (state.nextAction) lines.push(`Next: ${state.nextAction}`);
if (state.blockers.length > 0)
lines.push(`Blockers: ${state.blockers.join("; ")}`);
lines.push("");
lines.push(
`Dispatch: ${next.action}${next.unitType ? ` ${next.unitType}` : ""}${next.unitId ? ` ${next.unitId}` : ""}${next.reason ? ` - ${next.reason}` : ""}`,
);
if (uokDiagnostics) {
lines.push(
`UOK: ${uokDiagnostics.verdict ?? "unknown"} (${uokDiagnostics.classification ?? "unknown"})`,
);
}
if (runtime.units.length > 0) {
lines.push("");
lines.push("Runtime units:");
for (const unit of runtime.units.slice(0, 8)) {
lines.push(` ${unit.unitType} ${unit.unitId}: ${unit.status}`);
}
}
if (schedule) {
lines.push("");
lines.push(
`Schedule: ${schedule.pending_count} pending, ${schedule.overdue_count} overdue`,
);
}
if (state.registry.length > 0) {
lines.push("");
lines.push("Milestones:");
for (const milestone of state.registry) {
lines.push(` ${milestone.id}: ${milestone.title} (${milestone.status})`);
}
}
return lines.join("\n");
}
/**
* Handle `sf headless status` without spawning the interactive RPC child.
*
* Purpose: avoid the long-standing v2 init timeout for a command whose answer
* is fully available from DB-backed project state.
*
* Consumer: runHeadlessOnce direct-command bypass.
*/
export async function handleHeadlessStatus(
basePath: string,
options: { json?: boolean } = {},
): Promise<HeadlessStatusResult> {
const snapshot = await buildQuerySnapshot(basePath);
if (options.json) {
process.stdout.write(JSON.stringify(snapshot) + "\n");
} else {
process.stdout.write(formatHeadlessStatus(snapshot) + "\n");
}
return { exitCode: 0, data: snapshot };
}