init: initial commit

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Giuliano Silvestro
2026-04-30 08:26:34 +10:00
commit 262a56c2e5
10 changed files with 2914 additions and 0 deletions

99
scripts/sync-spec.mjs Normal file
View File

@@ -0,0 +1,99 @@
#!/usr/bin/env node
// Fetches the arcadia OpenAPI spec and regenerates ../src/generated/openapi.d.ts.
//
// Usage (from a consuming app, with arcadia reachable):
// node ../lib-arcadia-client/scripts/sync-spec.mjs
//
// Configurable via env:
// ARCADIA_OPENAPI_URL default: http://localhost:4000/api/openapi
// ARCADIA_BEARER_TOKEN optional, sent as Authorization if the spec route
// is gated in your deployment
//
// Requires `openapi-typescript` to be available (in the consuming app's
// node_modules, or globally). Run from a consuming app so the dep resolves:
// npm i -D openapi-typescript
import { writeFile, mkdir } from "node:fs/promises";
import { fileURLToPath, pathToFileURL } from "node:url";
import { dirname, resolve } from "node:path";
import { createRequire } from "node:module";
const here = dirname(fileURLToPath(import.meta.url));
const outDir = resolve(here, "..", "src", "generated");
const outFile = resolve(outDir, "openapi.d.ts");
const specUrl = process.env.ARCADIA_OPENAPI_URL ?? "http://localhost:4000/api/openapi";
async function main() {
console.log(`→ fetching ${specUrl}`);
const headers = {};
if (process.env.ARCADIA_BEARER_TOKEN) {
headers["Authorization"] = `Bearer ${process.env.ARCADIA_BEARER_TOKEN}`;
}
const res = await fetch(specUrl, { headers });
if (!res.ok) {
console.error(`spec fetch failed: ${res.status} ${res.statusText}`);
process.exit(1);
}
const spec = await res.json();
// Resolve openapi-typescript from the consuming app's node_modules, since
// the lib has no package.json of its own.
let openapiTs;
let astToString;
try {
const requireFromCwd = createRequire(resolve(process.cwd(), "package.json"));
const resolved = requireFromCwd.resolve("openapi-typescript");
const mod = await import(pathToFileURL(resolved).href);
// Handle both ESM (mod.default is the function) and CJS-via-import
// (mod.default is the namespace, mod.default.default is the function).
openapiTs = typeof mod.default === "function" ? mod.default : mod.default?.default;
astToString = mod.astToString ?? mod.default?.astToString;
} catch (err) {
console.error(
`openapi-typescript could not be resolved from ${process.cwd()}.\n` +
`Run from your consuming app's directory after installing it:\n` +
` cd <app> && npm i -D openapi-typescript\n` +
`Original error: ${err.message}`,
);
process.exit(1);
}
// Scrub malformed operation entries. Arcadia's spec contains some
// endpoints whose operation value is the bare string "ok" (a controller
// placeholder that never got replaced with a real OperationObject).
// openapi-typescript can't transform them — strip them and report so
// the generation succeeds for the rest. Fix is on the arcadia side.
const skipped = [];
if (spec.paths) {
for (const [path, item] of Object.entries(spec.paths)) {
if (!item || typeof item !== "object") continue;
for (const [verb, op] of Object.entries(item)) {
if (typeof op !== "object" || op === null) {
skipped.push(`${verb.toUpperCase()} ${path}`);
delete item[verb];
}
}
}
}
if (skipped.length) {
console.warn(`⚠ skipped ${skipped.length} malformed operations (arcadia spec bug):`);
for (const s of skipped.slice(0, 10)) console.warn(` - ${s}`);
if (skipped.length > 10) console.warn(` …and ${skipped.length - 10} more`);
}
console.log(`→ generating types`);
const ast = await openapiTs(spec);
const dts = astToString(ast);
await mkdir(outDir, { recursive: true });
const banner =
"// AUTO-GENERATED — do not edit by hand. Regenerate with sync-spec.mjs.\n" +
`// Source: ${specUrl}\n` +
`// Generated: ${new Date().toISOString()}\n\n`;
await writeFile(outFile, banner + dts, "utf8");
console.log(`✓ wrote ${outFile}`);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});