t3-code-android-nightly/packages/effect-codex-app-server/scripts/generate.ts
Julius Marminge 194c73f3f9
chore(deps): upgrade Effect to stable 4.0.1 (#16138)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 13:15:14 -07:00

867 lines
28 KiB
JavaScript

#!/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/http";
import { ChildProcess, ChildProcessSpawner } from "effect/process";
const UPSTREAM_REF = "687a119f0fcaace47e1f1abcc77cec6c813fd6da";
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.TaggedError<GeneratorError>()("GeneratorError", {
detail: Schema.String,
cause: Schema.optional(Schema.Defect()),
}) {
override get message() {
return this.detail;
}
}
const ManualSchemas: Record<string, Schema.Json> = {
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 adds plan slugs between our protocol refreshes (0.159 added `promax`).
// T3 Code only uses the plan for labels, so an unknown slug must not fail the
// whole `account/read` decode and take the provider down with it.
const DefinitionOverrides: Record<string, Schema.Json> = {
PlanType: { type: "string" },
};
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) {
// Unauthenticated GitHub API calls are capped at 60/hour; set GITHUB_TOKEN to lift that.
const token = process.env.GITHUB_TOKEN;
return yield* HttpClientRequest.get(url).pipe(
HttpClientRequest.setHeader("user-agent", USER_AGENT),
token && url.startsWith("https://api.github.com/")
? HttpClientRequest.bearerToken(token)
: (request) => request,
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<string, Schema.Json>;
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<string, Schema.Json> = {};
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;
}
type JsonSchemaNode = { readonly [key: string]: Schema.Json };
function isJsonSchemaNode(value: Schema.Json | undefined): value is JsonSchemaNode {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
// Adapts Codex's JSON Schema to Effect's importer, visiting only schema
// positions so a field literally named "properties" is left alone:
// - Effect imports an object without additionalProperties as an open record.
// Codex omits it for plain structs, so close objects that list properties.
// - Effect cannot intersect shared object fields with object alternatives,
// which Codex uses for "one of these keys plus shared fields"
// (image_url | file_id). Fold the shared fields into each alternative.
function adaptSchemaForEffect(value: Schema.Json): Schema.Json {
if (!isJsonSchemaNode(value)) {
return value;
}
const node: Record<string, Schema.Json> = { ...value };
for (const key of ["items", "additionalProperties"]) {
const child = node[key];
if (isJsonSchemaNode(child)) node[key] = adaptSchemaForEffect(child);
}
for (const key of ["anyOf", "oneOf", "allOf"]) {
const child = node[key];
if (Array.isArray(child)) node[key] = child.map(adaptSchemaForEffect);
}
for (const key of ["properties", "definitions"]) {
const child = node[key];
if (isJsonSchemaNode(child)) {
node[key] = Object.fromEntries(
Object.entries(child).map(([name, schema]) => [name, adaptSchemaForEffect(schema)]),
);
}
}
const { properties } = node;
if (
isJsonSchemaNode(properties) &&
Object.keys(properties).length > 0 &&
!("additionalProperties" in node)
) {
node.additionalProperties = false;
}
const alternativesKey = "anyOf" in node ? "anyOf" : "oneOf";
const alternatives = node[alternativesKey];
if (
!isJsonSchemaNode(properties) ||
!Array.isArray(alternatives) ||
!alternatives.every(
(alternative) => isJsonSchemaNode(alternative) && alternative.type === "object",
)
) {
return node;
}
const {
properties: _shared,
required,
type: _type,
additionalProperties: _closed,
...rest
} = node;
const sharedRequired = Array.isArray(required) ? required : [];
return {
...rest,
[alternativesKey]: alternatives.map((alternative) => {
const branch = alternative as JsonSchemaNode;
const branchProperties = isJsonSchemaNode(branch.properties) ? branch.properties : {};
const branchRequired = Array.isArray(branch.required) ? branch.required : [];
return {
...branch,
properties: { ...properties, ...branchProperties },
required: [...new Set([...sharedRequired, ...branchRequired])],
};
}),
};
}
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<MethodEntry> {
// Optional params render as `params?: Foo | undefined`; their JSON schema is `NullableFoo`.
const entryPattern = /\{\s*"method":\s*"([^"]+)",\s*id:\s*RequestId,\s*params(\??):\s*([^,}|]+)/g;
const entries: Array<MethodEntry> = [];
let match: RegExpExecArray | null;
while ((match = entryPattern.exec(fileContents)) !== null) {
entries.push({
method: match[1]!,
paramsType: `${match[2] ? "Nullable" : ""}${match[3]!.trim()}`,
});
}
return entries;
}
function parseNotificationEntries(fileContents: string): ReadonlyArray<MethodEntry> {
const entryPattern = /\{\s*"method":\s*"([^"]+)"(?:,\s*"params":\s*([^ }]+))?\s*\}/g;
const entries: Array<MethodEntry> = [];
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>,
): 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>,
): string {
const overrides: Record<string, string> = {
"account/gatewayOAuth/cancel": "GatewayOAuthCancelResponse",
"account/gatewayOAuth/login": "GatewayOAuthLoginResponse",
"account/gatewayOAuth/read": "GatewayOAuthReadResponse",
"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<MethodEntry>) {
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<MethodEntry>,
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<MethodEntry>,
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<GithubContentEntry>,
): ReadonlyArray<JsonSchemaFile> {
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<string, string>,
currentNamespace: string | undefined,
exportNameByQualifiedName: ReadonlyMap<string, string>,
): 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, `#/components/schemas/${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, `#/components/schemas/${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<string, Schema.Json> = {};
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 ?? {})) {
aggregateSchemas[localDefinitionNames.get(definitionName)!] = stripNullDefaults(
normalizeNullableTypes(
rewriteExternalRefs(
DefinitionOverrides[definitionName] ?? definitionSchema,
localDefinitionNames,
file.namespace,
exportNameByQualifiedName,
),
),
);
}
const topLevelSchema: Record<string, Schema.Json> = {};
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),
)) {
aggregateSchemas[name] = adaptSchemaForEffect(schema);
generator.addSchema(name, aggregateSchemas[name] as never);
}
const generatedEntries = new Map<string, string>();
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<string, Array<JsonSchemaFile>>();
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,
);