#!/usr/bin/env node import path from "path" import { fileURLToPath } from "url" import { writeFileWithMkdirs } from "./file-utils.mjs" import { getFqn, loadServicesFromProtoDescriptor } from "./proto-utils.mjs" const WEBVIEW_CLIENTS_FILE = path.resolve("webview-ui/src/services/grpc-client.ts") const VSCODE_SERVICES_FILE = path.resolve("src/generated/hosts/vscode/protobus-services.ts") const VSCODE_SERVICE_TYPES_FILE = path.resolve("src/generated/hosts/vscode/protobus-service-types.ts") const STANDALONE_SERVER_SETUP_FILE = path.resolve("src/generated/hosts/standalone/protobus-server-setup.ts") const SCRIPT_NAME = path.relative(process.cwd(), fileURLToPath(import.meta.url)) export async function main() { const { protobusServices } = await loadServicesFromProtoDescriptor() await generateWebviewProtobusClients(protobusServices) await generateVscodeServiceTypes(protobusServices) await generateVscodeProtobusServers(protobusServices) await generateStandaloneProtobusServiceSetup(protobusServices) console.log(`Generated ProtoBus files at:`) console.log(`- ${WEBVIEW_CLIENTS_FILE}`) console.log(`- ${VSCODE_SERVICE_TYPES_FILE}`) console.log(`- ${VSCODE_SERVICES_FILE}`) console.log(`- ${STANDALONE_SERVER_SETUP_FILE}`) } async function generateWebviewProtobusClients(protobusServices) { const clients = [] for (const [serviceName, def] of Object.entries(protobusServices)) { const rpcs = [] for (const [rpcName, rpc] of Object.entries(def.service)) { const requestType = getFqn(rpc.requestType.type.name) const responseType = getFqn(rpc.responseType.type.name) if (rpc.requestStream) { throw new Error("Request streaming is not supported") } if (!rpc.responseStream) { rpcs.push(` static async ${rpcName}(request: ${requestType}): Promise<${responseType}> { return this.makeUnaryRequest("${rpcName}", request, ${requestType}.toJSON, ${responseType}.fromJSON) }`) } else { rpcs.push(` static ${rpcName}(request: ${requestType}, callbacks: Callbacks<${responseType}>): ()=>void { return this.makeStreamingRequest("${rpcName}", request, ${requestType}.toJSON, ${responseType}.fromJSON, callbacks) }`) } } clients.push(`export class ${serviceName}Client extends ProtoBusClient { static override serviceName: string = "cline.${serviceName}" ${rpcs.join("\n")} }`) } // Create output file const output = `// GENERATED CODE -- DO NOT EDIT! // Generated by ${SCRIPT_NAME} import * as proto from "@shared/proto/index" import { ProtoBusClient, Callbacks } from "./grpc-client-base" ${clients.join("\n")} ` // Write output file await writeFileWithMkdirs(WEBVIEW_CLIENTS_FILE, output) } /** * Generate imports and function to add all the handlers to the server for all services defined in the proto files. */ async function generateVscodeServiceTypes(protobusServices) { const servers = [] for (const [serviceName, def] of Object.entries(protobusServices)) { const domain = getDomainName(serviceName) servers.push(`// ${domain} Service Handler Types`) servers.push(`export type ${serviceName}Handlers = {`) for (const [rpcName, rpc] of Object.entries(def.service)) { const requestType = getFqn(rpc.requestType.type.name) const responseType = getFqn(rpc.responseType.type.name) if (rpc.requestStream) { throw new Error("Request streaming is not supported") } if (!rpc.responseStream) { servers.push(` ${rpcName}:(controller: Controller, request: ${requestType}) => Promise<${responseType}>`) } else { servers.push( ` ${rpcName}:(controller: Controller, request: ${requestType}, responseStream: StreamingResponseHandler<${responseType}>, requestId?: string) => Promise`, ) } } servers.push(`}\n`) } // Create output file const output = `// GENERATED CODE -- DO NOT EDIT! // Generated by ${SCRIPT_NAME} import * as proto from "@shared/proto/index" import { Controller } from "@core/controller" import { StreamingResponseHandler } from "@/core/controller/grpc-handler" ${servers.join("\n")} ` // Write output file await writeFileWithMkdirs(VSCODE_SERVICE_TYPES_FILE, output) } /** * Generate imports and function to add all the handlers to the server for all services defined in the proto files. */ async function generateVscodeProtobusServers(protobusServices) { const imports = [] const servers = [] const serviceMap = [] for (const [serviceName, def] of Object.entries(protobusServices)) { const domain = getDomainName(serviceName) const dir = getDirName(serviceName) imports.push(`// ${domain} Service`) servers.push(`const ${serviceName}Handlers: serviceTypes.${serviceName}Handlers = {`) for (const [rpcName, _rpc] of Object.entries(def.service)) { imports.push(`import { ${rpcName} } from "@core/controller/${dir}/${rpcName}"`) servers.push(` ${rpcName}: ${rpcName},`) } servers.push(`} \n`) serviceMap.push(` "cline.${serviceName}": ${serviceName}Handlers,`) imports.push("") } // Create output file const output = `// GENERATED CODE -- DO NOT EDIT! // Generated by ${SCRIPT_NAME} import * as serviceTypes from "src/generated/hosts/vscode/protobus-service-types" ${imports.join("\n")} ${servers.join("\n")} export const serviceHandlers: Record = { ${serviceMap.join("\n")} } ` // Write output file await writeFileWithMkdirs(VSCODE_SERVICES_FILE, output) } /** * Generate imports and function to add all the handlers to the server for all services defined in the proto files. */ async function generateStandaloneProtobusServiceSetup(protobusServices) { const imports = [] const handlerSetup = [] for (const [name, def] of Object.entries(protobusServices)) { const domain = getDomainName(name) const dir = getDirName(name) imports.push(`// ${domain} Service`) handlerSetup.push(` // ${domain} Service`) handlerSetup.push(` server.addService(cline.${name}Service, {`) for (const [rpcName, rpc] of Object.entries(def.service)) { imports.push(`import { ${rpcName} } from "@core/controller/${dir}/${rpcName}"`) const requestType = "cline." + rpc.requestType.type.name const responseType = "cline." + rpc.responseType.type.name if (rpc.requestStream) { throw new Error("Request streaming is not supported") } if (rpc.responseStream) { handlerSetup.push( ` ${rpcName}: wrapStreamingResponse<${requestType},${responseType}>(${rpcName}, controller),`, ) } else { handlerSetup.push(` ${rpcName}: wrapper<${requestType},${responseType}>(${rpcName}, controller),`) } } handlerSetup.push(` });`) imports.push("") handlerSetup.push("") } // Create output file const output = `// GENERATED CODE -- DO NOT EDIT! // Generated by ${SCRIPT_NAME} import * as grpc from "@grpc/grpc-js" import { cline } from "@generated/grpc-js" import { Controller } from "@core/controller" import { GrpcHandlerWrapper, GrpcStreamingResponseHandlerWrapper } from "@hosts/external/grpc-types" ${imports.join("\n")} export function addProtobusServices( server: grpc.Server, controller: Controller, wrapper: GrpcHandlerWrapper, wrapStreamingResponse: GrpcStreamingResponseHandlerWrapper, ): void { ${handlerSetup.join("\n")} } ` // Write output file await writeFileWithMkdirs(STANDALONE_SERVER_SETUP_FILE, output) } function getDomainName(serviceName) { return serviceName.replace(/Service$/, "") } function getDirName(serviceName) { const domain = getDomainName(serviceName) return domain.charAt(0).toLowerCase() + domain.slice(1) } // Only run main if this script is executed directly if (import.meta.url === `file://${process.argv[1]}`) { main().catch((error) => { console.error(chalk.red("Error:"), error) process.exit(1) }) }