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`; const reserved = new Set([ "associatedtype", "class", "deinit", "enum", "extension", "fileprivate", "func", "import", "init", "inout", "internal", "let", "open", "operator", "private", "precedencegroup", "protocol", "public", "rethrows", "static", "struct", "subscript", "typealias", "var", ]); function camelCase(input: string) { return input .replace(/[^a-zA-Z0-9]+/g, " ") .trim() .toLowerCase() .split(/\s+/) .map((p, i) => (i === 0 ? p : p[0].toUpperCase() + p.slice(1))) .join(""); } function safeName(name: string) { const cc = camelCase(name.replace(/-/g, "_")); if (reserved.has(cc)) return `_${cc}`; return cc; } // filled later once schemas are loaded const schemaNameByObject = new Map(); function swiftType(schema: JsonSchema, required: boolean): string { const t = schema.type; const isOptional = !required; let base: string; const named = schemaNameByObject.get(schema as object); if (named) { base = named; } else 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 = "String"; } 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"); } const codingKeys: string[] = []; for (const [key, propSchema] of Object.entries(props)) { const propName = safeName(key); const propType = swiftType(propSchema, required.has(key)); lines.push(` public let ${propName}: ${propType}`); if (propName !== key) { codingKeys.push(` case ${propName} = "${key}"`); } else { codingKeys.push(` case ${propName}`); } } lines.push("\n public init(\n" + Object.entries(props) .map(([key, prop]) => { const propName = safeName(key); const req = required.has(key); return ` ${propName}: ${swiftType(prop, true)}${req ? "" : "?"}`; }) .join(",\n") + "\n ) {\n" + Object.entries(props) .map(([key]) => { const propName = safeName(key); return ` self.${propName} = ${propName}`; }) .join("\n") + "\n }\n" + " private enum CodingKeys: String, CodingKey {\n" + codingKeys.join("\n") + "\n }\n}"); lines.push(""); return lines.join("\n"); } function emitGatewayFrame(): string { const cases = ["req", "res", "event"]; const associated: Record = { req: "RequestFrame", res: "ResponseFrame", event: "EventFrame", }; const caseLines = cases.map((c) => ` case ${safeName(c)}(${associated[c]})`); const initLines = ` private enum CodingKeys: String, CodingKey { case type } public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) let type = try typeContainer.decode(String.self, forKey: .type) switch type { case "req": self = .req(try RequestFrame(from: decoder)) case "res": self = .res(try ResponseFrame(from: decoder)) case "event": self = .event(try EventFrame(from: decoder)) default: let container = try decoder.singleValueContainer() let raw = try container.decode([String: AnyCodable].self) self = .unknown(type: type, raw: raw) } } public func encode(to encoder: Encoder) throws { switch self { 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) } } `; return [ "public enum GatewayFrame: Codable {", ...caseLines, " case unknown(type: String, raw: [String: AnyCodable])", initLines, "}", "", ].join("\n"); } async function generate() { const definitions = Object.entries(ProtocolSchemas) as Array< [string, JsonSchema] >; for (const [name, schema] of definitions) { schemaNameByObject.set(schema as object, name); } 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); });