#!/usr/bin/env bun import { fileURLToPath } from "url" const dir = fileURLToPath(new URL("..", import.meta.url)) process.chdir(dir) import { $ } from "bun" import path from "path" import { createClient } from "@hey-api/openapi-ts" import { generateFiles, parseSpec } from "@workos/oagen" import { effectEmitter } from "./effect-emitter.js" const opencode = path.resolve(dir, "../../opencode") await $`bun dev generate > ${dir}/openapi.json`.cwd(opencode) await createClient({ input: "./openapi.json", output: { path: "./src/v2/gen", tsConfigPath: path.join(dir, "tsconfig.json"), clean: true, }, plugins: [ { name: "@hey-api/typescript", exportFromIndex: false, }, { name: "@hey-api/sdk", instance: "OpencodeClient", exportFromIndex: false, auth: false, paramsStructure: "flat", }, { name: "@hey-api/client-fetch", exportFromIndex: false, baseUrl: "http://localhost:4096", }, ], }) await patchSseTypes("./src/v2/gen/client/types.gen.ts") const openapi = await Bun.file("./openapi.json").json() const effect = generateFiles(await parseSpec("./openapi.json"), effectEmitter, { namespace: "Opencode", outputDir: "./src/v2/gen", emitterOptions: { serverSentEvents: serverSentEvents(openapi), }, }) for (const file of effect.files) { await Bun.write(path.join("./src/v2/gen", file.path), file.content) } await createClient({ input: "./example/effect-sdk/openapi.json", output: { path: "./example/effect-sdk/gen", tsConfigPath: path.join(dir, "tsconfig.json"), clean: true, }, plugins: [ { name: "@hey-api/typescript", exportFromIndex: false, }, { name: "@hey-api/client-fetch", exportFromIndex: false, baseUrl: "https://api.example.com", }, ], }) await patchSseTypes("./example/effect-sdk/gen/client/types.gen.ts") const exampleOpenapi = await Bun.file("./example/effect-sdk/openapi.json").json() const exampleEffect = generateFiles(await parseSpec("./example/effect-sdk/openapi.json"), effectEmitter, { namespace: "Example", outputDir: "./example/effect-sdk/gen", emitterOptions: { serverSentEvents: serverSentEvents(exampleOpenapi), }, }) for (const file of exampleEffect.files) { await Bun.write(path.join("./example/effect-sdk/gen", file.path), file.content) } await $`bun prettier --write src/gen` await $`bun prettier --write src/v2` await $`bun prettier --write example/effect-sdk/gen` await $`rm -rf dist` await $`bun tsc` await $`rm openapi.json` async function patchSseTypes(sseTypesPath: string) { // Patch a @hey-api/openapi-ts codegen bug: SseFn incorrectly passes the // endpoint's TError into the second generic of ServerSentEventsResult, which // is the AsyncGenerator's TReturn slot. Iterator return values have nothing // to do with HTTP errors, and any consumer that calls `.return()` or returns // from a mock generator gets type-checked against the wrong shape. Drop the // arg so TReturn defaults to void. const sseTypesFile = Bun.file(sseTypesPath) const sseTypesSource = await sseTypesFile.text() const sseTypesPatched = sseTypesSource.replace( "=> Promise>", "=> Promise>", ) if (sseTypesPatched === sseTypesSource) { throw new Error(`SseFn patch did not apply; @hey-api/openapi-ts output may have changed (${sseTypesPath})`) } await Bun.write(sseTypesPath, sseTypesPatched) } function serverSentEvents(spec: unknown) { if (!spec || typeof spec !== "object" || !("paths" in spec) || !spec.paths || typeof spec.paths !== "object") return [] return Object.entries(spec.paths).flatMap(([route, value]) => { if (!value || typeof value !== "object") return [] return Object.entries(value).flatMap(([method, operation]) => { if (!operation || typeof operation !== "object" || !("responses" in operation)) return [] if (!hasEventStream(operation.responses)) return [] return [`${method.toUpperCase()} ${route}`] }) }) } function hasEventStream(value: unknown): boolean { if (!value || typeof value !== "object") return false if ("text/event-stream" in value) return true return Object.values(value).some(hasEventStream) }