#!/usr/bin/env bun import { readFileSync } from "node:fs"; import { parse } from "@kitchen-md/core"; import { Command } from "commander"; import { err, ok, type Result } from "neverthrow"; import { render } from "./render.ts"; export type ViewError = { tag: "read-failed"; path: string; cause: string }; // The union of every error a command can surface to the boundary; new fallible commands add their variants here. export type CliError = ViewError; export function viewFile(path: string): Result { return readFile(path).map((content) => render(parse(content))); } export function formatError(error: CliError): string { switch (error.tag) { case "read-failed": return `cannot read '${error.path}': ${error.cause}`; } } // The one place a throwing API is turned into a Result; nothing above this leaks exceptions. function readFile(path: string): Result { try { return ok(readFileSync(path, "utf8")); } catch (error) { const cause = error instanceof Error ? error.message : String(error); return err({ tag: "read-failed", path, cause }); } } const program = new Command(); program.name("kitchen").description("Read and view KitchenMD Recipe Files"); program .command("view") .description("Render a Recipe File to the terminal") .argument("", "path to a Recipe File") .action((file: string) => { viewFile(file).match( (output) => process.stdout.write(output), (error) => { process.stderr.write(`kitchen: ${formatError(error)}\n`); process.exit(1); }, ); }); if (import.meta.main) { program.parse(); }