2026-04-30 21:55:17 +02:00
|
|
|
/**
|
|
|
|
|
* errors.ts — Structured error types for consistent, actionable CLI diagnostics.
|
|
|
|
|
*
|
|
|
|
|
* Purpose: every error path in the CLI and headless orchestrator should be
|
|
|
|
|
* able to emit context that helps users (and future debuggers) understand
|
|
|
|
|
* *what* failed, *where*, and *what to try next* — without depending on
|
|
|
|
|
* heavy error-handling libraries.
|
|
|
|
|
*
|
|
|
|
|
* Consumer: cli.ts, headless.ts, and any extension that surfaces user-facing
|
|
|
|
|
* failures. The types are plain data so they serialize cleanly to stderr,
|
|
|
|
|
* JSON batch output, and trace spans.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Core structured error type
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A user-facing or machine-readable error record with rich context.
|
|
|
|
|
*
|
2026-05-01 21:36:32 +02:00
|
|
|
* Purpose: provide a single, serializable shape that every error path can
|
|
|
|
|
* enrich incrementally (operation, file, guidance, retry hint) so that
|
|
|
|
|
* consumers — stderr printers, JSON exporters, and trace emitters — receive
|
|
|
|
|
* enough context to produce actionable output without guessing.
|
|
|
|
|
*
|
|
|
|
|
* Consumer: cli.ts catch blocks, headless.ts event handlers, trace span
|
|
|
|
|
* error events, and any extension that surfaces user-facing failures.
|
2026-04-30 21:55:17 +02:00
|
|
|
*/
|
|
|
|
|
export interface StructuredError {
|
|
|
|
|
/** Human-readable description of what went wrong. */
|
|
|
|
|
message: string;
|
|
|
|
|
|
|
|
|
|
/** The high-level operation that was in progress when the error occurred,
|
|
|
|
|
* e.g. "graph build", "session resume", "model validation". */
|
|
|
|
|
operation?: string;
|
|
|
|
|
|
|
|
|
|
/** The file path most relevant to the failure (the file being read,
|
|
|
|
|
* written, or expected). */
|
|
|
|
|
file?: string;
|
|
|
|
|
|
|
|
|
|
/** The line number inside `file` if known (e.g. from a parser error). */
|
|
|
|
|
line?: number;
|
|
|
|
|
|
|
|
|
|
/** Actionable guidance for the user — what to check or try next. */
|
|
|
|
|
guidance?: string;
|
|
|
|
|
|
|
|
|
|
/** Whether retrying the same operation (with the same inputs) might
|
|
|
|
|
* succeed, e.g. transient network failures. */
|
|
|
|
|
retry?: boolean;
|
|
|
|
|
|
|
|
|
|
/** The underlying cause, if this error wraps another. Kept as `unknown`
|
|
|
|
|
* so callers aren't forced to coerce to Error. */
|
|
|
|
|
cause?: unknown;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Convenience constructors
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create a {@link StructuredError} from a message and optional context.
|
|
|
|
|
*
|
|
|
|
|
* Purpose: reduce boilerplate at catch sites where we want to enrich a raw
|
|
|
|
|
* exception with operation/file context before logging or returning it.
|
|
|
|
|
*
|
|
|
|
|
* Consumer: cli.ts catch blocks, headless.ts event handlers.
|
|
|
|
|
*/
|
|
|
|
|
export function error(
|
|
|
|
|
message: string,
|
|
|
|
|
ctx?: Omit<StructuredError, "message">,
|
|
|
|
|
): StructuredError {
|
|
|
|
|
return { message, ...ctx };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Formatters
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Format a {@link StructuredError} as plain text suitable for stderr.
|
|
|
|
|
*
|
|
|
|
|
* Output shape (fields omitted when undefined):
|
|
|
|
|
* [sf] Error: <message>
|
|
|
|
|
* Operation: <operation>
|
|
|
|
|
* File: <file>:<line>
|
|
|
|
|
* Guidance: <guidance>
|
|
|
|
|
* Retryable: yes|no
|
2026-05-01 21:36:32 +02:00
|
|
|
*
|
|
|
|
|
* Purpose: give users a consistent, scannable error layout in the terminal
|
|
|
|
|
* so they can spot the file, guidance, and retry hint without parsing JSON.
|
|
|
|
|
*
|
|
|
|
|
* Consumer: cli.ts before writing to process.stderr.
|
2026-04-30 21:55:17 +02:00
|
|
|
*/
|
|
|
|
|
export function formatStructuredError(
|
|
|
|
|
err: StructuredError,
|
|
|
|
|
prefix = "[sf]",
|
|
|
|
|
): string {
|
|
|
|
|
const parts: string[] = [`${prefix} Error: ${err.message}`];
|
|
|
|
|
|
|
|
|
|
if (err.operation) {
|
|
|
|
|
parts.push(` Operation: ${err.operation}`);
|
|
|
|
|
}
|
|
|
|
|
if (err.file) {
|
|
|
|
|
const line = err.line !== undefined ? `:${err.line}` : "";
|
|
|
|
|
parts.push(` File: ${err.file}${line}`);
|
|
|
|
|
}
|
|
|
|
|
if (err.guidance) {
|
|
|
|
|
parts.push(` Guidance: ${err.guidance}`);
|
|
|
|
|
}
|
|
|
|
|
if (err.retry !== undefined) {
|
|
|
|
|
parts.push(` Retryable: ${err.retry ? "yes" : "no"}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return parts.join("\n") + "\n";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Format a {@link StructuredError} as a JSON object.
|
|
|
|
|
*
|
|
|
|
|
* Purpose: headless --output-format json mode can embed structured errors
|
|
|
|
|
* in the result payload instead of interleaving free-form text on stderr.
|
2026-05-01 21:36:32 +02:00
|
|
|
*
|
|
|
|
|
* Consumer: headless.ts when emitting batch JSON results.
|
2026-04-30 21:55:17 +02:00
|
|
|
*/
|
|
|
|
|
export function errorToJson(err: StructuredError): Record<string, unknown> {
|
|
|
|
|
const out: Record<string, unknown> = { message: err.message };
|
|
|
|
|
if (err.operation !== undefined) out.operation = err.operation;
|
|
|
|
|
if (err.file !== undefined) out.file = err.file;
|
|
|
|
|
if (err.line !== undefined) out.line = err.line;
|
|
|
|
|
if (err.guidance !== undefined) out.guidance = err.guidance;
|
|
|
|
|
if (err.retry !== undefined) out.retry = err.retry;
|
|
|
|
|
if (err.cause !== undefined) {
|
|
|
|
|
out.cause =
|
|
|
|
|
err.cause instanceof Error
|
|
|
|
|
? { message: err.cause.message, name: err.cause.name }
|
|
|
|
|
: String(err.cause);
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Predicates
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Narrow an `unknown` value to a {@link StructuredError}.
|
|
|
|
|
*
|
|
|
|
|
* Purpose: safe type guards at catch boundaries where the thrown value may
|
|
|
|
|
* be a plain Error, a StructuredError, or something else entirely.
|
2026-05-01 21:36:32 +02:00
|
|
|
*
|
|
|
|
|
* Consumer: headless.ts before deciding whether to call formatStructuredError
|
|
|
|
|
* or fall back to String(err).
|
2026-04-30 21:55:17 +02:00
|
|
|
*/
|
|
|
|
|
export function isStructuredError(val: unknown): val is StructuredError {
|
|
|
|
|
return (
|
|
|
|
|
typeof val === "object" &&
|
|
|
|
|
val !== null &&
|
|
|
|
|
"message" in val &&
|
|
|
|
|
typeof (val as Record<string, unknown>).message === "string"
|
|
|
|
|
);
|
|
|
|
|
}
|