build-go-proto.mjs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  1. #!/usr/bin/env node
  2. import chalk from "chalk"
  3. import { execSync } from "child_process"
  4. import * as fs from "fs/promises"
  5. import { globby } from "globby"
  6. import { createRequire } from "module"
  7. import * as path from "path"
  8. import { fileURLToPath } from "url"
  9. import { createServiceNameMap, parseProtoForServices } from "./proto-shared-utils.mjs"
  10. const require = createRequire(import.meta.url)
  11. const PROTOC = path.join(require.resolve("grpc-tools"), "../bin/protoc")
  12. const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url))
  13. const ROOT_DIR = path.resolve(SCRIPT_DIR, "..")
  14. const PROTO_DIR = path.resolve(ROOT_DIR, "proto")
  15. const GO_PROTO_DIR = path.join(ROOT_DIR, "src", "generated", "grpc-go")
  16. const GO_CLIENT_DIR = path.join(GO_PROTO_DIR, "client")
  17. const GO_SERVICE_CLIENT_DIR = path.join(GO_CLIENT_DIR, "services")
  18. const COMMON_TYPES = ["StringRequest", "EmptyRequest", "Empty", "String", "Int64Request", "KeyValuePair"]
  19. // Check if Go is installed
  20. function checkGoInstallation() {
  21. try {
  22. execSync("go version", { stdio: "pipe" })
  23. return true
  24. } catch (error) {
  25. return false
  26. }
  27. }
  28. // Check if a Go tool is available
  29. function checkGoTool(toolName) {
  30. try {
  31. execSync(`which ${toolName}`, { stdio: "pipe" })
  32. return true
  33. } catch (error) {
  34. // On Windows, 'which' might not be available, try 'where'
  35. try {
  36. execSync(`where ${toolName}`, { stdio: "pipe" })
  37. return true
  38. } catch (windowsError) {
  39. return false
  40. }
  41. }
  42. }
  43. // Install Go protobuf tools
  44. function installGoTools() {
  45. console.log(chalk.yellow("Installing Go protobuf tools..."))
  46. const tools = ["google.golang.org/protobuf/cmd/protoc-gen-go@latest", "google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest"]
  47. for (const tool of tools) {
  48. try {
  49. console.log(chalk.cyan(`Installing ${tool}...`))
  50. execSync(`GO111MODULE=on go install ${tool}`, {
  51. stdio: "inherit",
  52. env: { ...process.env, GO111MODULE: "on" },
  53. })
  54. } catch (error) {
  55. console.error(chalk.red(`Failed to install ${tool}:`), error.message)
  56. process.exit(1)
  57. }
  58. }
  59. console.log(chalk.green("Go protobuf tools installed successfully!"))
  60. }
  61. // Check if tools are in PATH and provide guidance
  62. function checkToolsInPath() {
  63. const tools = ["protoc-gen-go", "protoc-gen-go-grpc"]
  64. const missingTools = []
  65. for (const tool of tools) {
  66. if (!checkGoTool(tool)) {
  67. missingTools.push(tool)
  68. }
  69. }
  70. if (missingTools.length > 0) {
  71. console.log(chalk.yellow("Warning: Some Go protobuf tools are not in your PATH:"))
  72. missingTools.forEach((tool) => console.log(chalk.yellow(` - ${tool}`)))
  73. console.log()
  74. console.log(chalk.cyan("To fix this, add your Go bin directory to your PATH:"))
  75. // Get GOPATH and GOBIN
  76. let goPath, goBin
  77. try {
  78. goPath = execSync("go env GOPATH", { encoding: "utf8" }).trim()
  79. goBin = execSync("go env GOBIN", { encoding: "utf8" }).trim()
  80. } catch (error) {
  81. console.log(chalk.red("Could not determine Go paths. Please check your Go installation."))
  82. process.exit(1)
  83. }
  84. const binPath = goBin || path.join(goPath, "bin")
  85. if (process.platform === "win32") {
  86. console.log(chalk.cyan(` Windows (Command Prompt): set PATH=%PATH%;${binPath}`))
  87. console.log(chalk.cyan(` Windows (PowerShell): $env:PATH += ";${binPath}"`))
  88. console.log(chalk.cyan(` Or add "${binPath}" to your system PATH through System Properties`))
  89. } else {
  90. console.log(chalk.cyan(` Add this to your shell profile (~/.bashrc, ~/.zshrc, etc.):`))
  91. console.log(chalk.cyan(` export PATH="$PATH:${binPath}"`))
  92. console.log(chalk.cyan(` Then run: source ~/.bashrc (or restart your terminal)`))
  93. }
  94. console.log()
  95. // Try to continue anyway, as the tools might still work
  96. console.log(chalk.yellow("Attempting to continue anyway..."))
  97. }
  98. }
  99. // Setup Go dependencies
  100. async function setupGoDependencies() {
  101. console.log(chalk.cyan("Checking Go dependencies..."))
  102. // Check if Go is installed
  103. if (!checkGoInstallation()) {
  104. console.error(chalk.red("Error: Go is not installed or not in PATH."))
  105. console.error(chalk.red("Please install Go from https://golang.org/dl/ and ensure it's in your PATH."))
  106. process.exit(1)
  107. }
  108. console.log(chalk.green("✓ Go is installed"))
  109. // Check if protobuf tools are available
  110. const tools = ["protoc-gen-go", "protoc-gen-go-grpc"]
  111. const missingTools = tools.filter((tool) => !checkGoTool(tool))
  112. if (missingTools.length > 0) {
  113. console.log(chalk.yellow(`Missing Go protobuf tools: ${missingTools.join(", ")}`))
  114. installGoTools()
  115. } else {
  116. console.log(chalk.green("✓ Go protobuf tools are available"))
  117. }
  118. // Verify tools are in PATH
  119. checkToolsInPath()
  120. }
  121. export async function goProtoc(outDir, protoFiles) {
  122. // Setup dependencies first
  123. await setupGoDependencies()
  124. // Create output directory if it doesn't exist
  125. await fs.mkdir(outDir, { recursive: true })
  126. // Simple protoc command - proto files now have correct go_package paths
  127. const goProtocCommand = [
  128. PROTOC,
  129. `--proto_path="${PROTO_DIR}"`,
  130. `--go_out="${outDir}"`,
  131. `--go_opt=module=github.com/cline/grpc-go`,
  132. `--go-grpc_out="${outDir}"`,
  133. `--go-grpc_opt=module=github.com/cline/grpc-go`,
  134. ...protoFiles,
  135. ].join(" ")
  136. try {
  137. console.log(chalk.cyan(`Generating Go code in ${outDir}...`))
  138. execSync(goProtocCommand, { stdio: "inherit" })
  139. } catch (error) {
  140. console.error(chalk.red("Error generating Go code:"), error)
  141. // Provide additional help if the error might be related to missing tools
  142. if (error.message.includes("protoc-gen-go")) {
  143. console.log()
  144. console.log(chalk.yellow("This error might be caused by Go protobuf tools not being in your PATH."))
  145. console.log(chalk.yellow("Please ensure the tools are properly installed and accessible."))
  146. }
  147. process.exit(1)
  148. }
  149. await generateGoMod()
  150. await generateGoConnection()
  151. await generateGoClient()
  152. await generateGoServiceClients()
  153. }
  154. async function generateGoMod() {
  155. console.log(chalk.cyan("Generating Go module file..."))
  156. const goModContent = `module github.com/cline/grpc-go
  157. go 1.21
  158. require (
  159. google.golang.org/grpc v1.65.0
  160. google.golang.org/protobuf v1.34.2
  161. )
  162. require (
  163. golang.org/x/net v0.26.0 // indirect
  164. golang.org/x/sys v0.21.0 // indirect
  165. golang.org/x/text v0.16.0 // indirect
  166. google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect
  167. )
  168. `
  169. const goModPath = path.join(GO_PROTO_DIR, "go.mod")
  170. await fs.writeFile(goModPath, goModContent)
  171. console.log(chalk.green(`Generated Go module file at ${goModPath}`))
  172. }
  173. async function generateGoConnection() {
  174. console.log(chalk.cyan("Generating Go connection manager..."))
  175. // Create client directory if it doesn't exist
  176. await fs.mkdir(GO_CLIENT_DIR, { recursive: true })
  177. const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
  178. // Generated by scripts/build-go-proto.mjs
  179. package client
  180. import (
  181. "context"
  182. "fmt"
  183. "sync"
  184. "time"
  185. "google.golang.org/grpc"
  186. "google.golang.org/grpc/credentials/insecure"
  187. )
  188. // ConnectionConfig holds configuration for gRPC connection
  189. type ConnectionConfig struct {
  190. Address string
  191. Timeout time.Duration
  192. }
  193. // ConnectionManager manages gRPC connections
  194. type ConnectionManager struct {
  195. config *ConnectionConfig
  196. conn *grpc.ClientConn
  197. mutex sync.RWMutex
  198. }
  199. // NewConnectionManager creates a new connection manager
  200. func NewConnectionManager(config *ConnectionConfig) *ConnectionManager {
  201. if config.Timeout == 0 {
  202. config.Timeout = 30 * time.Second
  203. }
  204. return &ConnectionManager{
  205. config: config,
  206. }
  207. }
  208. // Connect establishes a gRPC connection
  209. func (cm *ConnectionManager) Connect(ctx context.Context) error {
  210. cm.mutex.Lock()
  211. defer cm.mutex.Unlock()
  212. if cm.conn != nil {
  213. return nil // Already connected
  214. }
  215. // Create context with timeout
  216. connectCtx, cancel := context.WithTimeout(ctx, cm.config.Timeout)
  217. defer cancel()
  218. // Establish gRPC connection
  219. conn, err := grpc.DialContext(connectCtx, cm.config.Address,
  220. grpc.WithTransportCredentials(insecure.NewCredentials()),
  221. grpc.WithBlock(),
  222. )
  223. if err != nil {
  224. return fmt.Errorf("failed to connect to %s: %w", cm.config.Address, err)
  225. }
  226. cm.conn = conn
  227. return nil
  228. }
  229. // Disconnect closes the gRPC connection
  230. func (cm *ConnectionManager) Disconnect() error {
  231. cm.mutex.Lock()
  232. defer cm.mutex.Unlock()
  233. if cm.conn == nil {
  234. return nil // Already disconnected
  235. }
  236. err := cm.conn.Close()
  237. cm.conn = nil
  238. return err
  239. }
  240. // GetConnection returns the current gRPC connection
  241. func (cm *ConnectionManager) GetConnection() *grpc.ClientConn {
  242. cm.mutex.RLock()
  243. defer cm.mutex.RUnlock()
  244. return cm.conn
  245. }
  246. // IsConnected returns true if connected
  247. func (cm *ConnectionManager) IsConnected() bool {
  248. cm.mutex.RLock()
  249. defer cm.mutex.RUnlock()
  250. return cm.conn != nil
  251. }
  252. `
  253. const connectionPath = path.join(GO_CLIENT_DIR, "connection.go")
  254. await fs.writeFile(connectionPath, content)
  255. console.log(chalk.green(`Generated Go connection manager at ${connectionPath}`))
  256. }
  257. async function generateGoClient() {
  258. console.log(chalk.cyan("Generating Go client..."))
  259. // Create client directory if it doesn't exist
  260. await fs.mkdir(GO_CLIENT_DIR, { recursive: true })
  261. // Get all proto files and parse services
  262. const protoFiles = await globby("**/*.proto", { cwd: PROTO_DIR })
  263. const services = await parseProtoForServices(protoFiles, PROTO_DIR)
  264. const serviceNameMap = createServiceNameMap(services)
  265. const serviceClients = Object.keys(serviceNameMap)
  266. .map(
  267. (name) =>
  268. `\t${name.charAt(0).toUpperCase() + name.slice(1)} *services.${name.charAt(0).toUpperCase() + name.slice(1)}Client`,
  269. )
  270. .join("\n")
  271. const serviceInitializers = Object.keys(serviceNameMap)
  272. .map(
  273. (name) =>
  274. `\tc.${name.charAt(0).toUpperCase() + name.slice(1)} = services.New${name.charAt(0).toUpperCase() + name.slice(1)}Client(conn)`,
  275. )
  276. .join("\n")
  277. const serviceNilOut = Object.keys(serviceNameMap)
  278. .map((name) => `\tc.${name.charAt(0).toUpperCase() + name.slice(1)} = nil`)
  279. .join("\n")
  280. const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
  281. // Generated by scripts/build-go-proto.mjs
  282. package client
  283. import (
  284. "context"
  285. "fmt"
  286. "sync"
  287. "google.golang.org/grpc"
  288. "github.com/cline/grpc-go/client/services"
  289. )
  290. // ClineClient provides a unified interface to all Cline services
  291. type ClineClient struct {
  292. connManager *ConnectionManager
  293. // Service clients
  294. ${serviceClients}
  295. // Connection state
  296. mutex sync.RWMutex
  297. connected bool
  298. }
  299. // NewClineClient creates a new unified Cline client
  300. func NewClineClient(address string) (*ClineClient, error) {
  301. config := &ConnectionConfig{
  302. Address: address,
  303. }
  304. connManager := NewConnectionManager(config)
  305. return &ClineClient{
  306. connManager: connManager,
  307. }, nil
  308. }
  309. // NewClineClientWithConfig creates a new Cline client with custom configuration
  310. func NewClineClientWithConfig(config *ConnectionConfig) (*ClineClient, error) {
  311. connManager := NewConnectionManager(config)
  312. return &ClineClient{
  313. connManager: connManager,
  314. }, nil
  315. }
  316. // Connect establishes connection to Cline Core and initializes service clients
  317. func (c *ClineClient) Connect(ctx context.Context) error {
  318. c.mutex.Lock()
  319. defer c.mutex.Unlock()
  320. if c.connected {
  321. return nil
  322. }
  323. // Establish gRPC connection
  324. if err := c.connManager.Connect(ctx); err != nil {
  325. return fmt.Errorf("failed to connect: %w", err)
  326. }
  327. // Initialize service clients
  328. conn := c.connManager.GetConnection()
  329. ${serviceInitializers}
  330. c.connected = true
  331. return nil
  332. }
  333. // Disconnect closes the connection to Cline Core
  334. func (c *ClineClient) Disconnect() error {
  335. c.mutex.Lock()
  336. defer c.mutex.Unlock()
  337. if !c.connected {
  338. return nil
  339. }
  340. err := c.connManager.Disconnect()
  341. c.connected = false
  342. // Clear service clients
  343. ${serviceNilOut}
  344. return err
  345. }
  346. // IsConnected returns true if the client is connected to Cline Core
  347. func (c *ClineClient) IsConnected() bool {
  348. c.mutex.RLock()
  349. defer c.mutex.RUnlock()
  350. return c.connected
  351. }
  352. // Reconnect closes the current connection and establishes a new one
  353. func (c *ClineClient) Reconnect(ctx context.Context) error {
  354. c.mutex.Lock()
  355. defer c.mutex.Unlock()
  356. // Disconnect first
  357. if c.connected {
  358. if err := c.connManager.Disconnect(); err != nil {
  359. return fmt.Errorf("failed to disconnect: %w", err)
  360. }
  361. c.connected = false
  362. }
  363. // Reconnect
  364. if err := c.connManager.Connect(ctx); err != nil {
  365. return fmt.Errorf("failed to reconnect: %w", err)
  366. }
  367. // Reinitialize service clients
  368. conn := c.connManager.GetConnection()
  369. ${serviceInitializers}
  370. c.connected = true
  371. return nil
  372. }
  373. // GetConnection returns the underlying gRPC connection
  374. func (c *ClineClient) GetConnection() *grpc.ClientConn {
  375. return c.connManager.GetConnection()
  376. }
  377. `
  378. const clientPath = path.join(GO_CLIENT_DIR, "cline_client.go")
  379. await fs.writeFile(clientPath, content)
  380. console.log(chalk.green(`Generated Go client at ${clientPath}`))
  381. }
  382. async function generateGoServiceClients() {
  383. console.log(chalk.cyan("Generating Go service clients..."))
  384. await fs.mkdir(GO_SERVICE_CLIENT_DIR, { recursive: true })
  385. const protoFiles = await globby("**/*.proto", { cwd: PROTO_DIR })
  386. const services = await parseProtoForServices(protoFiles, PROTO_DIR)
  387. for (const [serviceName, serviceDef] of Object.entries(services)) {
  388. const capitalizedServiceName = serviceName.charAt(0).toUpperCase() + serviceName.slice(1)
  389. const clientFileName = `${serviceName}_client.go`
  390. const clientPath = path.join(GO_SERVICE_CLIENT_DIR, clientFileName)
  391. const methods = serviceDef.methods
  392. .map((method) => {
  393. const capitalizedMethodName = method.name.charAt(0).toUpperCase() + method.name.slice(1)
  394. // Determine if types are from cline package (common types) or proto package (service-specific types)
  395. const requestTypeName = method.requestType.split(".").pop()
  396. const responseTypeName = method.responseType.split(".").pop()
  397. // Common types like StringRequest, Empty, etc. are in the cline package
  398. const requestType = COMMON_TYPES.includes(requestTypeName)
  399. ? `*cline.${requestTypeName}`
  400. : `*proto.${requestTypeName}`
  401. const responseType = COMMON_TYPES.includes(responseTypeName)
  402. ? `*cline.${responseTypeName}`
  403. : `*proto.${responseTypeName}`
  404. if (method.isResponseStreaming) {
  405. return `
  406. // ${capitalizedMethodName} subscribes to ${method.name} updates and returns a stream
  407. func (sc *${capitalizedServiceName}Client) ${capitalizedMethodName}(ctx context.Context, req ${requestType}) (proto.${serviceDef.name}_${capitalizedMethodName}Client, error) {
  408. stream, err := sc.client.${capitalizedMethodName}(ctx, req)
  409. if err != nil {
  410. return nil, fmt.Errorf("failed to subscribe to ${method.name}: %w", err)
  411. }
  412. return stream, nil
  413. }`
  414. } else {
  415. return `
  416. // ${capitalizedMethodName} retrieves the current application ${method.name}
  417. func (sc *${capitalizedServiceName}Client) ${capitalizedMethodName}(ctx context.Context, req ${requestType}) (${responseType}, error) {
  418. resp, err := sc.client.${capitalizedMethodName}(ctx, req)
  419. if err != nil {
  420. return nil, fmt.Errorf("failed to get latest ${method.name}: %w", err)
  421. }
  422. return resp, nil
  423. }`
  424. }
  425. })
  426. .join("\n")
  427. // Determine the correct proto import path based on the service location
  428. const protoImportPath =
  429. serviceDef.protoPackage === "host" ? '"github.com/cline/grpc-go/host"' : '"github.com/cline/grpc-go/cline"'
  430. // Check if we need to import cline package for common types
  431. const needsClineImport = serviceDef.methods.some((method) => {
  432. const requestTypeName = method.requestType.split(".").pop()
  433. const responseTypeName = method.responseType.split(".").pop()
  434. const commonTypes = ["StringRequest", "EmptyRequest", "Empty", "String", "Int64Request", "KeyValuePair"]
  435. return commonTypes.includes(requestTypeName) || commonTypes.includes(responseTypeName)
  436. })
  437. // Always import cline package if we need common types, regardless of service package
  438. const clineImport = needsClineImport ? ' cline "github.com/cline/grpc-go/cline"\n' : ""
  439. const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
  440. // Generated by scripts/build-go-proto.mjs
  441. package services
  442. import (
  443. "context"
  444. "fmt"
  445. ${clineImport} proto ${protoImportPath}
  446. "google.golang.org/grpc"
  447. )
  448. // ${capitalizedServiceName}Client wraps the generated ${serviceDef.name} gRPC client
  449. type ${capitalizedServiceName}Client struct {
  450. client proto.${serviceDef.name}Client
  451. }
  452. // New${capitalizedServiceName}Client creates a new ${capitalizedServiceName}Client
  453. func New${capitalizedServiceName}Client(conn *grpc.ClientConn) *${capitalizedServiceName}Client {
  454. return &${capitalizedServiceName}Client{
  455. client: proto.New${serviceDef.name}Client(conn),
  456. }
  457. }
  458. ${methods}
  459. `
  460. await fs.writeFile(clientPath, content)
  461. console.log(chalk.green(`Generated Go service client at ${clientPath}`))
  462. }
  463. }
  464. // Main execution block - run if this script is executed directly
  465. if (import.meta.url === `file://${process.argv[1]}`) {
  466. async function main() {
  467. try {
  468. console.log(chalk.cyan("Starting Go protobuf code generation..."))
  469. // Get all proto files
  470. const protoFiles = await globby("**/*.proto", { cwd: PROTO_DIR })
  471. console.log(chalk.cyan(`Found ${protoFiles.length} proto files`))
  472. // Set output directory for Go code - use the new location
  473. const goOutDir = GO_PROTO_DIR
  474. // Call the goProtoc function
  475. await goProtoc(goOutDir, protoFiles)
  476. console.log(chalk.green("✓ Go protobuf code generation completed successfully!"))
  477. } catch (error) {
  478. console.error(chalk.red("Error during Go protobuf generation:"), error)
  479. process.exit(1)
  480. }
  481. }
  482. main()
  483. }