772 lines · 24.2 KB · TypeScript
RawDownload
#!/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 { Effect, FileSystem, Layer, Logger, Path, Schema } from "effect";import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; const UPSTREAM_REF = "be75785504ff152fa6333e380a2d50642f42fba0";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]; 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>()("GeneratorError", {  detail: Schema.String,  cause: Schema.optional(Schema.Defect),}) {  override get message() {    return this.detail;  }} const ManualSchemas: Record<string, typeof Schema.Json.Type> = {  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"],  },}; 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) {  const response = yield* Effect.tryPromise({    try: () =>      fetch(url, {        headers: {          "user-agent": USER_AGENT,        },      }),    catch: (cause) =>      new GeneratorError({        detail: `Failed to fetch ${url}`,        cause,      }),  });   if (!response.ok) {    const detail = yield* Effect.tryPromise({      try: () => response.text(),      catch: () => "",    });    return yield* Effect.fail(      new GeneratorError({        detail: `Failed to download ${url}: ${response.status} ${detail}`,      }),    );  }   return yield* Effect.tryPromise({    try: () => response.text(),    catch: (cause) =>      new GeneratorError({        detail: `Failed to read response body for ${url}`,        cause,      }),  });}); const fetchDirectoryEntries = Effect.fn("fetchDirectoryEntries")(function* (path: string) {  const raw = yield* fetchText(`${GITHUB_API_BASE}/${path}?ref=${UPSTREAM_REF}`);  return yield* Schema.decodeEffect(Schema.fromJsonString(GithubContentEntries))(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: typeof Schema.Json.Type): typeof Schema.Json.Type {  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,    typeof Schema.Json.Type  >;  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, typeof Schema.Json.Type> = {};  for (const [key, child] of Object.entries(normalizedObject)) {    if (key !== "type") {      nextObject[key] = child;    }  }   return {    anyOf: [      {        ...nextObject,        type: nonNullType,      },      { type: "null" },    ],  };} function stripNullDefaults(value: typeof Schema.Json.Type): typeof Schema.Json.Type {  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 typeof Schema.Json.Type;} 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> {  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]!.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/logout": "LogoutAccountResponse",    "account/rateLimits/read": "GetAccountRateLimitsResponse",    "config/batchWrite": "ConfigWriteResponse",    "config/mcpServer/reload": "McpServerRefreshResponse",    "config/value/write": "ConfigWriteResponse",    "configRequirements/read": "ConfigRequirementsReadResponse",  };   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" : `typeof CodexSchema.${schemaName}.Type`;} 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: typeof Schema.Json.Type,  localDefinitionNames: ReadonlyMap<string, string>,  currentNamespace: string | undefined,  exportNameByQualifiedName: ReadonlyMap<string, string>,): typeof Schema.Json.Type {  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}`,

Lines 1–500 of 772.