import { promises as fs } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { ErrorCodes, PROTOCOL_VERSION, ProtocolSchemas, } from "../src/gateway/protocol/schema.js"; type JsonSchema = { type?: string | string[]; properties?: Record; required?: string[]; items?: JsonSchema; enum?: string[]; patternProperties?: Record; }; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(__dirname, ".."); const outPath = path.join( repoRoot, "apps", "macos", "Sources", "ClawdisProtocol", "GatewayModels.swift", ); const header = `// Generated by scripts/protocol-gen-swift.ts — do not edit by hand\nimport Foundation\n\npublic let GATEWAY_PROTOCOL_VERSION = ${PROTOCOL_VERSION}\n\npublic enum ErrorCode: String, Codable {\n${Object.values(ErrorCodes) .map((c) => ` case ${camelCase(c)} = "${c}"`) .join("\n")}\n}\n`; function camelCase(input: string) { return input .toLowerCase() .split("_") .map((p, i) => (i === 0 ? p : p[0].toUpperCase() + p.slice(1))) .join(""); } function swiftType(schema: JsonSchema, required: boolean): string { const t = schema.type; const isOptional = !required; let base: string; if (t === "string") base = "String"; else if (t === "integer") base = "Int"; else if (t === "number") base = "Double"; else if (t === "boolean") base = "Bool"; else if (t === "array") { base = `[${swiftType(schema.items ?? { type: "Any" }, true)}]`; } else if (schema.enum) { base = schema.enum.map((v) => `\"${v}\"`).join(" | "); base = "String"; // simplify enums to String; custom enums could be added if needed } else if (schema.patternProperties) { base = "[String: AnyCodable]"; } else if (t === "object") { base = "[String: AnyCodable]"; } else { base = "AnyCodable"; } return isOptional ? `${base}?` : base; } function emitStruct(name: string, schema: JsonSchema): string { const props = schema.properties ?? {}; const required = new Set(schema.required ?? []); const lines: string[] = []; lines.push(`public struct ${name}: Codable {`); if (Object.keys(props).length === 0) { lines.push("}\n"); return lines.join("\n"); } for (const [key, propSchema] of Object.entries(props)) { const propName = key === "description" ? "desc" : key; const propType = swiftType(propSchema, required.has(key)); lines.push(` public let ${propName}: ${propType}`); } lines.push("\n public init(\n" + Object.entries(props) .map(([key, prop]) => { const propName = key === "description" ? "desc" : key; const req = required.has(key); return ` ${propName}: ${swiftType(prop, true)}${req ? "" : "?"}`; }) .join(",\n") + "\n ) {\n" + Object.entries(props) .map(([key]) => { const propName = key === "description" ? "desc" : key; return ` self.${propName} = ${propName}`; }) .join("\n") + "\n }\n}"); lines.push(""); return lines.join("\n"); } function emitGatewayFrame(): string { const cases = [ "hello", "hello-ok", "hello-error", "req", "res", "event", ]; const associated: Record = { hello: "Hello", "hello-ok": "HelloOk", "hello-error": "HelloError", req: "RequestFrame", res: "ResponseFrame", event: "EventFrame", }; const caseLines = cases.map((c) => ` case ${camelCase(c)}(${associated[c]})`); const initLines = ` public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let raw = try container.decode([String: AnyCodable].self) guard let type = raw["type"]?.value as? String else { throw DecodingError.dataCorruptedError(in: container, debugDescription: "missing type") } switch type { case "hello": self = .hello(try decodePayload(Hello.self, from: raw)) case "hello-ok": self = .helloOk(try decodePayload(HelloOk.self, from: raw)) case "hello-error": self = .helloError(try decodePayload(HelloError.self, from: raw)) case "req": self = .req(try decodePayload(RequestFrame.self, from: raw)) case "res": self = .res(try decodePayload(ResponseFrame.self, from: raw)) case "event": self = .event(try decodePayload(EventFrame.self, from: raw)) default: self = .unknown(type: type, raw: raw) } } public func encode(to encoder: Encoder) throws { switch self { case .hello(let v): try v.encode(to: encoder) case .helloOk(let v): try v.encode(to: encoder) case .helloError(let v): try v.encode(to: encoder) case .req(let v): try v.encode(to: encoder) case .res(let v): try v.encode(to: encoder) case .event(let v): try v.encode(to: encoder) case .unknown(_, let raw): var container = encoder.singleValueContainer() try container.encode(raw) } } `; const helper = ` private func decodePayload(_ type: T.Type, from raw: [String: AnyCodable]) throws -> T { let data = try JSONSerialization.data(withJSONObject: raw) let decoder = JSONDecoder() return try decoder.decode(T.self, from: data) } `; return [ "public enum GatewayFrame: Codable {", ...caseLines, " case unknown(type: String, raw: [String: AnyCodable])", initLines, helper, "}", "", ].join("\n"); } async function generate() { const definitions = Object.entries(ProtocolSchemas) as Array< [string, JsonSchema] >; const parts: string[] = []; parts.push(header); // Value structs for (const [name, schema] of definitions) { if (name === "GatewayFrame") continue; if (schema.type === "object") { parts.push(emitStruct(name, schema)); } } // Frame enum must come after payload structs parts.push(emitGatewayFrame()); const content = parts.join("\n"); await fs.mkdir(path.dirname(outPath), { recursive: true }); await fs.writeFile(outPath, content); console.log(`wrote ${outPath}`); } generate().catch((err) => { console.error(err); process.exit(1); });