import { AdminApiError, AdminApiSchemaError, } from "#shared/admin-api-client.ts"; /** The failure kinds the CLI reports. Each kind maps to one exit code. */ export type CliErrorKind = | "usage" | "validation" | "not_allowed" | "not_found" | "server" | "io"; /** The exit code each kind answers with. */ export const EXIT_CODES: Record = { io: 6, not_allowed: 4, not_found: 5, server: 6, usage: 2, validation: 3, }; /** One problem with one body field. */ export type FieldProblem = { field: string; message: string }; /** The one error type the CLI raises. The message names the problem and the * fix; `status` carries the HTTP status when the server answered, and * `fields` names per-field problems when the body failed a check. */ export class CliError extends Error { readonly fields: FieldProblem[] | null; readonly kind: CliErrorKind; readonly status: number | undefined; constructor( kind: CliErrorKind, message: string, fields: FieldProblem[] | null = null, status?: number, ) { super(message); this.fields = fields; this.kind = kind; this.status = status; } } /** The JSON object stderr carries for one failure. An absent status means no * server answered, and the contract renders it as null. */ export const errorJson = (error: CliError): unknown => ({ error: { fields: error.fields, kind: error.kind, message: error.message, status: error.status ?? null, }, }); /** The exit code one mapped error answers with. */ export const exitCodeFor = (error: CliError): number => EXIT_CODES[error.kind]; /** The stderr line and exit code one failure reports: the contract's JSON * envelope with its trailing newline. `fallbackKind` reaches the failures * no other rule names. */ export const toErrorResult = ( error: unknown, fallbackKind: CliErrorKind = "server", ): { code: number; err: string } => { const cli = cliErrorFromUnknown(error, fallbackKind); return { code: exitCodeFor(cli), err: `${JSON.stringify(errorJson(cli))}\n` }; }; /** Map one HTTP status to the CLI error kind the contract names. */ const kindForStatus = (status: number | undefined): CliErrorKind => { if (status === 400) return "validation"; if (status === 401 || status === 403) return "not_allowed"; if (status === 404) return "not_found"; return "server"; }; /** The server's own error text when its body names one. */ const serverMessage = (error: AdminApiError): string => { const data = error.data; if ( typeof data === "object" && data !== null && "error" in data && typeof data.error === "string" ) { return data.error; } return error.message; }; /** Map any thrown value to the contract's error shape. A `CliError` passes * through; the typed client's refusal maps by status; a success answer the * table's envelope rejects keeps the served status under the server kind; * every other failure takes the caller's fallback kind — a transport * failure for the api command, a local input or environment failure for the * importer. */ export const cliErrorFromUnknown = ( error: unknown, fallbackKind: CliErrorKind = "server", ): CliError => { if (error instanceof CliError) return error; if (error instanceof AdminApiError) { return new CliError( kindForStatus(error.status), serverMessage(error), null, error.status, ); } if (error instanceof AdminApiSchemaError) { // The served status stays: the server did answer, and its status names // the malformed reply. return new CliError("server", error.message, null, error.status); } return new CliError( fallbackKind, error instanceof Error ? error.message : String(error), ); }; /** Map valibot's issues to the per-field problems the contract carries. */ export const fieldProblems = ( issues: readonly { message: string; path?: readonly { key: unknown }[] | undefined; }[], ): FieldProblem[] => issues.map((issue) => ({ field: issue.path?.map((item) => String(item.key)).join(".") ?? "", message: issue.message, }));