Files
cline/scripts/build-go-proto.mjs
T
097f8e6239 cline cli super alpha (#6644)
* super sketchy big merge with main

* gitignore

* gitignore

* Delete cli/bin/air

* Delete cli/bin directory

* Delete cli/cline-host

* Fix missing package.json in cli

Copy the package JSON into the dist-standalone dir during compilation.
Remove workaround for missing package.json

* Update scripts/build-cli.sh

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Remove reference to watchservice, it has been removed

* Update scripts/build-go-proto.mjs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix timestamp to string conversion

* diff.go ellipsis fix

* COMMON_TYPES

---------

Co-authored-by: Sarah Fortune <sarah.fortune@gmail.com>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-03 15:04:00 -07:00

600 lines
17 KiB
JavaScript

#!/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()
}