import { readFileSync, writeFileSync } from "node:fs"; import { apiFetch } from "../api.mjs"; import { emit } from "../output.mjs"; import { t } from "../i18n.mjs"; function truncate(v, max = 40) { if (!v) return "-"; const s = String(v); return s.length > max ? s.slice(0, max - 1) + "…" : s; } function toYaml(obj, indent = 0) { const pad = " ".repeat(indent); if (obj === null || obj === undefined) return "null"; if (typeof obj === "boolean") return String(obj); if (typeof obj === "number") return String(obj); if (typeof obj === "string") { if (/[\n:#{}[\],&*?|<>=!%@`]/.test(obj) || obj.trim() !== obj) { return JSON.stringify(obj); } return obj || '""'; } if (Array.isArray(obj)) { if (obj.length === 0) return "[]"; return obj.map((v) => `\n${pad}- ${toYaml(v, indent + 1)}`).join(""); } const entries = Object.entries(obj); if (entries.length === 0) return "{}"; return entries .map(([k, v]) => { const safeKey = /[^a-zA-Z0-9_-]/.test(k) ? JSON.stringify(k) : k; if (v !== null && typeof v === "object") { const nested = toYaml(v, indent + 1); if (Array.isArray(v) && v.length > 0) return `\n${pad}${safeKey}:${nested}`; if (!Array.isArray(v) && Object.keys(v).length > 0) return `\n${pad}${safeKey}:\n${nested}`; return `\n${pad}${safeKey}: ${nested}`; } return `\n${pad}${safeKey}: ${toYaml(v, indent + 1)}`; }) .join("") .trimStart(); } function validateBasic(spec) { if (!spec || typeof spec !== "object") throw new Error("spec is not an object"); if (!spec.openapi && !spec.swagger) throw new Error("missing openapi/swagger version field"); if (!spec.info) throw new Error("missing info object"); if (!spec.paths) throw new Error("missing paths object"); } const endpointSchema = [ { key: "method", header: "Method", width: 8 }, { key: "path", header: "Path", width: 45 }, { key: "operationId", header: "Operation ID", width: 25 }, { key: "summary", header: "Summary", width: 40, formatter: (v) => truncate(v, 40) }, ]; export function registerOpenapi(program) { const api = program.command("openapi").description(t("openapi.description")); api .command("dump") .description(t("openapi.dump.description")) .option("--format ", t("openapi.dump.format"), "yaml") .option("--out ", t("openapi.dump.out")) .action(async (opts, cmd) => { const res = await apiFetch("/api/openapi/spec"); if (!res.ok) { process.stderr.write(`Error: ${res.status}\n`); process.exit(1); } const data = await res.json(); const serialized = opts.format === "yaml" ? toYaml(data) + "\n" : JSON.stringify(data, null, 2); if (opts.out) { writeFileSync(opts.out, serialized); process.stdout.write(`Saved to ${opts.out}\n`); } else { process.stdout.write(serialized); } }); api .command("validate") .description(t("openapi.validate.description")) .action(async (opts, cmd) => { const res = await apiFetch("/api/openapi/spec"); if (!res.ok) { process.stderr.write(`Error: ${res.status}\n`); process.exit(1); } const spec = await res.json(); try { validateBasic(spec); process.stdout.write("Spec is valid\n"); } catch (err) { process.stderr.write(`Invalid: ${err.message}\n`); process.exit(1); } }); api .command("try ") .description(t("openapi.try.description")) .option("--method ", t("openapi.try.method"), "GET") .option("--body ", t("openapi.try.body")) .option("--query ", t("openapi.try.query"), (v, prev = []) => [...prev, v.split("=")], []) .option("--header ", t("openapi.try.header"), (v, prev = []) => [...prev, v.split("=")], []) .action(async (path, opts, cmd) => { const body = opts.body ? JSON.parse(readFileSync(opts.body, "utf8")) : undefined; const query = Object.fromEntries(opts.query ?? []); const headers = Object.fromEntries(opts.header ?? []); const res = await apiFetch("/api/openapi/try", { method: "POST", body: { path, method: opts.method, body, query, headers }, }); if (!res.ok) { process.stderr.write(`Error: ${res.status}\n`); process.exit(1); } emit(await res.json(), cmd.optsWithGlobals()); }); api .command("endpoints") .description(t("openapi.endpoints.description")) .option("--search ", t("openapi.endpoints.search")) .action(async (opts, cmd) => { const res = await apiFetch("/api/openapi/spec"); if (!res.ok) { process.stderr.write(`Error: ${res.status}\n`); process.exit(1); } const spec = await res.json(); const rows = []; for (const [path, methods] of Object.entries(spec.paths ?? {})) { for (const [method, def] of Object.entries(methods)) { if (["parameters", "summary"].includes(method)) continue; const summary = def.summary ?? def.description ?? ""; if ( opts.search && !path.includes(opts.search) && !summary.toLowerCase().includes(opts.search.toLowerCase()) ) continue; rows.push({ method: method.toUpperCase(), path, summary, operationId: def.operationId }); } } emit(rows, cmd.optsWithGlobals(), endpointSchema); }); api .command("paths") .description(t("openapi.paths.description")) .action(async (opts, cmd) => { const res = await apiFetch("/api/openapi/spec"); if (!res.ok) { process.stderr.write(`Error: ${res.status}\n`); process.exit(1); } const spec = await res.json(); const paths = Object.keys(spec.paths ?? {}).sort(); emit( paths.map((p) => ({ path: p })), cmd.optsWithGlobals() ); }); }