import { execSync } from "child_process"; import { existsSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "fs"; import { join } from "path"; import { tmpdir } from "os"; import { createGenerator, type Config } from "ts-json-schema-generator"; import { createNormalizedSchema, type NormalizedSchema } from "./normalize.js"; import type { JSONSchema7 } from "json-schema"; const PI_SOURCE_URL = "https://codeload.github.com/badlogic/pi-mono/tar.gz/refs/heads/main"; const RPC_TYPES_PATH = "packages/coding-agent/src/modes/rpc/rpc-types.ts"; const TARGET_TYPES = ["RpcEvent", "RpcResponse", "RpcCommand"] as const; export async function extractPiSchema(): Promise { console.log("Extracting Pi schema from pi-mono sources..."); const tempDir = mkdtempSync(join(tmpdir(), "pi-schema-")); try { const archivePath = join(tempDir, "pi-mono.tar.gz"); await downloadToFile(PI_SOURCE_URL, archivePath); execSync(`tar -xzf "${archivePath}" -C "${tempDir}"`, { stdio: ["ignore", "ignore", "ignore"], }); const repoRoot = findRepoRoot(tempDir); const rpcTypesPath = join(repoRoot, RPC_TYPES_PATH); if (!existsSync(rpcTypesPath)) { throw new Error(`rpc-types.ts not found at ${rpcTypesPath}`); } const tsconfig = resolveTsconfig(repoRoot); const definitions = generateDefinitions(rpcTypesPath, tsconfig); if (Object.keys(definitions).length === 0) { console.log(" [warn] No schemas extracted from source, using fallback"); return createFallbackSchema(); } console.log(` [ok] Extracted ${Object.keys(definitions).length} types from source`); return createNormalizedSchema("pi", "Pi RPC Schema", definitions); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.log(` [warn] Pi schema extraction failed: ${errorMessage}`); console.log(" [fallback] Using embedded schema definitions"); return createFallbackSchema(); } finally { rmSync(tempDir, { recursive: true, force: true }); } } async function downloadToFile(url: string, filePath: string): Promise { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const buffer = Buffer.from(await response.arrayBuffer()); writeFileSync(filePath, buffer); } function findRepoRoot(root: string): string { const entries = readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()); const repoDir = entries.find((entry) => entry.name.startsWith("pi-mono")); if (!repoDir) { throw new Error("pi-mono source directory not found after extraction"); } return join(root, repoDir.name); } function resolveTsconfig(root: string): string | undefined { const candidates = [join(root, "tsconfig.json"), join(root, "tsconfig.base.json"), join(root, "packages", "coding-agent", "tsconfig.json")]; return candidates.find((path) => existsSync(path)); } function generateDefinitions(rpcTypesPath: string, tsconfigPath?: string): Record { const definitions: Record = {}; for (const typeName of TARGET_TYPES) { const config: Config = { path: rpcTypesPath, type: typeName, expose: "all", skipTypeCheck: false, topRef: false, ...(tsconfigPath ? { tsconfig: tsconfigPath } : {}), }; const schema = createGenerator(config).createSchema(typeName) as JSONSchema7; mergeDefinitions(definitions, schema, typeName); } return definitions; } function mergeDefinitions(target: Record, schema: JSONSchema7, typeName: string): void { if (schema.definitions) { for (const [name, def] of Object.entries(schema.definitions)) { target[name] = def as JSONSchema7; } } else if (schema.$defs) { for (const [name, def] of Object.entries(schema.$defs)) { target[name] = def as JSONSchema7; } } else { target[typeName] = schema; } if (!target[typeName]) { target[typeName] = schema; } } function createFallbackSchema(): NormalizedSchema { const definitions: Record = { RpcEvent: { type: "object", properties: { type: { type: "string" }, sessionId: { type: "string" }, messageId: { type: "string" }, message: { $ref: "#/definitions/RpcMessage" }, assistantMessageEvent: { $ref: "#/definitions/AssistantMessageEvent" }, toolCallId: { type: "string" }, toolName: { type: "string" }, args: {}, partialResult: {}, result: { $ref: "#/definitions/ToolResult" }, isError: { type: "boolean" }, error: {}, }, required: ["type"], }, RpcMessage: { type: "object", properties: { role: { type: "string" }, content: {}, }, }, AssistantMessageEvent: { type: "object", properties: { type: { type: "string" }, delta: { type: "string" }, content: {}, partial: {}, messageId: { type: "string" }, }, }, ToolResult: { type: "object", properties: { type: { type: "string" }, content: { type: "string" }, text: { type: "string" }, }, }, RpcResponse: { type: "object", properties: { type: { type: "string", const: "response" }, id: { type: "integer" }, success: { type: "boolean" }, data: {}, error: {}, }, required: ["type"], }, RpcCommand: { type: "object", properties: { type: { type: "string", const: "command" }, id: { type: "integer" }, command: { type: "string" }, params: {}, }, required: ["type", "command"], }, }; console.log(` [ok] Using fallback schema with ${Object.keys(definitions).length} definitions`); return createNormalizedSchema("pi", "Pi RPC Schema", definitions); }