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
108 lines
3.4 KiB
TypeScript
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 };
|
|
}
|