Spaces:
Runtime error
Runtime error
File size: 6,345 Bytes
cd8bd0a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | #!/usr/bin/env node
/**
* Generates bin/cli/api-commands/<tag>.mjs from the OpenAPI spec.
* Run: npm run build:cli-api
*/
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import * as yaml from "js-yaml";
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, "..", "..");
const SPEC_PATH = process.env.OPENAPI_SPEC || join(ROOT, "docs/openapi.yaml");
const OUT_DIR = join(ROOT, "bin/cli/api-commands");
// Operations already covered by hand-crafted commands — skip in generated output.
const IGNORED_OP_IDS = new Set([
"createChatCompletion",
"streamChatCompletion",
"listModels",
"getModel",
"createEmbedding",
"createImage",
"createImageEdit",
"createImageVariation",
"createTranscription",
"createSpeech",
"createModeration",
]);
function kebab(s) {
return s
.replace(/([A-Z])/g, (m) => "-" + m.toLowerCase())
.replace(/^-/, "")
.replace(/_/g, "-")
.replace(/--+/g, "-");
}
function camelCase(s) {
return s.replace(/[-_](\w)/g, (_, c) => c.toUpperCase());
}
function escapeStr(s) {
return String(s || "")
.replace(/\\/g, "\\\\")
.replace(/"/g, '\\"')
.slice(0, 150);
}
if (!existsSync(OUT_DIR)) mkdirSync(OUT_DIR, { recursive: true });
const spec = yaml.load(readFileSync(SPEC_PATH, "utf8"));
/** @type {Record<string, Array<{path: string, method: string, opId: string, op: object}>>} */
const byTag = {};
for (const [path, methods] of Object.entries(spec.paths || {})) {
for (const [method, op] of Object.entries(methods)) {
if (["parameters", "summary", "description", "servers"].includes(method)) continue;
if (typeof op !== "object" || op === null) continue;
if (IGNORED_OP_IDS.has(op.operationId)) continue;
const rawTag = op.tags?.[0] || "uncategorized";
const tag = rawTag
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "");
const opId = op.operationId || `${method}-${path.replace(/[^a-z0-9]/gi, "-")}`;
byTag[tag] = byTag[tag] || [];
byTag[tag].push({ path, method, opId, op });
}
}
const generatedTags = [];
for (const [tag, ops] of Object.entries(byTag)) {
const fnName = `register_${tag.replace(/-/g, "_")}`;
const lines = [
`// AUTO-GENERATED from ${SPEC_PATH.replace(ROOT + "/", "")}. Do not edit.`,
`import { apiFetch } from "../api.mjs";`,
`import { emit } from "../output.mjs";`,
`import { readFileSync } from "node:fs";`,
``,
`export function ${fnName}(parent) {`,
` const tag = parent.command("${tag}").description("${escapeStr(ops[0]?.op?.tags?.[0] || tag)} endpoints");`,
];
for (const { path, method, opId, op } of ops) {
const cmdName = kebab(opId);
const params = op.parameters || [];
const pathParams = params.filter((p) => p.in === "path");
const queryParams = params.filter((p) => p.in === "query");
const hasBody = !!op.requestBody;
const summary = escapeStr(op.summary || op.description || cmdName);
lines.push(` tag.command("${cmdName}")`);
lines.push(` .description("${summary}")`);
for (const p of pathParams) {
lines.push(
` .requiredOption("--${kebab(p.name)} <${p.name}>", "${escapeStr(p.description)}")`
);
}
for (const p of queryParams) {
const flag = p.required ? "requiredOption" : "option";
lines.push(` .${flag}("--${kebab(p.name)} <${p.name}>", "${escapeStr(p.description)}")`);
}
if (hasBody) {
lines.push(` .option("--body <jsonOrPath>", "JSON body or @path/to/file.json")`);
}
lines.push(` .action(async (opts, cmd) => {`);
lines.push(` const gOpts = cmd.optsWithGlobals();`);
// Build URL with path param substitution
lines.push(` let url = "${path}";`);
for (const p of pathParams) {
lines.push(
` url = url.replace("{${p.name}}", encodeURIComponent(opts.${camelCase(kebab(p.name))} ?? ""));`
);
}
// Build query string from query params
if (queryParams.length > 0) {
lines.push(` const qs = new URLSearchParams();`);
for (const p of queryParams) {
const optName = camelCase(kebab(p.name));
lines.push(
` if (opts.${optName} != null) qs.set("${p.name}", String(opts.${optName}));`
);
}
lines.push(` if (qs.toString()) url += "?" + qs.toString();`);
}
// Body handling
if (hasBody) {
lines.push(` let body;`);
lines.push(` if (opts.body) {`);
lines.push(` body = opts.body.startsWith("@")`);
lines.push(` ? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))`);
lines.push(` : JSON.parse(opts.body);`);
lines.push(` }`);
}
const bodyArg = hasBody ? ", body" : "";
lines.push(
` const res = await apiFetch(url, { method: "${method.toUpperCase()}"${hasBody ? ", body" : ""}, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });`
);
lines.push(` const data = res.ok ? await res.json() : await res.text();`);
lines.push(` emit(data, gOpts);`);
lines.push(` });`);
}
lines.push(`}`);
const content = lines.join("\n") + "\n";
writeFileSync(join(OUT_DIR, `${tag}.mjs`), content);
generatedTags.push(tag);
console.log(`[generate] ${tag}.mjs — ${ops.length} operations`);
}
// Generate registry
const registryLines = [
`// AUTO-GENERATED. Do not edit.`,
...generatedTags.map((t) => `import { register_${t.replace(/-/g, "_")} } from "./${t}.mjs";`),
``,
`export const API_TAGS = ${JSON.stringify(generatedTags)};`,
``,
`export function registerApiCommands(program) {`,
` const api = program`,
` .command("api")`,
` .description("Direct REST API access (generated from OpenAPI spec)");`,
` api`,
` .command("tags")`,
` .description("List available API tag groups")`,
` .action(() => { API_TAGS.forEach((t) => console.log(t)); });`,
...generatedTags.map((t) => ` register_${t.replace(/-/g, "_")}(api);`),
`}`,
];
writeFileSync(join(OUT_DIR, "registry.mjs"), registryLines.join("\n") + "\n");
console.log(`[generate] registry.mjs — ${generatedTags.length} tags`);
console.log("[generate] Done.");
|