| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599 |
- #!/usr/bin/env node
- import chalk from "chalk"
- import { execSync } from "child_process"
- import * as fs from "fs/promises"
- import { globby } from "globby"
- import { createRequire } from "module"
- import * as path from "path"
- import { fileURLToPath } from "url"
- import { createServiceNameMap, parseProtoForServices } from "./proto-shared-utils.mjs"
- const require = createRequire(import.meta.url)
- const PROTOC = path.join(require.resolve("grpc-tools"), "../bin/protoc")
- const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url))
- const ROOT_DIR = path.resolve(SCRIPT_DIR, "..")
- const PROTO_DIR = path.resolve(ROOT_DIR, "proto")
- const GO_PROTO_DIR = path.join(ROOT_DIR, "src", "generated", "grpc-go")
- const GO_CLIENT_DIR = path.join(GO_PROTO_DIR, "client")
- const GO_SERVICE_CLIENT_DIR = path.join(GO_CLIENT_DIR, "services")
- const COMMON_TYPES = ["StringRequest", "EmptyRequest", "Empty", "String", "Int64Request", "KeyValuePair"]
- // Check if Go is installed
- function checkGoInstallation() {
- try {
- execSync("go version", { stdio: "pipe" })
- return true
- } catch (error) {
- return false
- }
- }
- // Check if a Go tool is available
- function checkGoTool(toolName) {
- try {
- execSync(`which ${toolName}`, { stdio: "pipe" })
- return true
- } catch (error) {
- // On Windows, 'which' might not be available, try 'where'
- try {
- execSync(`where ${toolName}`, { stdio: "pipe" })
- return true
- } catch (windowsError) {
- return false
- }
- }
- }
- // Install Go protobuf tools
- function installGoTools() {
- console.log(chalk.yellow("Installing Go protobuf tools..."))
- const tools = ["google.golang.org/protobuf/cmd/protoc-gen-go@latest", "google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest"]
- for (const tool of tools) {
- try {
- console.log(chalk.cyan(`Installing ${tool}...`))
- execSync(`GO111MODULE=on go install ${tool}`, {
- stdio: "inherit",
- env: { ...process.env, GO111MODULE: "on" },
- })
- } catch (error) {
- console.error(chalk.red(`Failed to install ${tool}:`), error.message)
- process.exit(1)
- }
- }
- console.log(chalk.green("Go protobuf tools installed successfully!"))
- }
- // Check if tools are in PATH and provide guidance
- function checkToolsInPath() {
- const tools = ["protoc-gen-go", "protoc-gen-go-grpc"]
- const missingTools = []
- for (const tool of tools) {
- if (!checkGoTool(tool)) {
- missingTools.push(tool)
- }
- }
- if (missingTools.length > 0) {
- console.log(chalk.yellow("Warning: Some Go protobuf tools are not in your PATH:"))
- missingTools.forEach((tool) => console.log(chalk.yellow(` - ${tool}`)))
- console.log()
- console.log(chalk.cyan("To fix this, add your Go bin directory to your PATH:"))
- // Get GOPATH and GOBIN
- let goPath, goBin
- try {
- goPath = execSync("go env GOPATH", { encoding: "utf8" }).trim()
- goBin = execSync("go env GOBIN", { encoding: "utf8" }).trim()
- } catch (error) {
- console.log(chalk.red("Could not determine Go paths. Please check your Go installation."))
- process.exit(1)
- }
- const binPath = goBin || path.join(goPath, "bin")
- if (process.platform === "win32") {
- console.log(chalk.cyan(` Windows (Command Prompt): set PATH=%PATH%;${binPath}`))
- console.log(chalk.cyan(` Windows (PowerShell): $env:PATH += ";${binPath}"`))
- console.log(chalk.cyan(` Or add "${binPath}" to your system PATH through System Properties`))
- } else {
- console.log(chalk.cyan(` Add this to your shell profile (~/.bashrc, ~/.zshrc, etc.):`))
- console.log(chalk.cyan(` export PATH="$PATH:${binPath}"`))
- console.log(chalk.cyan(` Then run: source ~/.bashrc (or restart your terminal)`))
- }
- console.log()
- // Try to continue anyway, as the tools might still work
- console.log(chalk.yellow("Attempting to continue anyway..."))
- }
- }
- // Setup Go dependencies
- async function setupGoDependencies() {
- console.log(chalk.cyan("Checking Go dependencies..."))
- // Check if Go is installed
- if (!checkGoInstallation()) {
- console.error(chalk.red("Error: Go is not installed or not in PATH."))
- console.error(chalk.red("Please install Go from https://golang.org/dl/ and ensure it's in your PATH."))
- process.exit(1)
- }
- console.log(chalk.green("✓ Go is installed"))
- // Check if protobuf tools are available
- const tools = ["protoc-gen-go", "protoc-gen-go-grpc"]
- const missingTools = tools.filter((tool) => !checkGoTool(tool))
- if (missingTools.length > 0) {
- console.log(chalk.yellow(`Missing Go protobuf tools: ${missingTools.join(", ")}`))
- installGoTools()
- } else {
- console.log(chalk.green("✓ Go protobuf tools are available"))
- }
- // Verify tools are in PATH
- checkToolsInPath()
- }
- export async function goProtoc(outDir, protoFiles) {
- // Setup dependencies first
- await setupGoDependencies()
- // Create output directory if it doesn't exist
- await fs.mkdir(outDir, { recursive: true })
- // Simple protoc command - proto files now have correct go_package paths
- const goProtocCommand = [
- PROTOC,
- `--proto_path="${PROTO_DIR}"`,
- `--go_out="${outDir}"`,
- `--go_opt=module=github.com/cline/grpc-go`,
- `--go-grpc_out="${outDir}"`,
- `--go-grpc_opt=module=github.com/cline/grpc-go`,
- ...protoFiles,
- ].join(" ")
- try {
- console.log(chalk.cyan(`Generating Go code in ${outDir}...`))
- execSync(goProtocCommand, { stdio: "inherit" })
- } catch (error) {
- console.error(chalk.red("Error generating Go code:"), error)
- // Provide additional help if the error might be related to missing tools
- if (error.message.includes("protoc-gen-go")) {
- console.log()
- console.log(chalk.yellow("This error might be caused by Go protobuf tools not being in your PATH."))
- console.log(chalk.yellow("Please ensure the tools are properly installed and accessible."))
- }
- process.exit(1)
- }
- await generateGoMod()
- await generateGoConnection()
- await generateGoClient()
- await generateGoServiceClients()
- }
- async function generateGoMod() {
- console.log(chalk.cyan("Generating Go module file..."))
- const goModContent = `module github.com/cline/grpc-go
- go 1.21
- require (
- google.golang.org/grpc v1.65.0
- google.golang.org/protobuf v1.34.2
- )
- require (
- golang.org/x/net v0.26.0 // indirect
- golang.org/x/sys v0.21.0 // indirect
- golang.org/x/text v0.16.0 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect
- )
- `
- const goModPath = path.join(GO_PROTO_DIR, "go.mod")
- await fs.writeFile(goModPath, goModContent)
- console.log(chalk.green(`Generated Go module file at ${goModPath}`))
- }
- async function generateGoConnection() {
- console.log(chalk.cyan("Generating Go connection manager..."))
- // Create client directory if it doesn't exist
- await fs.mkdir(GO_CLIENT_DIR, { recursive: true })
- const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
- // Generated by scripts/build-go-proto.mjs
- package client
- import (
- "context"
- "fmt"
- "sync"
- "time"
- "google.golang.org/grpc"
- "google.golang.org/grpc/credentials/insecure"
- )
- // ConnectionConfig holds configuration for gRPC connection
- type ConnectionConfig struct {
- Address string
- Timeout time.Duration
- }
- // ConnectionManager manages gRPC connections
- type ConnectionManager struct {
- config *ConnectionConfig
- conn *grpc.ClientConn
- mutex sync.RWMutex
- }
- // NewConnectionManager creates a new connection manager
- func NewConnectionManager(config *ConnectionConfig) *ConnectionManager {
- if config.Timeout == 0 {
- config.Timeout = 30 * time.Second
- }
- return &ConnectionManager{
- config: config,
- }
- }
- // Connect establishes a gRPC connection
- func (cm *ConnectionManager) Connect(ctx context.Context) error {
- cm.mutex.Lock()
- defer cm.mutex.Unlock()
- if cm.conn != nil {
- return nil // Already connected
- }
- // Create context with timeout
- connectCtx, cancel := context.WithTimeout(ctx, cm.config.Timeout)
- defer cancel()
- // Establish gRPC connection
- conn, err := grpc.DialContext(connectCtx, cm.config.Address,
- grpc.WithTransportCredentials(insecure.NewCredentials()),
- grpc.WithBlock(),
- )
- if err != nil {
- return fmt.Errorf("failed to connect to %s: %w", cm.config.Address, err)
- }
- cm.conn = conn
- return nil
- }
- // Disconnect closes the gRPC connection
- func (cm *ConnectionManager) Disconnect() error {
- cm.mutex.Lock()
- defer cm.mutex.Unlock()
- if cm.conn == nil {
- return nil // Already disconnected
- }
- err := cm.conn.Close()
- cm.conn = nil
- return err
- }
- // GetConnection returns the current gRPC connection
- func (cm *ConnectionManager) GetConnection() *grpc.ClientConn {
- cm.mutex.RLock()
- defer cm.mutex.RUnlock()
- return cm.conn
- }
- // IsConnected returns true if connected
- func (cm *ConnectionManager) IsConnected() bool {
- cm.mutex.RLock()
- defer cm.mutex.RUnlock()
- return cm.conn != nil
- }
- `
- const connectionPath = path.join(GO_CLIENT_DIR, "connection.go")
- await fs.writeFile(connectionPath, content)
- console.log(chalk.green(`Generated Go connection manager at ${connectionPath}`))
- }
- async function generateGoClient() {
- console.log(chalk.cyan("Generating Go client..."))
- // Create client directory if it doesn't exist
- await fs.mkdir(GO_CLIENT_DIR, { recursive: true })
- // Get all proto files and parse services
- const protoFiles = await globby("**/*.proto", { cwd: PROTO_DIR })
- const services = await parseProtoForServices(protoFiles, PROTO_DIR)
- const serviceNameMap = createServiceNameMap(services)
- const serviceClients = Object.keys(serviceNameMap)
- .map(
- (name) =>
- `\t${name.charAt(0).toUpperCase() + name.slice(1)} *services.${name.charAt(0).toUpperCase() + name.slice(1)}Client`,
- )
- .join("\n")
- const serviceInitializers = Object.keys(serviceNameMap)
- .map(
- (name) =>
- `\tc.${name.charAt(0).toUpperCase() + name.slice(1)} = services.New${name.charAt(0).toUpperCase() + name.slice(1)}Client(conn)`,
- )
- .join("\n")
- const serviceNilOut = Object.keys(serviceNameMap)
- .map((name) => `\tc.${name.charAt(0).toUpperCase() + name.slice(1)} = nil`)
- .join("\n")
- const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
- // Generated by scripts/build-go-proto.mjs
- package client
- import (
- "context"
- "fmt"
- "sync"
- "google.golang.org/grpc"
- "github.com/cline/grpc-go/client/services"
- )
- // ClineClient provides a unified interface to all Cline services
- type ClineClient struct {
- connManager *ConnectionManager
-
- // Service clients
- ${serviceClients}
-
- // Connection state
- mutex sync.RWMutex
- connected bool
- }
- // NewClineClient creates a new unified Cline client
- func NewClineClient(address string) (*ClineClient, error) {
- config := &ConnectionConfig{
- Address: address,
- }
-
- connManager := NewConnectionManager(config)
-
- return &ClineClient{
- connManager: connManager,
- }, nil
- }
- // NewClineClientWithConfig creates a new Cline client with custom configuration
- func NewClineClientWithConfig(config *ConnectionConfig) (*ClineClient, error) {
- connManager := NewConnectionManager(config)
-
- return &ClineClient{
- connManager: connManager,
- }, nil
- }
- // Connect establishes connection to Cline Core and initializes service clients
- func (c *ClineClient) Connect(ctx context.Context) error {
- c.mutex.Lock()
- defer c.mutex.Unlock()
-
- if c.connected {
- return nil
- }
-
- // Establish gRPC connection
- if err := c.connManager.Connect(ctx); err != nil {
- return fmt.Errorf("failed to connect: %w", err)
- }
-
- // Initialize service clients
- conn := c.connManager.GetConnection()
- ${serviceInitializers}
-
- c.connected = true
- return nil
- }
- // Disconnect closes the connection to Cline Core
- func (c *ClineClient) Disconnect() error {
- c.mutex.Lock()
- defer c.mutex.Unlock()
-
- if !c.connected {
- return nil
- }
-
- err := c.connManager.Disconnect()
- c.connected = false
-
- // Clear service clients
- ${serviceNilOut}
-
- return err
- }
- // IsConnected returns true if the client is connected to Cline Core
- func (c *ClineClient) IsConnected() bool {
- c.mutex.RLock()
- defer c.mutex.RUnlock()
- return c.connected
- }
- // Reconnect closes the current connection and establishes a new one
- func (c *ClineClient) Reconnect(ctx context.Context) error {
- c.mutex.Lock()
- defer c.mutex.Unlock()
-
- // Disconnect first
- if c.connected {
- if err := c.connManager.Disconnect(); err != nil {
- return fmt.Errorf("failed to disconnect: %w", err)
- }
- c.connected = false
- }
-
- // Reconnect
- if err := c.connManager.Connect(ctx); err != nil {
- return fmt.Errorf("failed to reconnect: %w", err)
- }
-
- // Reinitialize service clients
- conn := c.connManager.GetConnection()
- ${serviceInitializers}
-
- c.connected = true
- return nil
- }
- // GetConnection returns the underlying gRPC connection
- func (c *ClineClient) GetConnection() *grpc.ClientConn {
- return c.connManager.GetConnection()
- }
- `
- const clientPath = path.join(GO_CLIENT_DIR, "cline_client.go")
- await fs.writeFile(clientPath, content)
- console.log(chalk.green(`Generated Go client at ${clientPath}`))
- }
- async function generateGoServiceClients() {
- console.log(chalk.cyan("Generating Go service clients..."))
- await fs.mkdir(GO_SERVICE_CLIENT_DIR, { recursive: true })
- const protoFiles = await globby("**/*.proto", { cwd: PROTO_DIR })
- const services = await parseProtoForServices(protoFiles, PROTO_DIR)
- for (const [serviceName, serviceDef] of Object.entries(services)) {
- const capitalizedServiceName = serviceName.charAt(0).toUpperCase() + serviceName.slice(1)
- const clientFileName = `${serviceName}_client.go`
- const clientPath = path.join(GO_SERVICE_CLIENT_DIR, clientFileName)
- const methods = serviceDef.methods
- .map((method) => {
- const capitalizedMethodName = method.name.charAt(0).toUpperCase() + method.name.slice(1)
- // Determine if types are from cline package (common types) or proto package (service-specific types)
- const requestTypeName = method.requestType.split(".").pop()
- const responseTypeName = method.responseType.split(".").pop()
- // Common types like StringRequest, Empty, etc. are in the cline package
- const requestType = COMMON_TYPES.includes(requestTypeName)
- ? `*cline.${requestTypeName}`
- : `*proto.${requestTypeName}`
- const responseType = COMMON_TYPES.includes(responseTypeName)
- ? `*cline.${responseTypeName}`
- : `*proto.${responseTypeName}`
- if (method.isResponseStreaming) {
- return `
- // ${capitalizedMethodName} subscribes to ${method.name} updates and returns a stream
- func (sc *${capitalizedServiceName}Client) ${capitalizedMethodName}(ctx context.Context, req ${requestType}) (proto.${serviceDef.name}_${capitalizedMethodName}Client, error) {
- stream, err := sc.client.${capitalizedMethodName}(ctx, req)
- if err != nil {
- return nil, fmt.Errorf("failed to subscribe to ${method.name}: %w", err)
- }
- return stream, nil
- }`
- } else {
- return `
- // ${capitalizedMethodName} retrieves the current application ${method.name}
- func (sc *${capitalizedServiceName}Client) ${capitalizedMethodName}(ctx context.Context, req ${requestType}) (${responseType}, error) {
- resp, err := sc.client.${capitalizedMethodName}(ctx, req)
- if err != nil {
- return nil, fmt.Errorf("failed to get latest ${method.name}: %w", err)
- }
- return resp, nil
- }`
- }
- })
- .join("\n")
- // Determine the correct proto import path based on the service location
- const protoImportPath =
- serviceDef.protoPackage === "host" ? '"github.com/cline/grpc-go/host"' : '"github.com/cline/grpc-go/cline"'
- // Check if we need to import cline package for common types
- const needsClineImport = serviceDef.methods.some((method) => {
- const requestTypeName = method.requestType.split(".").pop()
- const responseTypeName = method.responseType.split(".").pop()
- const commonTypes = ["StringRequest", "EmptyRequest", "Empty", "String", "Int64Request", "KeyValuePair"]
- return commonTypes.includes(requestTypeName) || commonTypes.includes(responseTypeName)
- })
- // Always import cline package if we need common types, regardless of service package
- const clineImport = needsClineImport ? ' cline "github.com/cline/grpc-go/cline"\n' : ""
- const content = `// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
- // Generated by scripts/build-go-proto.mjs
- package services
- import (
- "context"
- "fmt"
- ${clineImport} proto ${protoImportPath}
- "google.golang.org/grpc"
- )
- // ${capitalizedServiceName}Client wraps the generated ${serviceDef.name} gRPC client
- type ${capitalizedServiceName}Client struct {
- client proto.${serviceDef.name}Client
- }
- // New${capitalizedServiceName}Client creates a new ${capitalizedServiceName}Client
- func New${capitalizedServiceName}Client(conn *grpc.ClientConn) *${capitalizedServiceName}Client {
- return &${capitalizedServiceName}Client{
- client: proto.New${serviceDef.name}Client(conn),
- }
- }
- ${methods}
- `
- await fs.writeFile(clientPath, content)
- console.log(chalk.green(`Generated Go service client at ${clientPath}`))
- }
- }
- // Main execution block - run if this script is executed directly
- if (import.meta.url === `file://${process.argv[1]}`) {
- async function main() {
- try {
- console.log(chalk.cyan("Starting Go protobuf code generation..."))
- // Get all proto files
- const protoFiles = await globby("**/*.proto", { cwd: PROTO_DIR })
- console.log(chalk.cyan(`Found ${protoFiles.length} proto files`))
- // Set output directory for Go code - use the new location
- const goOutDir = GO_PROTO_DIR
- // Call the goProtoc function
- await goProtoc(goOutDir, protoFiles)
- console.log(chalk.green("✓ Go protobuf code generation completed successfully!"))
- } catch (error) {
- console.error(chalk.red("Error during Go protobuf generation:"), error)
- process.exit(1)
- }
- }
- main()
- }
|