#!/usr/bin/env node import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { make as makeJsonSchemaGenerator } from "@effect/openapi-generator/JsonSchemaGenerator"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Logger from "effect/Logger"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse, } from "effect/unstable/http"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; const UPSTREAM_REF = "678157acaa819d5510adfe359abb5d0392cfe461"; const USER_AGENT = "effect-codex-app-server-generator"; const GITHUB_API_BASE = "https://api.github.com/repos/openai/codex/contents/codex-rs/app-server-protocol"; const GithubContentEntries = Schema.Array( Schema.Struct({ name: Schema.String, path: Schema.String, download_url: Schema.NullOr(Schema.String), type: Schema.String, }), ); type GithubContentEntry = (typeof GithubContentEntries.Type)[number]; const JsonSchemaDocument = Schema.StructWithRest( Schema.Struct({ definitions: Schema.optionalKey(Schema.Record(Schema.String, Schema.Json)), }), [Schema.Record(Schema.String, Schema.Json)], ); const decodeGithubContentEntries = Schema.decodeEffect(Schema.fromJsonString(GithubContentEntries)); const decodeJsonSchemaDocument = Schema.decodeEffect(Schema.fromJsonString(JsonSchemaDocument)); interface GeneratedPaths { readonly generatedDir: string; readonly schemaOutputPath: string; readonly metaOutputPath: string; readonly namespacesOutputPath: string; } interface MethodEntry { readonly method: string; readonly paramsType?: string; } interface JsonSchemaFile { readonly namespace?: string; readonly exportName: string; readonly fileName: string; readonly downloadUrl: string; readonly qualifiedName: string; } class GeneratorError extends Schema.TaggedErrorClass()("GeneratorError", { detail: Schema.String, cause: Schema.optional(Schema.Defect()), }) { override get message() { return this.detail; } } const ManualSchemas: Record = { GetAuthStatusParams: { type: "object", title: "GetAuthStatusParams", properties: { includeToken: { anyOf: [{ type: "boolean" }, { type: "null" }], }, refreshToken: { anyOf: [{ type: "boolean" }, { type: "null" }], }, }, }, GetConversationSummaryParams: { title: "GetConversationSummaryParams", oneOf: [ { type: "object", properties: { rolloutPath: { type: "string" }, }, required: ["rolloutPath"], }, { type: "object", properties: { conversationId: { type: "string" }, }, required: ["conversationId"], }, ], }, GetConversationSummaryResponse: { type: "object", title: "GetConversationSummaryResponse", properties: { summary: {}, }, required: ["summary"], }, GitDiffToRemoteParams: { type: "object", title: "GitDiffToRemoteParams", properties: { cwd: { type: "string" }, }, required: ["cwd"], }, GitDiffToRemoteResponse: { type: "object", title: "GitDiffToRemoteResponse", properties: { sha: { type: "string" }, diff: { type: "string" }, }, required: ["sha", "diff"], }, GetAuthStatusResponse: { type: "object", title: "GetAuthStatusResponse", properties: { authMethod: { anyOf: [{}, { type: "null" }], }, authToken: { anyOf: [{ type: "string" }, { type: "null" }], }, requiresOpenaiAuth: { anyOf: [{ type: "boolean" }, { type: "null" }], }, }, required: ["authMethod", "authToken", "requiresOpenaiAuth"], }, }; // Codex 0.150 added these multi-agent values before our next full protocol // refresh. Keep every generated response namespace compatible with them. const Codex0150DefinitionSchemas: Record = { CollabAgentTool: { type: "string", enum: [ "spawnAgent", "sendInput", "resumeAgent", "wait", "closeAgent", "sendMessage", "followupTask", "interruptAgent", "listAgents", ], }, CollabAgentToolCallStatus: { type: "string", enum: ["inProgress", "completed", "failed", "interrupted"], }, PlanType: { type: "string", enum: [ "free", "go", "plus", "pro", "prolite", "team", "self_serve_business_prolite", "self_serve_business_usage_based", "business", "ent26", "enterprise_cbp_automation", "enterprise_cbp_usage_based", "enterprise", "edu", "edu_plus", "edu_pro", "unknown", ], }, SubAgentActivityKind: { type: "string", enum: ["started", "interacted", "interrupted", "completed"], }, }; function applyCodex0151DefinitionCompatibility( exportName: string, definitionName: string, definitionSchema: Schema.Json, ): Schema.Json { const isThreadResponse = exportName === "V2ThreadReadResponse" || exportName === "V2ThreadResumeResponse" || exportName === "V2ThreadRollbackResponse"; if ( !isThreadResponse || definitionName !== "CodexErrorInfo" || typeof definitionSchema !== "object" ) { return definitionSchema; } const schema = definitionSchema as { readonly oneOf?: ReadonlyArray<{ readonly enum?: ReadonlyArray }>; }; const [firstVariant, ...remainingVariants] = schema.oneOf ?? []; if (!firstVariant?.enum || firstVariant.enum.includes("rateLimitExceeded")) { return definitionSchema; } return { ...definitionSchema, oneOf: [ { ...firstVariant, enum: [...firstVariant.enum, "rateLimitExceeded"] }, ...remainingVariants, ], }; } const getGeneratedPaths = Effect.fn("getGeneratedPaths")(function* () { const path = yield* Path.Path; const generatedDir = path.join(import.meta.dirname, "..", "src", "_generated"); return { generatedDir, schemaOutputPath: path.join(generatedDir, "schema.gen.ts"), metaOutputPath: path.join(generatedDir, "meta.gen.ts"), namespacesOutputPath: path.join(generatedDir, "namespaces.gen.ts"), } satisfies GeneratedPaths; }); const ensureGeneratedDir = Effect.fn("ensureGeneratedDir")(function* () { const fs = yield* FileSystem.FileSystem; const { generatedDir } = yield* getGeneratedPaths(); yield* fs.makeDirectory(generatedDir, { recursive: true }); }); const fetchText = Effect.fn("fetchText")(function* (url: string) { return yield* HttpClientRequest.get(url).pipe( HttpClientRequest.setHeader("user-agent", USER_AGENT), HttpClient.execute, Effect.flatMap(HttpClientResponse.filterStatusOk), Effect.flatMap((okResponse) => okResponse.text), Effect.mapError( (cause) => new GeneratorError({ detail: `Failed to fetch ${url}`, cause, }), ), ); }); const fetchDirectoryEntries = Effect.fn("fetchDirectoryEntries")(function* (path: string) { const raw = yield* fetchText(`${GITHUB_API_BASE}/${path}?ref=${UPSTREAM_REF}`); return yield* decodeGithubContentEntries(raw); }); function collectSchemaEntries( chunk: string, ): ReadonlyArray<{ readonly name: string; readonly code: string }> { const lines = chunk .split("\n") .map((line) => line.trim()) .filter((line) => line.length > 0 && !line.startsWith("//")); const entries: Array<{ name: string; code: string }> = []; for (let index = 0; index < lines.length; index += 1) { const typeLine = lines[index]; if (!typeLine?.startsWith("export type ")) { continue; } const constLine = lines[index + 1]; if (!constLine?.startsWith("export const ")) { throw new Error(`Malformed generator output near: ${typeLine}`); } const match = /^export type ([A-Za-z0-9_]+)/.exec(typeLine); if (!match?.[1]) { throw new Error(`Could not extract schema name from: ${typeLine}`); } entries.push({ name: match[1], code: `${typeLine}\n${constLine}`, }); index += 1; } return entries; } function normalizeNullableTypes(value: Schema.Json): Schema.Json { if (Array.isArray(value)) { return value.map(normalizeNullableTypes); } if (value === null || typeof value !== "object") { return value; } const normalizedEntries = Object.entries(value).map(([key, child]) => [ key, normalizeNullableTypes(child), ]); const normalizedObject = Object.fromEntries(normalizedEntries) as Record; const typeValue = normalizedObject.type; if (!Array.isArray(typeValue)) { return normalizedObject; } const normalizedTypes = typeValue.filter((entry): entry is string => typeof entry === "string"); if (normalizedTypes.length !== typeValue.length || !normalizedTypes.includes("null")) { return normalizedObject; } const nonNullTypes = normalizedTypes.filter((entry) => entry !== "null"); if (nonNullTypes.length !== 1) { return normalizedObject; } const nonNullType = nonNullTypes[0]!; const nextObject: Record = {}; for (const [key, child] of Object.entries(normalizedObject)) { if (key !== "type") { nextObject[key] = child; } } return { anyOf: [ { ...nextObject, type: nonNullType, }, { type: "null" }, ], }; } function stripNullDefaults(value: Schema.Json): Schema.Json { if (Array.isArray(value)) { return value.map(stripNullDefaults); } if (value === null || typeof value !== "object") { return value; } return Object.fromEntries( Object.entries(value) .filter(([key, child]) => !(key === "default" && child === null)) .map(([key, child]) => [key, stripNullDefaults(child)]), ) as Schema.Json; } // Codex 0.153 adds async questions to agent messages. Keep older protocol // fields until the next full refresh, including every thread history namespace. function addAsyncQuestionFields(value: Schema.Json): Schema.Json { if (Array.isArray(value)) { return value.map(addAsyncQuestionFields); } if (value === null || typeof value !== "object") { return value; } const properties = "properties" in value ? value.properties : undefined; const itemType = properties && typeof properties === "object" && "type" in properties ? properties.type : undefined; if ( properties && typeof properties === "object" && itemType && typeof itemType === "object" && "enum" in itemType && Array.isArray(itemType.enum) && itemType.enum.includes("agentMessage") ) { return { ...value, properties: { ...properties, delivery: { anyOf: [{ type: "string", enum: ["async"] }, { type: "null" }] }, questions: { anyOf: [ { type: "array", items: { type: "object", properties: { title: { type: "string" }, options: { anyOf: [{ type: "array", items: { type: "string" } }, { type: "null" }], }, }, required: ["title"], }, }, { type: "null" }, ], }, }, }; } return Object.fromEntries( Object.entries(value).map(([key, child]) => [key, addAsyncQuestionFields(child)]), ); } function toPascalCaseMethod(method: string) { return method .split("/") .flatMap((segment) => segment.split(/(?=[A-Z])/)) .flatMap((segment) => segment.split(/[-_]/)) .filter(Boolean) .map((segment) => segment[0]!.toUpperCase() + segment.slice(1)) .join(""); } function parseRequestEntries(fileContents: string): ReadonlyArray { const entryPattern = /\{\s*"method":\s*"([^"]+)",\s*id:\s*RequestId,\s*params:\s*([^,}]+)/g; const entries: Array = []; let match: RegExpExecArray | null; while ((match = entryPattern.exec(fileContents)) !== null) { entries.push({ method: match[1]!, paramsType: match[2]!.trim(), }); } return entries; } function parseNotificationEntries(fileContents: string): ReadonlyArray { const entryPattern = /\{\s*"method":\s*"([^"]+)"(?:,\s*"params":\s*([^ }]+))?\s*\}/g; const entries: Array = []; let match: RegExpExecArray | null; while ((match = entryPattern.exec(fileContents)) !== null) { entries.push({ method: match[1]!, ...(match[2] ? { paramsType: match[2].trim() } : {}), }); } return entries; } function resolveSchemaTypeName( rawTypeName: string, generatedSchemaNames: ReadonlySet, ): string { if (rawTypeName === "undefined") { return "undefined"; } const candidates = [ rawTypeName, `V2${rawTypeName}`, `V1${rawTypeName}`, `SerdeJson${rawTypeName}`, ]; for (const candidate of candidates) { if (generatedSchemaNames.has(candidate)) { return candidate; } } throw new Error(`Unable to resolve schema type name: ${rawTypeName}`); } function resolveResponseTypeName( method: string, paramsType: string | undefined, generatedSchemaNames: ReadonlySet, ): string { const overrides: Record = { "account/logout": "LogoutAccountResponse", "account/rateLimits/read": "GetAccountRateLimitsResponse", "account/usage/read": "GetAccountTokenUsageResponse", "account/workspaceMessages/read": "GetWorkspaceMessagesResponse", "config/batchWrite": "ConfigWriteResponse", "config/mcpServer/reload": "McpServerRefreshResponse", "config/value/write": "ConfigWriteResponse", "configRequirements/read": "ConfigRequirementsReadResponse", "externalAgentConfig/import/readHistories": "ExternalAgentConfigImportHistoriesReadResponse", }; const override = overrides[method]; if (override) { return resolveSchemaTypeName(override, generatedSchemaNames); } if (paramsType && paramsType !== "undefined") { const fromParams = paramsType.replace(/Params$/, "Response"); try { return resolveSchemaTypeName(fromParams, generatedSchemaNames); } catch { // Fall through to method-based lookup. } } return resolveSchemaTypeName(`${toPascalCaseMethod(method)}Response`, generatedSchemaNames); } function renderMethodConstants(constantName: string, entries: ReadonlyArray) { return [ `export const ${constantName} = {`, ...entries.map( (entry) => ` ${JSON.stringify(entry.method)}: ${JSON.stringify(entry.method)},`, ), "} as const;", "", ].join("\n"); } function renderTypeInterface( interfaceName: string, entries: ReadonlyArray, typeName: (entry: MethodEntry) => string, ) { return [ `export interface ${interfaceName} {`, ...entries.map((entry) => ` readonly ${JSON.stringify(entry.method)}: ${typeName(entry)};`), "}", "", ].join("\n"); } function renderSchemaMap( constantName: string, entries: ReadonlyArray, typeName: (entry: MethodEntry) => string, ) { return [ `export const ${constantName} = {`, ...entries.map((entry) => { const schemaName = typeName(entry); return ` ${JSON.stringify(entry.method)}: ${ schemaName === "undefined" ? "undefined" : `CodexSchema.${schemaName}` },`; }), "} as const;", "", ].join("\n"); } function renderSchemaTypeReference(schemaName: string) { return schemaName === "undefined" ? "undefined" : `CodexSchema.${schemaName}`; } function exportNameForPath(filePath: string): string { const relative = filePath.replace(/^schema\/json\//, "").replace(/\.json$/, ""); if (!relative.includes("/")) { return relative; } const [namespace, name] = relative.split("/", 2) as [string, string]; const namespacePrefix = namespace .split(/[^A-Za-z0-9]+/) .filter(Boolean) .map((segment) => segment[0]!.toUpperCase() + segment.slice(1)) .join(""); return `${namespacePrefix}${name}`; } function buildJsonSchemaFiles( entries: ReadonlyArray, ): ReadonlyArray { return entries .filter( (entry) => entry.type === "file" && entry.name.endsWith(".json") && entry.download_url !== null && !entry.name.startsWith("codex_app_server_protocol."), ) .map((entry) => { const relative = entry.path.replace(/^codex-rs\/app-server-protocol\/schema\/json\//, ""); const parts = relative.split("/"); if (parts.length > 1) { return { namespace: parts[0]!, exportName: exportNameForPath(relative), fileName: entry.name, downloadUrl: entry.download_url!, qualifiedName: relative.replace(/\.json$/, ""), } satisfies JsonSchemaFile; } return { exportName: exportNameForPath(relative), fileName: entry.name, downloadUrl: entry.download_url!, qualifiedName: relative.replace(/\.json$/, ""), } satisfies JsonSchemaFile; }); } function rewriteExternalRefs( value: Schema.Json, localDefinitionNames: ReadonlyMap, currentNamespace: string | undefined, exportNameByQualifiedName: ReadonlyMap, ): Schema.Json { if (Array.isArray(value)) { return value.map((entry) => rewriteExternalRefs(entry, localDefinitionNames, currentNamespace, exportNameByQualifiedName), ); } if (value === null || typeof value !== "object") { return value; } return Object.fromEntries( Object.entries(value).map(([key, child]) => { if (key === "$ref" && typeof child === "string" && child.startsWith("#/definitions/")) { const definitionName = child.slice("#/definitions/".length); const localRewrite = localDefinitionNames.get(definitionName); if (localRewrite) { return [key, `#/definitions/${localRewrite}`]; } const candidates = [ ...(currentNamespace ? [`${currentNamespace}/${definitionName}`] : []), definitionName, definitionName.replace(/^v[12]\//, ""), definitionName.replace(/^serde_json\//, ""), `v2/${definitionName}`, `v1/${definitionName}`, `serde_json/${definitionName}`, ]; const rewritten = candidates .map((candidate) => exportNameByQualifiedName.get(candidate)) .find((candidate) => candidate !== undefined); if (!rewritten) { throw new Error(`Missing rewritten definition for ref: ${child}`); } return [key, `#/definitions/${rewritten}`]; } return [ key, rewriteExternalRefs( child, localDefinitionNames, currentNamespace, exportNameByQualifiedName, ), ]; }), ) as Schema.Json; } const generateFiles = Effect.fn("generateFiles")(function* () { yield* ensureGeneratedDir(); const [rootJsonEntries, v1JsonEntries, v2JsonEntries] = yield* Effect.all([ fetchDirectoryEntries("schema/json"), fetchDirectoryEntries("schema/json/v1"), fetchDirectoryEntries("schema/json/v2"), ]); const jsonSchemaFiles = [ ...buildJsonSchemaFiles(rootJsonEntries), ...buildJsonSchemaFiles(v1JsonEntries), ...buildJsonSchemaFiles(v2JsonEntries), ].toSorted((left, right) => left.exportName.localeCompare(right.exportName)); const exportNameByQualifiedName = new Map( jsonSchemaFiles.map((file) => [file.qualifiedName, file.exportName]), ); const aggregateSchemas: Record = {}; for (const file of jsonSchemaFiles) { const raw = yield* fetchText(file.downloadUrl); const parsed = yield* decodeJsonSchemaDocument(raw); const localDefinitionNames = new Map( Object.keys(parsed.definitions ?? {}).map((definitionName) => [ definitionName, `${file.exportName}__${definitionName.replace(/[^A-Za-z0-9]/g, "")}`, ]), ); for (const [definitionName, definitionSchema] of Object.entries(parsed.definitions ?? {})) { const compatibleDefinitionSchema = Codex0150DefinitionSchemas[definitionName] ?? applyCodex0151DefinitionCompatibility(file.exportName, definitionName, definitionSchema); aggregateSchemas[localDefinitionNames.get(definitionName)!] = stripNullDefaults( normalizeNullableTypes( rewriteExternalRefs( compatibleDefinitionSchema, localDefinitionNames, file.namespace, exportNameByQualifiedName, ), ), ); } const topLevelSchema: Record = {}; for (const [key, value] of Object.entries(parsed)) { if (key !== "definitions") { topLevelSchema[key] = value; } } aggregateSchemas[file.exportName] = stripNullDefaults( normalizeNullableTypes( rewriteExternalRefs( topLevelSchema, localDefinitionNames, file.namespace, exportNameByQualifiedName, ), ), ); } for (const [name, schema] of Object.entries(ManualSchemas)) { if (!(name in aggregateSchemas)) { aggregateSchemas[name] = stripNullDefaults(normalizeNullableTypes(schema)); } } const generator = makeJsonSchemaGenerator(); for (const [name, schema] of Object.entries(aggregateSchemas).toSorted(([left], [right]) => left.localeCompare(right), )) { generator.addSchema(name, addAsyncQuestionFields(schema) as never); } const generatedEntries = new Map(); const output = generator.generate("openapi-3.1", aggregateSchemas as never, false).trim(); if (output.length > 0) { for (const entry of collectSchemaEntries(output)) { if (!generatedEntries.has(entry.name)) { generatedEntries.set(entry.name, entry.code); } } } const generatedSchemaNames = new Set(generatedEntries.keys()); const clientRequestRaw = yield* fetchText( `https://raw.githubusercontent.com/openai/codex/${UPSTREAM_REF}/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts`, ); const clientNotificationRaw = yield* fetchText( `https://raw.githubusercontent.com/openai/codex/${UPSTREAM_REF}/codex-rs/app-server-protocol/schema/typescript/ClientNotification.ts`, ); const serverRequestRaw = yield* fetchText( `https://raw.githubusercontent.com/openai/codex/${UPSTREAM_REF}/codex-rs/app-server-protocol/schema/typescript/ServerRequest.ts`, ); const serverNotificationRaw = yield* fetchText( `https://raw.githubusercontent.com/openai/codex/${UPSTREAM_REF}/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts`, ); const clientRequestEntries = parseRequestEntries(clientRequestRaw); const clientNotificationEntries = parseNotificationEntries(clientNotificationRaw); const serverRequestEntries = parseRequestEntries(serverRequestRaw); const serverNotificationEntries = parseNotificationEntries(serverNotificationRaw); const prelude = [ "// This file is generated by the effect-codex-app-server package. Do not edit manually.", `// Upstream protocol ref: ${UPSTREAM_REF}`, "", ]; const schemaOutput = [ ...prelude, 'import * as Schema from "effect/Schema";', "", [...generatedEntries.values()].join("\n\n"), "", ].join("\n"); const metaOutput = [ ...prelude, 'import * as CodexSchema from "./schema.gen.ts";', "", renderMethodConstants("CLIENT_REQUEST_METHODS", clientRequestEntries), renderMethodConstants("CLIENT_NOTIFICATION_METHODS", clientNotificationEntries), renderMethodConstants("SERVER_REQUEST_METHODS", serverRequestEntries), renderMethodConstants("SERVER_NOTIFICATION_METHODS", serverNotificationEntries), "export type ClientRequestMethod = keyof typeof CLIENT_REQUEST_METHODS;", "export type ClientNotificationMethod = keyof typeof CLIENT_NOTIFICATION_METHODS;", "export type ServerRequestMethod = keyof typeof SERVER_REQUEST_METHODS;", "export type ServerNotificationMethod = keyof typeof SERVER_NOTIFICATION_METHODS;", "", renderTypeInterface("ClientRequestParamsByMethod", clientRequestEntries, (entry) => renderSchemaTypeReference( resolveSchemaTypeName(entry.paramsType ?? "undefined", generatedSchemaNames), ), ), renderTypeInterface("ClientRequestResponsesByMethod", clientRequestEntries, (entry) => renderSchemaTypeReference( resolveResponseTypeName(entry.method, entry.paramsType, generatedSchemaNames), ), ), renderTypeInterface("ClientNotificationParamsByMethod", clientNotificationEntries, (entry) => renderSchemaTypeReference( resolveSchemaTypeName(entry.paramsType ?? "undefined", generatedSchemaNames), ), ), renderTypeInterface("ServerRequestParamsByMethod", serverRequestEntries, (entry) => renderSchemaTypeReference( resolveSchemaTypeName(entry.paramsType ?? "undefined", generatedSchemaNames), ), ), renderTypeInterface("ServerRequestResponsesByMethod", serverRequestEntries, (entry) => renderSchemaTypeReference( resolveResponseTypeName(entry.method, entry.paramsType, generatedSchemaNames), ), ), renderTypeInterface("ServerNotificationParamsByMethod", serverNotificationEntries, (entry) => renderSchemaTypeReference( resolveSchemaTypeName(entry.paramsType ?? "undefined", generatedSchemaNames), ), ), renderSchemaMap("CLIENT_REQUEST_PARAMS", clientRequestEntries, (entry) => resolveSchemaTypeName(entry.paramsType ?? "undefined", generatedSchemaNames), ), renderSchemaMap("CLIENT_REQUEST_RESPONSES", clientRequestEntries, (entry) => resolveResponseTypeName(entry.method, entry.paramsType, generatedSchemaNames), ), renderSchemaMap("CLIENT_NOTIFICATION_PARAMS", clientNotificationEntries, (entry) => resolveSchemaTypeName(entry.paramsType ?? "undefined", generatedSchemaNames), ), renderSchemaMap("SERVER_REQUEST_PARAMS", serverRequestEntries, (entry) => resolveSchemaTypeName(entry.paramsType ?? "undefined", generatedSchemaNames), ), renderSchemaMap("SERVER_REQUEST_RESPONSES", serverRequestEntries, (entry) => resolveResponseTypeName(entry.method, entry.paramsType, generatedSchemaNames), ), renderSchemaMap("SERVER_NOTIFICATION_PARAMS", serverNotificationEntries, (entry) => resolveSchemaTypeName(entry.paramsType ?? "undefined", generatedSchemaNames), ), ].join("\n"); const namespaceGroups = new Map>(); for (const file of jsonSchemaFiles) { if (!file.namespace) { continue; } const current = namespaceGroups.get(file.namespace) ?? []; current.push(file); namespaceGroups.set(file.namespace, current); } const namespacesOutput = [ ...prelude, 'import * as CodexSchema from "./schema.gen.ts";', "", ...[...namespaceGroups.entries()] .toSorted(([left], [right]) => left.localeCompare(right)) .map(([namespace, files]) => { const constantName = namespace.replace(/[^A-Za-z0-9]/g, ""); return [ `export const ${constantName} = {`, ...files .toSorted((left, right) => left.fileName.localeCompare(right.fileName)) .map( (file) => ` ${JSON.stringify(file.fileName.replace(/\.json$/, ""))}: CodexSchema.${file.exportName},`, ), "} as const;", "", ].join("\n"); }), ].join("\n"); const fs = yield* FileSystem.FileSystem; const { generatedDir, metaOutputPath, namespacesOutputPath, schemaOutputPath } = yield* getGeneratedPaths(); yield* fs.writeFileString(schemaOutputPath, schemaOutput); yield* fs.writeFileString(metaOutputPath, metaOutput); yield* fs.writeFileString(namespacesOutputPath, namespacesOutput); yield* Effect.log(`Generated Codex App Server schemas from ${UPSTREAM_REF}`); yield* Effect.service(ChildProcessSpawner.ChildProcessSpawner).pipe( Effect.flatMap((spawner) => spawner.spawn(ChildProcess.make("vp", ["fmt", generatedDir, "--write"])), ), Effect.flatMap((child) => child.exitCode), Effect.tap((code) => code === 0 ? Effect.void : Effect.fail( new GeneratorError({ detail: `vp fmt failed with exit code ${code}`, }), ), ), ); }); generateFiles().pipe( Effect.scoped, Effect.provide( Layer.mergeAll( Logger.layer([Logger.consolePretty()]), NodeServices.layer, FetchHttpClient.layer, ), ), NodeRuntime.runMain, );