mirror of
https://github.com/cline/cline.git
synced 2026-09-01 23:19:18 +08:00
Compare commits
22 Commits
main
...
cli-windows
| Author | SHA1 | Date | |
|---|---|---|---|
| 570003eaa6 | |||
| d4733706d8 | |||
| a727d5c347 | |||
| ae306adc42 | |||
| 2f3002d215 | |||
| 41404e1a13 | |||
| d6244efda1 | |||
| be410c2927 | |||
| 6c67a0c8fc | |||
| 31e3c082ec | |||
| a75f4a63c8 | |||
| 0b059cf36f | |||
| 7699c52abf | |||
| 24281d98e1 | |||
| 0af7d0911d | |||
| 83af73a5f9 | |||
| 9d875e6222 | |||
| 125e2ad01f | |||
| fd46c97825 | |||
| 7426513245 | |||
| f49a856ee4 | |||
| 24568fed0d |
@@ -5,6 +5,7 @@ node_modules
|
||||
tmp
|
||||
.vscode-test/
|
||||
*.vsix
|
||||
/pkg
|
||||
|
||||
.DS_Store
|
||||
.idea
|
||||
|
||||
+7
-1
@@ -70,7 +70,8 @@
|
||||
"noControlCharactersInRegex": "off",
|
||||
"noShadowRestrictedNames": "off",
|
||||
"noArrayIndexKey": "info",
|
||||
"noAssignInExpressions": "info"
|
||||
"noAssignInExpressions": "info",
|
||||
"useIterableCallbackReturn": "off"
|
||||
},
|
||||
"complexity": {
|
||||
"noUselessConstructor": "off",
|
||||
@@ -111,6 +112,11 @@
|
||||
"expand": "always"
|
||||
}
|
||||
},
|
||||
"css": {
|
||||
"parser": {
|
||||
"tailwindDirectives": true
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"includes": [
|
||||
"**",
|
||||
|
||||
+2
-1
@@ -59,7 +59,8 @@
|
||||
},
|
||||
"os": [
|
||||
"darwin",
|
||||
"linux"
|
||||
"linux",
|
||||
"win32"
|
||||
],
|
||||
"cpu": [
|
||||
"x64",
|
||||
|
||||
@@ -3,15 +3,9 @@ package global
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/cline/cli/pkg/common"
|
||||
"github.com/cline/grpc-go/cline"
|
||||
)
|
||||
|
||||
// ClineClients manages Cline instances using the new registry system
|
||||
@@ -242,269 +236,3 @@ func (c *ClineClients) EnsureInstanceAtAddress(ctx context.Context, address stri
|
||||
return fmt.Errorf("cannot start remote instance at %s", normalized)
|
||||
}
|
||||
|
||||
func startClineHost(hostPort int, workspaces []string) (*exec.Cmd, error) {
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Starting cline-host on port %d\n", hostPort)
|
||||
}
|
||||
|
||||
// Get the directory where the cline binary is located
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get executable path: %w", err)
|
||||
}
|
||||
binDir := path.Dir(execPath)
|
||||
clineHostPath := path.Join(binDir, "cline-host")
|
||||
|
||||
// Build command arguments
|
||||
args := []string{
|
||||
"--verbose",
|
||||
"--port", fmt.Sprintf("%d", hostPort),
|
||||
}
|
||||
|
||||
for _, ws := range workspaces {
|
||||
args = append(args, "--workspace", ws)
|
||||
}
|
||||
|
||||
// Start the cline-host process
|
||||
cmd := exec.Command(clineHostPath, args...)
|
||||
|
||||
// Create logs directory in ~/.cline/logs
|
||||
logsDir := path.Join(Config.ConfigPath, "logs")
|
||||
if err := os.MkdirAll(logsDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create logs directory: %w", err)
|
||||
}
|
||||
|
||||
// Create timestamped log file
|
||||
timestamp := time.Now().Format("2006-01-02-15-04-05")
|
||||
logFileName := fmt.Sprintf("cline-host-%s-localhost-%d.log", timestamp, hostPort)
|
||||
logFilePath := path.Join(logsDir, logFileName)
|
||||
logFile, err := os.Create(logFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create log file: %w", err)
|
||||
}
|
||||
|
||||
// Redirect stdout and stderr to log file
|
||||
cmd.Stdout = logFile
|
||||
cmd.Stderr = logFile
|
||||
|
||||
// Put the child process in a new process group so Ctrl+C doesn't kill it
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
logFile.Close()
|
||||
return nil, fmt.Errorf("failed to start cline-host: %w", err)
|
||||
}
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Started cline-host (PID: %d)\n", cmd.Process.Pid)
|
||||
fmt.Printf("Logging cline-host output to: %s\n", logFilePath)
|
||||
}
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
// KillInstanceByAddress kills a Cline instance by its address
|
||||
func KillInstanceByAddress(ctx context.Context, registry *ClientRegistry, address string) error {
|
||||
// Check if the instance exists in the registry
|
||||
_, err := registry.GetInstance(address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("instance %s not found in registry", address)
|
||||
}
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Killing instance: %s\n", address)
|
||||
}
|
||||
|
||||
// Get gRPC client and process info
|
||||
client, err := registry.GetClient(ctx, address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to instance %s: %w", address, err)
|
||||
}
|
||||
|
||||
processInfo, err := client.State.GetProcessInfo(ctx, &cline.EmptyRequest{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get process info for instance %s: %w", address, err)
|
||||
}
|
||||
|
||||
pid := int(processInfo.ProcessId)
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Terminating process PID %d...\n", pid)
|
||||
}
|
||||
|
||||
// Kill the process
|
||||
if err := syscall.Kill(pid, syscall.SIGTERM); err != nil {
|
||||
return fmt.Errorf("failed to kill process %d: %w", pid, err)
|
||||
}
|
||||
|
||||
// Wait for the instance to remove itself from registry
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Waiting for instance to clean up registry entry...\n")
|
||||
}
|
||||
for range 5 {
|
||||
time.Sleep(1 * time.Second)
|
||||
if !registry.HasInstanceAtAddress(address) {
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Instance %s successfully killed and removed from registry.\n", address)
|
||||
}
|
||||
|
||||
// Update default instance if needed
|
||||
instances, err := registry.ListInstancesCleaned(ctx)
|
||||
if err == nil && len(instances) > 0 {
|
||||
// ensureDefaultInstance logic will handle setting a new default
|
||||
defaultInstance := registry.GetDefaultInstance()
|
||||
if defaultInstance == address || defaultInstance == "" {
|
||||
if len(instances) > 0 {
|
||||
if err := registry.SetDefaultInstance(instances[0].Address); err == nil {
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Updated default instance to: %s\n", instances[0].Address)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("instance killed but failed to remove itself from registry within 5 seconds")
|
||||
}
|
||||
|
||||
func startClineCore(corePort, hostPort int) (*exec.Cmd, error) {
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Starting cline-core on port %d (with hostbridge on %d)\n", corePort, hostPort)
|
||||
}
|
||||
|
||||
// Get the executable path and resolve symlinks (for npm global installs)
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get executable path: %w", err)
|
||||
}
|
||||
|
||||
// Resolve symlinks to get the real path
|
||||
// For npm global installs, execPath might be a symlink like:
|
||||
// /opt/homebrew/bin/cline -> /opt/homebrew/lib/node_modules/cline/bin/cline
|
||||
realPath, err := filepath.EvalSymlinks(execPath)
|
||||
if err != nil {
|
||||
// If we can't resolve symlinks, fall back to the original path
|
||||
realPath = execPath
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Warning: Could not resolve symlinks for %s: %v\n", execPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
binDir := path.Dir(realPath)
|
||||
installDir := path.Dir(binDir)
|
||||
clineCorePath := path.Join(installDir, "cline-core.js")
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Executable path: %s\n", execPath)
|
||||
if realPath != execPath {
|
||||
fmt.Printf("Real path (after resolving symlinks): %s\n", realPath)
|
||||
}
|
||||
fmt.Printf("Bin directory: %s\n", binDir)
|
||||
fmt.Printf("Install directory: %s\n", installDir)
|
||||
fmt.Printf("Looking for cline-core.js at: %s\n", clineCorePath)
|
||||
}
|
||||
|
||||
// Check if cline-core.js exists at the primary location
|
||||
var finalClineCorePath string
|
||||
var finalInstallDir string
|
||||
if _, err := os.Stat(clineCorePath); os.IsNotExist(err) {
|
||||
// Development mode: Try ../../dist-standalone/cline-core.js
|
||||
// This handles the case where we're running from cli/bin/cline
|
||||
devClineCorePath := path.Join(binDir, "..", "..", "dist-standalone", "cline-core.js")
|
||||
devInstallDir := path.Join(binDir, "..", "..", "dist-standalone")
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Primary location not found, trying development path: %s\n", devClineCorePath)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(devClineCorePath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("cline-core.js not found at '%s' or '%s'. Please ensure you're running from the correct location or reinstall with 'npm install -g cline'", clineCorePath, devClineCorePath)
|
||||
}
|
||||
|
||||
finalClineCorePath = devClineCorePath
|
||||
finalInstallDir = devInstallDir
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Using development mode: cline-core.js found at %s\n", finalClineCorePath)
|
||||
}
|
||||
} else {
|
||||
finalClineCorePath = clineCorePath
|
||||
finalInstallDir = installDir
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Using production mode: cline-core.js found at %s\n", finalClineCorePath)
|
||||
}
|
||||
}
|
||||
|
||||
// Create logs directory in ~/.cline/logs
|
||||
logsDir := path.Join(Config.ConfigPath, "logs")
|
||||
if err := os.MkdirAll(logsDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create logs directory: %w", err)
|
||||
}
|
||||
|
||||
// Create timestamped log file
|
||||
timestamp := time.Now().Format("2006-01-02-15-04-05")
|
||||
logFileName := fmt.Sprintf("cline-core-%s-localhost-%d.log", timestamp, corePort)
|
||||
logFilePath := path.Join(logsDir, logFileName)
|
||||
logFile, err := os.Create(logFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create log file: %w", err)
|
||||
}
|
||||
|
||||
// Start the cline-core process with --config flag using system node
|
||||
args := []string{finalClineCorePath,
|
||||
"--port", fmt.Sprintf("%d", corePort),
|
||||
"--host-bridge-port", fmt.Sprintf("%d", hostPort),
|
||||
"--config", Config.ConfigPath}
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Using system node\n")
|
||||
}
|
||||
|
||||
cmd := exec.Command("node", args...)
|
||||
|
||||
// Set working directory to installation root
|
||||
cmd.Dir = finalInstallDir
|
||||
|
||||
// Redirect stdout and stderr to log file
|
||||
cmd.Stdout = logFile
|
||||
cmd.Stderr = logFile
|
||||
|
||||
// Put the child process in a new process group so Ctrl+C doesn't kill it
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
}
|
||||
|
||||
// Set environment variables with NODE_PATH for both real and fake node_modules
|
||||
// The fake node_modules contains the vscode stub that can't be in the real node_modules
|
||||
env := os.Environ()
|
||||
realNodeModules := path.Join(finalInstallDir, "node_modules")
|
||||
fakeNodeModules := path.Join(finalInstallDir, "fake_node_modules")
|
||||
nodePath := fmt.Sprintf("%s%c%s", realNodeModules, os.PathListSeparator, fakeNodeModules)
|
||||
|
||||
env = append(env,
|
||||
fmt.Sprintf("NODE_PATH=%s", nodePath),
|
||||
// These control gRPC debug logging
|
||||
//"GRPC_TRACE=all",
|
||||
//"GRPC_VERBOSITY=DEBUG",
|
||||
"NODE_ENV=development",
|
||||
)
|
||||
cmd.Env = env
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("NODE_PATH set to: %s\n", nodePath)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
logFile.Close()
|
||||
return nil, fmt.Errorf("failed to start cline-core: %w", err)
|
||||
}
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Started cline-core (PID: %d)\n", cmd.Process.Pid)
|
||||
fmt.Printf("Logging cline-core output to: %s\n", logFilePath)
|
||||
}
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
//go:build !windows
|
||||
|
||||
package global
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/cline/grpc-go/cline"
|
||||
)
|
||||
|
||||
func startClineHost(hostPort int, workspaces []string) (*exec.Cmd, error) {
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Starting cline-host on port %d\n", hostPort)
|
||||
}
|
||||
|
||||
// Get the directory where the cline binary is located
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get executable path: %w", err)
|
||||
}
|
||||
binDir := filepath.Dir(execPath)
|
||||
clineHostPath := filepath.Join(binDir, "cline-host")
|
||||
|
||||
// Build command arguments
|
||||
args := []string{
|
||||
"--verbose",
|
||||
"--port", fmt.Sprintf("%d", hostPort),
|
||||
}
|
||||
|
||||
for _, ws := range workspaces {
|
||||
args = append(args, "--workspace", ws)
|
||||
}
|
||||
|
||||
// Start the cline-host process
|
||||
cmd := exec.Command(clineHostPath, args...)
|
||||
|
||||
// Create logs directory in ~/.cline/logs
|
||||
logsDir := filepath.Join(Config.ConfigPath, "logs")
|
||||
if err := os.MkdirAll(logsDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create logs directory: %w", err)
|
||||
}
|
||||
|
||||
// Create timestamped log file
|
||||
timestamp := time.Now().Format("2006-01-02-15-04-05")
|
||||
logFileName := fmt.Sprintf("cline-host-%s-localhost-%d.log", timestamp, hostPort)
|
||||
logFilePath := filepath.Join(logsDir, logFileName)
|
||||
logFile, err := os.Create(logFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create log file: %w", err)
|
||||
}
|
||||
|
||||
// Redirect stdout and stderr to log file
|
||||
cmd.Stdout = logFile
|
||||
cmd.Stderr = logFile
|
||||
|
||||
// Put the child process in a new process group so Ctrl+C doesn't kill it
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
logFile.Close()
|
||||
return nil, fmt.Errorf("failed to start cline-host: %w", err)
|
||||
}
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Started cline-host (PID: %d)\n", cmd.Process.Pid)
|
||||
fmt.Printf("Logging cline-host output to: %s\n", logFilePath)
|
||||
}
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
// KillInstanceByAddress kills a Cline instance by its address
|
||||
func KillInstanceByAddress(ctx context.Context, registry *ClientRegistry, address string) error {
|
||||
// Check if the instance exists in the registry
|
||||
_, err := registry.GetInstance(address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("instance %s not found in registry", address)
|
||||
}
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Killing instance: %s\n", address)
|
||||
}
|
||||
|
||||
// Get gRPC client and process info
|
||||
client, err := registry.GetClient(ctx, address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to instance %s: %w", address, err)
|
||||
}
|
||||
|
||||
processInfo, err := client.State.GetProcessInfo(ctx, &cline.EmptyRequest{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get process info for instance %s: %w", address, err)
|
||||
}
|
||||
|
||||
pid := int(processInfo.ProcessId)
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Terminating process PID %d...\n", pid)
|
||||
}
|
||||
|
||||
// Kill the process
|
||||
if err := syscall.Kill(pid, syscall.SIGTERM); err != nil {
|
||||
return fmt.Errorf("failed to kill process %d: %w", pid, err)
|
||||
}
|
||||
|
||||
// Wait for the instance to remove itself from registry
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Waiting for instance to clean up registry entry...\n")
|
||||
}
|
||||
for range 5 {
|
||||
time.Sleep(1 * time.Second)
|
||||
if !registry.HasInstanceAtAddress(address) {
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Instance %s successfully killed and removed from registry.\n", address)
|
||||
}
|
||||
|
||||
// Update default instance if needed
|
||||
instances, err := registry.ListInstancesCleaned(ctx)
|
||||
if err == nil && len(instances) > 0 {
|
||||
// ensureDefaultInstance logic will handle setting a new default
|
||||
defaultInstance := registry.GetDefaultInstance()
|
||||
if defaultInstance == address || defaultInstance == "" {
|
||||
if len(instances) > 0 {
|
||||
if err := registry.SetDefaultInstance(instances[0].Address); err == nil {
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Updated default instance to: %s\n", instances[0].Address)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("instance killed but failed to remove itself from registry within 5 seconds")
|
||||
}
|
||||
|
||||
func startClineCore(corePort, hostPort int) (*exec.Cmd, error) {
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Starting cline-core on port %d (with hostbridge on %d)\n", corePort, hostPort)
|
||||
}
|
||||
|
||||
// Get the executable path and resolve symlinks (for npm global installs)
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get executable path: %w", err)
|
||||
}
|
||||
|
||||
// Resolve symlinks to get the real path
|
||||
// For npm global installs, execPath might be a symlink like:
|
||||
// /opt/homebrew/bin/cline -> /opt/homebrew/lib/node_modules/cline/bin/cline
|
||||
realPath, err := filepath.EvalSymlinks(execPath)
|
||||
if err != nil {
|
||||
// If we can't resolve symlinks, fall back to the original path
|
||||
realPath = execPath
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Warning: Could not resolve symlinks for %s: %v\n", execPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
binDir := filepath.Dir(realPath)
|
||||
installDir := filepath.Dir(binDir)
|
||||
clineCorePath := filepath.Join(installDir, "cline-core.js")
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Executable path: %s\n", execPath)
|
||||
if realPath != execPath {
|
||||
fmt.Printf("Real path (after resolving symlinks): %s\n", realPath)
|
||||
}
|
||||
fmt.Printf("Bin directory: %s\n", binDir)
|
||||
fmt.Printf("Install directory: %s\n", installDir)
|
||||
fmt.Printf("Looking for cline-core.js at: %s\n", clineCorePath)
|
||||
}
|
||||
|
||||
// Check if cline-core.js exists at the primary location
|
||||
var finalClineCorePath string
|
||||
var finalInstallDir string
|
||||
if _, err := os.Stat(clineCorePath); os.IsNotExist(err) {
|
||||
// Development mode: Try ../../dist-standalone/cline-core.js
|
||||
// This handles the case where we're running from cli/bin/cline
|
||||
devClineCorePath := filepath.Join(binDir, "..", "dist-standalone", "cline-core.js")
|
||||
devInstallDir := filepath.Join(binDir, "..", "dist-standalone")
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Primary location not found, trying development path: %s\n", devClineCorePath)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(devClineCorePath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("cline-core.js not found at '%s' or '%s'. Please ensure you're running from the correct location or reinstall with 'npm install -g cline'", clineCorePath, devClineCorePath)
|
||||
}
|
||||
|
||||
finalClineCorePath = devClineCorePath
|
||||
finalInstallDir = devInstallDir
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Using development mode: cline-core.js found at %s\n", finalClineCorePath)
|
||||
}
|
||||
} else {
|
||||
finalClineCorePath = clineCorePath
|
||||
finalInstallDir = installDir
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Using production mode: cline-core.js found at %s\n", finalClineCorePath)
|
||||
}
|
||||
}
|
||||
|
||||
// Create logs directory in ~/.cline/logs
|
||||
logsDir := filepath.Join(Config.ConfigPath, "logs")
|
||||
if err := os.MkdirAll(logsDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create logs directory: %w", err)
|
||||
}
|
||||
|
||||
// Create timestamped log file
|
||||
timestamp := time.Now().Format("2006-01-02-15-04-05")
|
||||
logFileName := fmt.Sprintf("cline-core-%s-localhost-%d.log", timestamp, corePort)
|
||||
logFilePath := filepath.Join(logsDir, logFileName)
|
||||
logFile, err := os.Create(logFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create log file: %w", err)
|
||||
}
|
||||
|
||||
// Start the cline-core process with --config flag using system node
|
||||
args := []string{finalClineCorePath,
|
||||
"--port", fmt.Sprintf("%d", corePort),
|
||||
"--host-bridge-port", fmt.Sprintf("%d", hostPort),
|
||||
"--config", Config.ConfigPath}
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Using system node\n")
|
||||
}
|
||||
|
||||
cmd := exec.Command("node", args...)
|
||||
|
||||
// Set working directory to installation root
|
||||
cmd.Dir = finalInstallDir
|
||||
|
||||
// Redirect stdout and stderr to log file
|
||||
cmd.Stdout = logFile
|
||||
cmd.Stderr = logFile
|
||||
|
||||
// Put the child process in a new process group so Ctrl+C doesn't kill it
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
}
|
||||
|
||||
// Set environment variables with NODE_PATH for both real and fake node_modules
|
||||
// The fake node_modules contains the vscode stub that can't be in the real node_modules
|
||||
env := os.Environ()
|
||||
realNodeModules := filepath.Join(finalInstallDir, "node_modules")
|
||||
fakeNodeModules := filepath.Join(finalInstallDir, "fake_node_modules")
|
||||
nodePath := fmt.Sprintf("%s%c%s", realNodeModules, os.PathListSeparator, fakeNodeModules)
|
||||
|
||||
env = append(env,
|
||||
fmt.Sprintf("NODE_PATH=%s", nodePath),
|
||||
// These control gRPC debug logging
|
||||
//"GRPC_TRACE=all",
|
||||
//"GRPC_VERBOSITY=DEBUG",
|
||||
"NODE_ENV=development",
|
||||
)
|
||||
cmd.Env = env
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("NODE_PATH set to: %s\n", nodePath)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
logFile.Close()
|
||||
return nil, fmt.Errorf("failed to start cline-core: %w", err)
|
||||
}
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Started cline-core (PID: %d)\n", cmd.Process.Pid)
|
||||
fmt.Printf("Logging cline-core output to: %s\n", logFilePath)
|
||||
}
|
||||
return cmd, nil
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
//go:build windows
|
||||
|
||||
package global
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/cline/grpc-go/cline"
|
||||
)
|
||||
|
||||
func startClineHost(hostPort int, workspaces []string) (*exec.Cmd, error) {
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Starting cline-host on port %d\n", hostPort)
|
||||
}
|
||||
|
||||
// Get the directory where the cline binary is located
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get executable path: %w", err)
|
||||
}
|
||||
binDir := filepath.Dir(execPath)
|
||||
clineHostPath := filepath.Join(binDir, "cline-host.exe")
|
||||
|
||||
// Build command arguments
|
||||
args := []string{
|
||||
"--verbose",
|
||||
"--port", fmt.Sprintf("%d", hostPort),
|
||||
}
|
||||
|
||||
for _, ws := range workspaces {
|
||||
args = append(args, "--workspace", ws)
|
||||
}
|
||||
|
||||
// Start the cline-host process
|
||||
cmd := exec.Command(clineHostPath, args...)
|
||||
|
||||
// Create logs directory in ~/.cline/logs
|
||||
logsDir := filepath.Join(Config.ConfigPath, "logs")
|
||||
if err := os.MkdirAll(logsDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create logs directory: %w", err)
|
||||
}
|
||||
|
||||
// Create timestamped log file
|
||||
timestamp := time.Now().Format("2006-01-02-15-04-05")
|
||||
logFileName := fmt.Sprintf("cline-host-%s-localhost-%d.log", timestamp, hostPort)
|
||||
logFilePath := filepath.Join(logsDir, logFileName)
|
||||
logFile, err := os.Create(logFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create log file: %w", err)
|
||||
}
|
||||
|
||||
// Redirect stdout and stderr to log file
|
||||
cmd.Stdout = logFile
|
||||
cmd.Stderr = logFile
|
||||
|
||||
// Put the child process in a new process group so Ctrl+C doesn't kill it
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
logFile.Close()
|
||||
return nil, fmt.Errorf("failed to start cline-host: %w", err)
|
||||
}
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Started cline-host (PID: %d)\n", cmd.Process.Pid)
|
||||
fmt.Printf("Logging cline-host output to: %s\n", logFilePath)
|
||||
}
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
// KillInstanceByAddress kills a Cline instance by its address
|
||||
func KillInstanceByAddress(ctx context.Context, registry *ClientRegistry, address string) error {
|
||||
// Check if the instance exists in the registry
|
||||
_, err := registry.GetInstance(address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("instance %s not found in registry", address)
|
||||
}
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Killing instance: %s\n", address)
|
||||
}
|
||||
|
||||
// Get gRPC client and process info
|
||||
client, err := registry.GetClient(ctx, address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to instance %s: %w", address, err)
|
||||
}
|
||||
|
||||
processInfo, err := client.State.GetProcessInfo(ctx, &cline.EmptyRequest{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get process info for instance %s: %w", address, err)
|
||||
}
|
||||
|
||||
pid := int(processInfo.ProcessId)
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Terminating process PID %d...\n", pid)
|
||||
}
|
||||
|
||||
// Find and kill the process using os.Process
|
||||
// On Windows, os.Process.Kill() properly calls TerminateProcess with the correct handle
|
||||
process, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find process %d: %w", pid, err)
|
||||
}
|
||||
|
||||
if err := process.Kill(); err != nil {
|
||||
return fmt.Errorf("failed to kill process %d: %w", pid, err)
|
||||
}
|
||||
|
||||
// Wait for the instance to remove itself from registry
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Waiting for instance to clean up registry entry...\n")
|
||||
}
|
||||
for range 5 {
|
||||
time.Sleep(1 * time.Second)
|
||||
if !registry.HasInstanceAtAddress(address) {
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Instance %s successfully killed and removed from registry.\n", address)
|
||||
}
|
||||
|
||||
// Update default instance if needed
|
||||
instances, err := registry.ListInstancesCleaned(ctx)
|
||||
if err == nil && len(instances) > 0 {
|
||||
// ensureDefaultInstance logic will handle setting a new default
|
||||
defaultInstance := registry.GetDefaultInstance()
|
||||
if defaultInstance == address || defaultInstance == "" {
|
||||
if len(instances) > 0 {
|
||||
if err := registry.SetDefaultInstance(instances[0].Address); err == nil {
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Updated default instance to: %s\n", instances[0].Address)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("instance killed but failed to remove itself from registry within 5 seconds")
|
||||
}
|
||||
|
||||
func startClineCore(corePort, hostPort int) (*exec.Cmd, error) {
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Starting cline-core on port %d (with hostbridge on %d)\n", corePort, hostPort)
|
||||
}
|
||||
|
||||
// Get the executable path and resolve symlinks (for npm global installs)
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get executable path: %w", err)
|
||||
}
|
||||
|
||||
// Resolve symlinks to get the real path
|
||||
// For npm global installs, execPath might be a symlink like:
|
||||
// /opt/homebrew/bin/cline -> /opt/homebrew/lib/node_modules/cline/bin/cline
|
||||
realPath, err := filepath.EvalSymlinks(execPath)
|
||||
if err != nil {
|
||||
// If we can't resolve symlinks, fall back to the original path
|
||||
realPath = execPath
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Warning: Could not resolve symlinks for %s: %v\n", execPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
binDir := filepath.Dir(realPath)
|
||||
installDir := filepath.Dir(binDir)
|
||||
clineCorePath := filepath.Join(installDir, "cline-core.js")
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Executable path: %s\n", execPath)
|
||||
if realPath != execPath {
|
||||
fmt.Printf("Real path (after resolving symlinks): %s\n", realPath)
|
||||
}
|
||||
fmt.Printf("Bin directory: %s\n", binDir)
|
||||
fmt.Printf("Install directory: %s\n", installDir)
|
||||
fmt.Printf("Looking for cline-core.js at: %s\n", clineCorePath)
|
||||
}
|
||||
|
||||
// Check if cline-core.js exists at the primary location
|
||||
var finalClineCorePath string
|
||||
var finalInstallDir string
|
||||
if _, err := os.Stat(clineCorePath); os.IsNotExist(err) {
|
||||
// Development mode: Try ../../dist-standalone/cline-core.js
|
||||
// This handles the case where we're running from cli/bin/cline
|
||||
devClineCorePath := filepath.Join(binDir, "..", "dist-standalone", "cline-core.js")
|
||||
devInstallDir := filepath.Join(binDir, "..", "dist-standalone")
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Primary location not found, trying development path: %s\n", devClineCorePath)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(devClineCorePath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("cline-core.js not found at '%s' or '%s'. Please ensure you're running from the correct location or reinstall with 'npm install -g cline'", clineCorePath, devClineCorePath)
|
||||
}
|
||||
|
||||
finalClineCorePath = devClineCorePath
|
||||
finalInstallDir = devInstallDir
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Using development mode: cline-core.js found at %s\n", finalClineCorePath)
|
||||
}
|
||||
} else {
|
||||
finalClineCorePath = clineCorePath
|
||||
finalInstallDir = installDir
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Using production mode: cline-core.js found at %s\n", finalClineCorePath)
|
||||
}
|
||||
}
|
||||
|
||||
// Create logs directory in ~/.cline/logs
|
||||
logsDir := filepath.Join(Config.ConfigPath, "logs")
|
||||
if err := os.MkdirAll(logsDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create logs directory: %w", err)
|
||||
}
|
||||
|
||||
// Create timestamped log file
|
||||
timestamp := time.Now().Format("2006-01-02-15-04-05")
|
||||
logFileName := fmt.Sprintf("cline-core-%s-localhost-%d.log", timestamp, corePort)
|
||||
logFilePath := filepath.Join(logsDir, logFileName)
|
||||
logFile, err := os.Create(logFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create log file: %w", err)
|
||||
}
|
||||
|
||||
// Start the cline-core process with --config flag using system node
|
||||
args := []string{finalClineCorePath,
|
||||
"--port", fmt.Sprintf("%d", corePort),
|
||||
"--host-bridge-port", fmt.Sprintf("%d", hostPort),
|
||||
"--config", Config.ConfigPath}
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Using system node\n")
|
||||
}
|
||||
|
||||
cmd := exec.Command("node", args...)
|
||||
|
||||
// Set working directory to installation root
|
||||
cmd.Dir = finalInstallDir
|
||||
|
||||
// Redirect stdout and stderr to log file
|
||||
cmd.Stdout = logFile
|
||||
cmd.Stderr = logFile
|
||||
|
||||
// Put the child process in a new process group so Ctrl+C doesn't kill it
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
|
||||
}
|
||||
|
||||
// Set environment variables with NODE_PATH for both real and fake node_modules
|
||||
// The fake node_modules contains the vscode stub that can't be in the real node_modules
|
||||
env := os.Environ()
|
||||
realNodeModules := filepath.Join(finalInstallDir, "node_modules")
|
||||
fakeNodeModules := filepath.Join(finalInstallDir, "fake_node_modules")
|
||||
nodePath := fmt.Sprintf("%s%c%s", realNodeModules, os.PathListSeparator, fakeNodeModules)
|
||||
|
||||
env = append(env,
|
||||
fmt.Sprintf("NODE_PATH=%s", nodePath),
|
||||
// These control gRPC debug logging
|
||||
//"GRPC_TRACE=all",
|
||||
//"GRPC_VERBOSITY=DEBUG",
|
||||
"NODE_ENV=development",
|
||||
)
|
||||
cmd.Env = env
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("NODE_PATH set to: %s\n", nodePath)
|
||||
fmt.Printf("Attempting to run command: %s\n", cmd)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
logFile.Close()
|
||||
return nil, fmt.Errorf("failed to start cline-core: %w", err)
|
||||
}
|
||||
|
||||
if Config.Verbose {
|
||||
fmt.Printf("Started cline-core (PID: %d)\n", cmd.Process.Pid)
|
||||
fmt.Printf("Logging cline-core output to: %s\n", logFilePath)
|
||||
}
|
||||
return cmd, nil
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
@@ -247,28 +246,6 @@ type killResult struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func killInstanceProcess(ctx context.Context, registry *global.ClientRegistry, address string) killResult {
|
||||
// Get gRPC client and process info
|
||||
client, err := registry.GetClient(ctx, address)
|
||||
if err != nil {
|
||||
return killResult{address: address, alreadyDead: true, err: nil}
|
||||
}
|
||||
|
||||
processInfo, err := client.State.GetProcessInfo(ctx, &cline.EmptyRequest{})
|
||||
if err != nil {
|
||||
return killResult{address: address, alreadyDead: true, err: nil}
|
||||
}
|
||||
|
||||
pid := int(processInfo.ProcessId)
|
||||
|
||||
// Kill the process
|
||||
if err := syscall.Kill(pid, syscall.SIGTERM); err != nil {
|
||||
return killResult{address: address, pid: pid, err: err}
|
||||
}
|
||||
|
||||
return killResult{address: address, pid: pid, err: nil}
|
||||
}
|
||||
|
||||
func newInstanceListCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
//go:build !windows
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"syscall"
|
||||
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
"github.com/cline/grpc-go/cline"
|
||||
)
|
||||
|
||||
func killInstanceProcess(ctx context.Context, registry *global.ClientRegistry, address string) killResult {
|
||||
// Get gRPC client and process info
|
||||
client, err := registry.GetClient(ctx, address)
|
||||
if err != nil {
|
||||
return killResult{address: address, alreadyDead: true, err: nil}
|
||||
}
|
||||
|
||||
processInfo, err := client.State.GetProcessInfo(ctx, &cline.EmptyRequest{})
|
||||
if err != nil {
|
||||
return killResult{address: address, alreadyDead: true, err: nil}
|
||||
}
|
||||
|
||||
pid := int(processInfo.ProcessId)
|
||||
|
||||
// Kill the process
|
||||
if err := syscall.Kill(pid, syscall.SIGTERM); err != nil {
|
||||
return killResult{address: address, pid: pid, err: err}
|
||||
}
|
||||
|
||||
return killResult{address: address, pid: pid, err: nil}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
//go:build windows
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/cline/cli/pkg/cli/global"
|
||||
"github.com/cline/grpc-go/cline"
|
||||
)
|
||||
|
||||
func killInstanceProcess(ctx context.Context, registry *global.ClientRegistry, address string) killResult {
|
||||
// Get gRPC client and process info
|
||||
client, err := registry.GetClient(ctx, address)
|
||||
if err != nil {
|
||||
return killResult{address: address, alreadyDead: true, err: nil}
|
||||
}
|
||||
|
||||
processInfo, err := client.State.GetProcessInfo(ctx, &cline.EmptyRequest{})
|
||||
if err != nil {
|
||||
return killResult{address: address, alreadyDead: true, err: nil}
|
||||
}
|
||||
|
||||
pid := int(processInfo.ProcessId)
|
||||
|
||||
// Find the process by PID
|
||||
process, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
// Process may already be dead
|
||||
return killResult{address: address, pid: pid, alreadyDead: true, err: nil}
|
||||
}
|
||||
|
||||
// Kill the process - on Windows, os.Process.Kill() calls TerminateProcess internally
|
||||
if err := process.Kill(); err != nil {
|
||||
return killResult{address: address, pid: pid, err: fmt.Errorf("failed to terminate process: %w", err)}
|
||||
}
|
||||
|
||||
return killResult{address: address, pid: pid, err: nil}
|
||||
}
|
||||
Generated
+6100
-3551
File diff suppressed because it is too large
Load Diff
+19
-3
@@ -339,12 +339,28 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run build:all-surfaces",
|
||||
"build:vscode": "npx tsx scripts/build.ts --surface=vscode",
|
||||
"build:vscode:prod": "npx tsx scripts/build.ts --surface=vscode --prod",
|
||||
"build:jetbrains": "npx tsx scripts/build.ts --surface=jetbrains",
|
||||
"build:jetbrains:prod": "npx tsx scripts/build.ts --surface=jetbrains --prod",
|
||||
"build:cli:unix": "npx tsx scripts/build.ts --surface=cli --platform=unix",
|
||||
"build:cli:windows": "npx tsx scripts/build.ts --surface=cli --platform=windows",
|
||||
"build:cli:all-platforms": "npx tsx scripts/build.ts --surface=cli --platform=all",
|
||||
"build:cli:unix:prod": "npx tsx scripts/build.ts --surface=cli --platform=unix --prod",
|
||||
"build:cli:windows:prod": "npx tsx scripts/build.ts --surface=cli --platform=windows --prod",
|
||||
"build:cli:all-platforms:prod": "npx tsx scripts/build.ts --surface=cli --platform=all --prod",
|
||||
"build:all-surfaces": "npx tsx scripts/build.ts --surface=all",
|
||||
"build:all-surfaces:prod": "npx tsx scripts/build.ts --surface=all --prod",
|
||||
"build:all-surfaces:all-platforms": "npx tsx scripts/build.ts --surface=all --platform=all",
|
||||
"build:all-surfaces:all-platforms:prod": "npx tsx scripts/build.ts --surface=all --platform=all --prod",
|
||||
"build:all-surfaces:all-platforms:all-stages": "npx tsx scripts/build.ts --surface=all --platform=all --all-stages",
|
||||
"build:npm": "npx tsx scripts/build.ts --surface=npm",
|
||||
"build:npm:prod": "npx tsx scripts/build.ts --surface=npm --prod",
|
||||
"vscode:prepublish": "npm run package",
|
||||
"compile": "npm run check-types && npm run lint && node esbuild.mjs",
|
||||
"compile-standalone": "npm run check-types && npm run lint && node esbuild.mjs --standalone",
|
||||
"compile-standalone-npm": "npm run protos && npm run protos-go && npm run check-types && npm run lint && node esbuild.mjs --standalone",
|
||||
"compile-cli": "scripts/build-cli.sh",
|
||||
"compile-cli-all-platforms": "scripts/build-cli-all-platforms.sh",
|
||||
"compile-cli-man-page": "pandoc cli/man/cline.1.md -s -t man -o cli/man/cline.1",
|
||||
"test:install": "bash scripts/test-install.sh",
|
||||
"dev:cli:watch": "node scripts/dev-cli-watch.mjs",
|
||||
@@ -499,7 +515,7 @@
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
"archiver": "^7.0.1",
|
||||
"axios": "^1.12.0",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"better-sqlite3": "^12.5.0",
|
||||
"cheerio": "^1.0.0",
|
||||
"chokidar": "^4.0.1",
|
||||
"chrome-launcher": "^1.1.2",
|
||||
|
||||
@@ -25,6 +25,7 @@ cd cli
|
||||
|
||||
# Define target platforms for cross-compilation
|
||||
PLATFORMS=(
|
||||
"windows/amd64"
|
||||
"darwin/arm64"
|
||||
"darwin/amd64"
|
||||
"linux/amd64"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { execSync } from "child_process"
|
||||
import * as fs from "fs/promises"
|
||||
import { globby } from "globby"
|
||||
import { createRequire } from "module"
|
||||
import { platform } from "os"
|
||||
import * as path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import { createServiceNameMap, parseProtoForServices } from "./proto-shared-utils.mjs"
|
||||
@@ -26,7 +27,7 @@ function checkGoInstallation() {
|
||||
try {
|
||||
execSync("go version", { stdio: "pipe" })
|
||||
return true
|
||||
} catch (error) {
|
||||
} catch (_) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -36,12 +37,12 @@ function checkGoTool(toolName) {
|
||||
try {
|
||||
execSync(`which ${toolName}`, { stdio: "pipe" })
|
||||
return true
|
||||
} catch (error) {
|
||||
} catch (_) {
|
||||
// On Windows, 'which' might not be available, try 'where'
|
||||
try {
|
||||
execSync(`where ${toolName}`, { stdio: "pipe" })
|
||||
return true
|
||||
} catch (windowsError) {
|
||||
} catch (_) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -51,12 +52,26 @@ function checkGoTool(toolName) {
|
||||
function installGoTools() {
|
||||
console.log(chalk.yellow("Installing Go protobuf tools..."))
|
||||
|
||||
const OSPREFIX = (() => {
|
||||
if (platform() === "win32") {
|
||||
if (process.env.ComSpec) {
|
||||
if (process.env.PSModulePath && !process.env.SHELL) {
|
||||
return `$Env:GO111MODULE="on";`
|
||||
}
|
||||
return `set GO111MODULE=on &&`
|
||||
}
|
||||
return `GO111MODULE=on`
|
||||
} else {
|
||||
return `GO111MODULE=on`
|
||||
}
|
||||
})()
|
||||
|
||||
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}`, {
|
||||
execSync(`${OSPREFIX} go install ${tool}`, {
|
||||
stdio: "inherit",
|
||||
env: { ...process.env, GO111MODULE: "on" },
|
||||
})
|
||||
@@ -82,7 +97,9 @@ function checkToolsInPath() {
|
||||
|
||||
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}`)))
|
||||
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:"))
|
||||
|
||||
@@ -91,7 +108,7 @@ function checkToolsInPath() {
|
||||
try {
|
||||
goPath = execSync("go env GOPATH", { encoding: "utf8" }).trim()
|
||||
goBin = execSync("go env GOBIN", { encoding: "utf8" }).trim()
|
||||
} catch (error) {
|
||||
} catch (_) {
|
||||
console.log(chalk.red("Could not determine Go paths. Please check your Go installation."))
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -573,7 +590,7 @@ ${methods}
|
||||
}
|
||||
|
||||
// Main execution block - run if this script is executed directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
if (fileURLToPath(import.meta.url) === process.argv[1]) {
|
||||
async function main() {
|
||||
try {
|
||||
console.log(chalk.cyan("Starting Go protobuf code generation..."))
|
||||
|
||||
@@ -12,7 +12,7 @@ import { main as generateHostBridgeClient } from "./generate-host-bridge-client.
|
||||
import { main as generateProtoBusSetup } from "./generate-protobus-setup.mjs"
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const PROTOC = path.join(require.resolve("grpc-tools"), "../bin/protoc")
|
||||
const PROTOC = `"${path.join(require.resolve("grpc-tools"), "../bin/protoc")}"`
|
||||
|
||||
const PROTO_DIR = path.resolve("proto")
|
||||
const TS_OUT_DIR = path.resolve("src/shared/proto")
|
||||
|
||||
@@ -0,0 +1,724 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
/**
|
||||
* Unified Build Orchestrator for Cline
|
||||
*
|
||||
* Cross-platform build script that works on Windows, macOS, and Linux.
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx scripts/build.ts --surface=<vscode|jetbrains|cli|all> [options]
|
||||
*
|
||||
* Options:
|
||||
* --surface=<surface> Build target: vscode, jetbrains, cli, or all
|
||||
* --platform=<platform> CLI platform: unix, windows, or all (default: unix)
|
||||
* --prod Production build (minification, strip debug symbols)
|
||||
* --all-stages Build both dev and prod
|
||||
*
|
||||
* Examples:
|
||||
* npx tsx scripts/build.ts --surface=vscode
|
||||
* npx tsx scripts/build.ts --surface=cli --platform=windows --prod
|
||||
* npx tsx scripts/build.ts --surface=all --platform=all --all-stages
|
||||
*/
|
||||
|
||||
import { execSync } from "node:child_process"
|
||||
import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
const ROOT_DIR = path.resolve(__dirname, "..")
|
||||
|
||||
// Types
|
||||
type Surface = "vscode" | "jetbrains" | "cli" | "npm" | "all"
|
||||
type PlatformGroup = "unix" | "windows" | "all"
|
||||
|
||||
interface CliTarget {
|
||||
GOOS: string
|
||||
GOARCH: string
|
||||
}
|
||||
|
||||
interface ParsedArgs {
|
||||
surface: Surface
|
||||
platform: PlatformGroup
|
||||
prod: boolean
|
||||
allStages: boolean
|
||||
}
|
||||
|
||||
interface RunOptions {
|
||||
cwd?: string
|
||||
env?: NodeJS.ProcessEnv
|
||||
silent?: boolean
|
||||
}
|
||||
|
||||
interface BuildState {
|
||||
protos: boolean
|
||||
protosGo: boolean
|
||||
webview: boolean
|
||||
}
|
||||
|
||||
// CLI build targets by platform group
|
||||
const CLI_PLATFORMS: Record<"unix" | "windows", CliTarget[]> = {
|
||||
unix: [
|
||||
{ GOOS: "darwin", GOARCH: "amd64" },
|
||||
{ GOOS: "darwin", GOARCH: "arm64" },
|
||||
{ GOOS: "linux", GOARCH: "amd64" },
|
||||
{ GOOS: "linux", GOARCH: "arm64" },
|
||||
],
|
||||
windows: [
|
||||
{ GOOS: "windows", GOARCH: "amd64" },
|
||||
{ GOOS: "windows", GOARCH: "arm64" },
|
||||
],
|
||||
}
|
||||
|
||||
// Colors for terminal output
|
||||
const colors = {
|
||||
reset: "\x1b[0m",
|
||||
bright: "\x1b[1m",
|
||||
red: "\x1b[31m",
|
||||
green: "\x1b[32m",
|
||||
yellow: "\x1b[33m",
|
||||
blue: "\x1b[34m",
|
||||
cyan: "\x1b[36m",
|
||||
} as const
|
||||
|
||||
function log(message: string, color: string = colors.reset): void {
|
||||
console.log(`${color}${message}${colors.reset}`)
|
||||
}
|
||||
|
||||
function logStep(step: string, message: string): void {
|
||||
log(`\n${colors.bright}[${step}]${colors.reset} ${message}`, colors.cyan)
|
||||
}
|
||||
|
||||
function logSuccess(message: string): void {
|
||||
log(` ✓ ${message}`, colors.green)
|
||||
}
|
||||
|
||||
function logError(message: string): void {
|
||||
log(` ✗ ${message}`, colors.red)
|
||||
}
|
||||
|
||||
function logWarning(message: string): void {
|
||||
log(` ! ${message}`, colors.yellow)
|
||||
}
|
||||
|
||||
function logSeparator(): void {
|
||||
log("=".repeat(60), colors.bright)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse command line arguments
|
||||
*/
|
||||
function parseArgs(): ParsedArgs {
|
||||
const args = process.argv.slice(2)
|
||||
const result: ParsedArgs = {
|
||||
surface: "all",
|
||||
platform: "unix",
|
||||
prod: false,
|
||||
allStages: false,
|
||||
}
|
||||
|
||||
for (const arg of args) {
|
||||
if (arg.startsWith("--surface=")) {
|
||||
result.surface = arg.split("=")[1] as Surface
|
||||
} else if (arg.startsWith("--platform=")) {
|
||||
result.platform = arg.split("=")[1] as PlatformGroup
|
||||
} else if (arg === "--prod") {
|
||||
result.prod = true
|
||||
} else if (arg === "--all-stages") {
|
||||
result.allStages = true
|
||||
} else if (arg === "--help" || arg === "-h") {
|
||||
console.log(`
|
||||
Unified Build Orchestrator for Cline
|
||||
|
||||
Usage:
|
||||
npx tsx scripts/build.ts --surface=<surface> [options]
|
||||
|
||||
Surfaces:
|
||||
vscode Build VS Code extension
|
||||
jetbrains Build JetBrains standalone package
|
||||
cli Build Go CLI binaries
|
||||
npm Build npm package (CLI + standalone for npm distribution)
|
||||
all Build all surfaces (vscode + jetbrains + cli, excludes npm)
|
||||
|
||||
Options:
|
||||
--platform=<platform> CLI platform target: unix, windows, or all (default: unix)
|
||||
--prod Production build (minification, strip debug symbols)
|
||||
--all-stages Build both dev and prod stages
|
||||
|
||||
Environment Variables (required for npm --prod):
|
||||
TELEMETRY_SERVICE_API_KEY PostHog telemetry API key
|
||||
ERROR_SERVICE_API_KEY Error tracking API key
|
||||
|
||||
Examples:
|
||||
npm run build:vscode # VS Code dev build
|
||||
npm run build:cli:windows:prod # CLI Windows production build
|
||||
npm run build:npm # npm package dev build
|
||||
npm run build:npm:prod # npm package prod build (requires env vars)
|
||||
npm run build:all-surfaces:all-platforms:all-stages # Everything
|
||||
`)
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate surface
|
||||
if (!["vscode", "jetbrains", "cli", "npm", "all"].includes(result.surface)) {
|
||||
logError(`Invalid surface: ${result.surface}. Must be one of: vscode, jetbrains, cli, npm, all`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Validate platform
|
||||
if (!["unix", "windows", "all"].includes(result.platform)) {
|
||||
logError(`Invalid platform: ${result.platform}. Must be one of: unix, windows, all`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a command synchronously with cross-platform support
|
||||
*/
|
||||
function run(cmd: string, opts: RunOptions = {}): void {
|
||||
const { cwd = ROOT_DIR, env = process.env, silent = false } = opts
|
||||
|
||||
if (!silent) {
|
||||
log(` $ ${cmd}`, colors.yellow)
|
||||
}
|
||||
|
||||
try {
|
||||
execSync(cmd, {
|
||||
cwd,
|
||||
env,
|
||||
stdio: silent ? "pipe" : "inherit",
|
||||
})
|
||||
} catch {
|
||||
throw new Error(`Command failed: ${cmd}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a command and return the output
|
||||
*/
|
||||
function runCapture(cmd: string, opts: { cwd?: string } = {}): string | null {
|
||||
const { cwd = ROOT_DIR } = opts
|
||||
|
||||
try {
|
||||
return execSync(cmd, {
|
||||
cwd,
|
||||
encoding: "utf8",
|
||||
}).trim()
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get git commit hash
|
||||
*/
|
||||
function getGitCommit(): string {
|
||||
return runCapture("git rev-parse --short HEAD") || "unknown"
|
||||
}
|
||||
|
||||
/**
|
||||
* Read package.json version
|
||||
*/
|
||||
function getPackageVersion(packagePath: string): string {
|
||||
const fullPath = path.join(ROOT_DIR, packagePath)
|
||||
const pkg = JSON.parse(fs.readFileSync(fullPath, "utf8")) as { version: string }
|
||||
return pkg.version
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Go ldflags string
|
||||
*/
|
||||
function buildLdflags(prod: boolean): string {
|
||||
const version = getPackageVersion("package.json")
|
||||
const cliVersion = getPackageVersion("cli/package.json")
|
||||
const commit = getGitCommit()
|
||||
const date = new Date().toISOString()
|
||||
const builtBy = process.env.USER || process.env.USERNAME || "unknown"
|
||||
|
||||
let ldflags =
|
||||
`-X 'github.com/cline/cli/pkg/cli/global.Version=${version}' ` +
|
||||
`-X 'github.com/cline/cli/pkg/cli/global.CliVersion=${cliVersion}' ` +
|
||||
`-X 'github.com/cline/cli/pkg/cli/global.Commit=${commit}' ` +
|
||||
`-X 'github.com/cline/cli/pkg/cli/global.Date=${date}' ` +
|
||||
`-X 'github.com/cline/cli/pkg/cli/global.BuiltBy=${builtBy}'`
|
||||
|
||||
if (prod) {
|
||||
ldflags += " -s -w" // Strip debug symbols and DWARF
|
||||
}
|
||||
|
||||
return ldflags
|
||||
}
|
||||
|
||||
// Track what has been built to avoid duplicate work
|
||||
const buildState: BuildState = {
|
||||
protos: false,
|
||||
protosGo: false,
|
||||
webview: false,
|
||||
}
|
||||
|
||||
/**
|
||||
* Build protobuf definitions
|
||||
*/
|
||||
async function buildProtos(): Promise<void> {
|
||||
if (buildState.protos) {
|
||||
logSuccess("Protos already built, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
logStep("PROTOS", "Building protobuf definitions")
|
||||
run("npm run protos")
|
||||
buildState.protos = true
|
||||
logSuccess("Protos built")
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Go protobuf definitions
|
||||
*/
|
||||
async function buildProtosGo(): Promise<void> {
|
||||
if (buildState.protosGo) {
|
||||
logSuccess("Go protos already built, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
logStep("PROTOS-GO", "Building Go protobuf definitions")
|
||||
run("npm run protos-go")
|
||||
buildState.protosGo = true
|
||||
logSuccess("Go protos built")
|
||||
}
|
||||
|
||||
/**
|
||||
* Build webview UI
|
||||
*/
|
||||
async function buildWebview(): Promise<void> {
|
||||
if (buildState.webview) {
|
||||
logSuccess("Webview already built, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
logStep("WEBVIEW", "Building webview UI")
|
||||
run("npm run build:webview")
|
||||
buildState.webview = true
|
||||
logSuccess("Webview built")
|
||||
}
|
||||
|
||||
/**
|
||||
* Build VS Code extension
|
||||
*/
|
||||
async function buildVscode(prod: boolean): Promise<void> {
|
||||
const stage = prod ? "prod" : "dev"
|
||||
logStep("VSCODE", `Building VS Code extension (${stage})`)
|
||||
|
||||
await buildProtos()
|
||||
await buildWebview()
|
||||
|
||||
// Run esbuild
|
||||
const productionFlag = prod ? " --production" : ""
|
||||
run(`node esbuild.mjs${productionFlag}`)
|
||||
|
||||
logSuccess(`VS Code extension built (${stage})`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build JetBrains standalone package
|
||||
*/
|
||||
async function buildJetbrains(prod: boolean): Promise<void> {
|
||||
const stage = prod ? "prod" : "dev"
|
||||
logStep("JETBRAINS", `Building JetBrains standalone package (${stage})`)
|
||||
|
||||
await buildProtos()
|
||||
await buildProtosGo()
|
||||
await buildWebview()
|
||||
|
||||
// Prepare dist-standalone directory
|
||||
const distDir = path.join(ROOT_DIR, "dist-standalone")
|
||||
const extensionDir = path.join(distDir, "extension")
|
||||
|
||||
fs.mkdirSync(extensionDir, { recursive: true })
|
||||
fs.copyFileSync(path.join(ROOT_DIR, "package.json"), path.join(extensionDir, "package.json"))
|
||||
|
||||
// Run esbuild with standalone flag
|
||||
const productionFlag = prod ? " --production" : ""
|
||||
run(`node esbuild.mjs --standalone${productionFlag}`)
|
||||
|
||||
// Run package-standalone.mjs (always builds for all platforms)
|
||||
run("node scripts/package-standalone.mjs")
|
||||
|
||||
logSuccess(`JetBrains standalone package built (${stage})`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build CLI binaries for specified platforms
|
||||
*/
|
||||
async function buildCli(platformGroups: PlatformGroup[], prod: boolean): Promise<void> {
|
||||
const stage = prod ? "prod" : "dev"
|
||||
|
||||
// Expand platform groups to individual targets
|
||||
const targets: CliTarget[] = []
|
||||
for (const group of platformGroups) {
|
||||
if (group === "all") {
|
||||
targets.push(...CLI_PLATFORMS.unix, ...CLI_PLATFORMS.windows)
|
||||
} else if (CLI_PLATFORMS[group]) {
|
||||
targets.push(...CLI_PLATFORMS[group])
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate targets
|
||||
const uniqueTargets = [...new Map(targets.map((t) => [`${t.GOOS}-${t.GOARCH}`, t])).values()]
|
||||
|
||||
logStep("CLI", `Building CLI binaries (${stage}) for ${uniqueTargets.length} platform(s)`)
|
||||
|
||||
await buildProtos()
|
||||
await buildProtosGo()
|
||||
|
||||
// Prepare directories
|
||||
const cliDir = path.join(ROOT_DIR, "cli")
|
||||
const cliBinDir = path.join(cliDir, "bin")
|
||||
const distBinDir = path.join(ROOT_DIR, "dist-standalone", "bin")
|
||||
|
||||
fs.mkdirSync(cliBinDir, { recursive: true })
|
||||
fs.mkdirSync(distBinDir, { recursive: true })
|
||||
|
||||
// Also ensure dist-standalone/extension exists for package.json
|
||||
const extensionDir = path.join(ROOT_DIR, "dist-standalone", "extension")
|
||||
fs.mkdirSync(extensionDir, { recursive: true })
|
||||
fs.copyFileSync(path.join(ROOT_DIR, "package.json"), path.join(extensionDir, "package.json"))
|
||||
|
||||
const ldflags = buildLdflags(prod)
|
||||
|
||||
// Build for each target
|
||||
for (const { GOOS, GOARCH } of uniqueTargets) {
|
||||
const ext = GOOS === "windows" ? ".exe" : ""
|
||||
const platformSuffix = `${GOOS}-${GOARCH === "amd64" ? "x64" : GOARCH}`
|
||||
|
||||
log(` Building for ${platformSuffix}...`, colors.blue)
|
||||
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
GOOS,
|
||||
GOARCH,
|
||||
GO111MODULE: "on",
|
||||
}
|
||||
|
||||
// Build cline binary
|
||||
const clineOutput = path.join(cliBinDir, `cline-${platformSuffix}${ext}`)
|
||||
run(`go build -ldflags "${ldflags}" -o "${clineOutput}" ./cmd/cline`, {
|
||||
cwd: cliDir,
|
||||
env,
|
||||
silent: true,
|
||||
})
|
||||
logSuccess(`cline-${platformSuffix}${ext} built`)
|
||||
|
||||
// Build cline-host binary
|
||||
const hostOutput = path.join(cliBinDir, `cline-host-${platformSuffix}${ext}`)
|
||||
run(`go build -ldflags "${ldflags}" -o "${hostOutput}" ./cmd/cline-host`, {
|
||||
cwd: cliDir,
|
||||
env,
|
||||
silent: true,
|
||||
})
|
||||
logSuccess(`cline-host-${platformSuffix}${ext} built`)
|
||||
|
||||
// Copy to dist-standalone/bin
|
||||
fs.copyFileSync(clineOutput, path.join(distBinDir, `cline-${platformSuffix}${ext}`))
|
||||
fs.copyFileSync(hostOutput, path.join(distBinDir, `cline-host-${platformSuffix}${ext}`))
|
||||
}
|
||||
|
||||
// If building for current platform, also create generic binaries
|
||||
const currentOS = process.platform === "win32" ? "windows" : process.platform
|
||||
const currentTarget = uniqueTargets.find((t) => t.GOOS === currentOS && t.GOARCH === process.arch)
|
||||
|
||||
if (currentTarget) {
|
||||
const ext = currentOS === "windows" ? ".exe" : ""
|
||||
const platformSuffix = `${currentOS}-${process.arch}`
|
||||
|
||||
// Copy to generic names in cli/bin and dist-standalone/bin
|
||||
fs.copyFileSync(path.join(cliBinDir, `cline-${platformSuffix}${ext}`), path.join(cliBinDir, `cline${ext}`))
|
||||
fs.copyFileSync(path.join(cliBinDir, `cline-host-${platformSuffix}${ext}`), path.join(cliBinDir, `cline-host${ext}`))
|
||||
fs.copyFileSync(path.join(distBinDir, `cline-${platformSuffix}${ext}`), path.join(distBinDir, `cline${ext}`))
|
||||
fs.copyFileSync(path.join(distBinDir, `cline-host-${platformSuffix}${ext}`), path.join(distBinDir, `cline-host${ext}`))
|
||||
|
||||
logSuccess(`Generic binaries created for current platform (${platformSuffix})`)
|
||||
}
|
||||
|
||||
logSuccess(`CLI binaries built (${stage})`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate telemetry environment variables are set
|
||||
*/
|
||||
function validateTelemetryEnvVars(): void {
|
||||
logStep("ENV", "Validating telemetry environment variables")
|
||||
|
||||
const requiredVars = ["TELEMETRY_SERVICE_API_KEY", "ERROR_SERVICE_API_KEY"]
|
||||
const optionalVars = ["CLINE_ENVIRONMENT", "POSTHOG_TELEMETRY_ENABLED"]
|
||||
const missingVars: string[] = []
|
||||
|
||||
for (const varName of requiredVars) {
|
||||
const value = process.env[varName]
|
||||
if (!value) {
|
||||
missingVars.push(varName)
|
||||
logError(`${varName} is not set`)
|
||||
} else {
|
||||
// Show first 10 chars for verification (don't expose full key)
|
||||
logSuccess(`${varName} is set (${value.substring(0, 10)}...)`)
|
||||
}
|
||||
}
|
||||
|
||||
for (const varName of optionalVars) {
|
||||
const value = process.env[varName]
|
||||
if (!value) {
|
||||
logWarning(`${varName} is not set (optional)`)
|
||||
} else {
|
||||
logSuccess(`${varName} is set: ${value}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (missingVars.length > 0) {
|
||||
log("\n", colors.reset)
|
||||
logError("Missing required environment variables:")
|
||||
log("", colors.reset)
|
||||
log(' export TELEMETRY_SERVICE_API_KEY="your_posthog_api_key"', colors.yellow)
|
||||
log(' export ERROR_SERVICE_API_KEY="your_error_tracking_api_key"', colors.yellow)
|
||||
log("", colors.reset)
|
||||
throw new Error(`Missing required environment variables: ${missingVars.join(", ")}`)
|
||||
}
|
||||
|
||||
logSuccess("Environment variables validated")
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean dist-standalone directory
|
||||
*/
|
||||
function cleanDistStandalone(): void {
|
||||
logStep("CLEAN", "Cleaning dist-standalone directory")
|
||||
|
||||
const distDir = path.join(ROOT_DIR, "dist-standalone")
|
||||
|
||||
if (fs.existsSync(distDir)) {
|
||||
fs.rmSync(distDir, { recursive: true, force: true })
|
||||
logSuccess("Removed dist-standalone directory")
|
||||
} else {
|
||||
logSuccess("dist-standalone directory does not exist, nothing to clean")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify telemetry keys were injected into compiled output
|
||||
*/
|
||||
function verifyTelemetryInjection(): void {
|
||||
logStep("VERIFY", "Verifying telemetry keys were injected")
|
||||
|
||||
const clineCorePath = path.join(ROOT_DIR, "dist-standalone", "cline-core.js")
|
||||
|
||||
if (!fs.existsSync(clineCorePath)) {
|
||||
throw new Error(`Compiled file not found: ${clineCorePath}`)
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(clineCorePath, "utf8")
|
||||
|
||||
// Check if process.env references still exist (bad - means they weren't replaced)
|
||||
if (content.includes("process.env.TELEMETRY_SERVICE_API_KEY")) {
|
||||
logError("Keys were NOT injected! Found 'process.env.TELEMETRY_SERVICE_API_KEY' in compiled code")
|
||||
logError("This means the environment variables were not replaced during build")
|
||||
throw new Error("Telemetry keys were not injected into compiled code")
|
||||
}
|
||||
|
||||
// Check if PostHog endpoint is present (good - means config is there)
|
||||
if (content.includes("data.cline.bot")) {
|
||||
logSuccess("Telemetry keys successfully injected into compiled code")
|
||||
} else {
|
||||
logWarning("Could not verify PostHog config in compiled code")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Print npm build summary
|
||||
*/
|
||||
function printNpmBuildSummary(): void {
|
||||
const distDir = path.join(ROOT_DIR, "dist-standalone")
|
||||
let version = "unknown"
|
||||
|
||||
try {
|
||||
const pkgPath = path.join(distDir, "package.json")
|
||||
if (fs.existsSync(pkgPath)) {
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")) as { version: string }
|
||||
version = pkg.version
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors reading version
|
||||
}
|
||||
|
||||
log("\n", colors.reset)
|
||||
logSeparator()
|
||||
log("NPM Package Build Summary", colors.green)
|
||||
logSeparator()
|
||||
log("")
|
||||
log(` Package location: ${colors.cyan}dist-standalone/${colors.reset}`, colors.reset)
|
||||
log(` Package version: ${colors.cyan}${version}${colors.reset}`, colors.reset)
|
||||
log("")
|
||||
log(" Next steps:", colors.bright)
|
||||
log(` 1. Test locally: ${colors.yellow}cd dist-standalone && npm link${colors.reset}`, colors.reset)
|
||||
log(` 2. Verify: ${colors.yellow}cline version${colors.reset}`, colors.reset)
|
||||
log(` 3. Publish: ${colors.yellow}cd dist-standalone && npm publish${colors.reset}`, colors.reset)
|
||||
log("")
|
||||
log(
|
||||
` ${colors.yellow}Note: Check PostHog dashboard after running cline commands to verify telemetry${colors.reset}`,
|
||||
colors.reset,
|
||||
)
|
||||
logSeparator()
|
||||
}
|
||||
|
||||
/**
|
||||
* Build npm package (CLI + standalone for npm distribution)
|
||||
*/
|
||||
async function buildNpm(prod: boolean): Promise<void> {
|
||||
const stage = prod ? "prod" : "dev"
|
||||
logStep("NPM", `Building npm package (${stage})`)
|
||||
|
||||
// Step 1: Validate telemetry env vars (prod only)
|
||||
if (prod) {
|
||||
validateTelemetryEnvVars()
|
||||
}
|
||||
|
||||
// Step 2: Clean dist-standalone directory
|
||||
cleanDistStandalone()
|
||||
|
||||
// Step 3: Build shared dependencies
|
||||
await buildProtos()
|
||||
await buildProtosGo()
|
||||
await buildWebview()
|
||||
|
||||
// Step 4: Build CLI for all platforms
|
||||
await buildCli(["all"], prod)
|
||||
|
||||
// Step 5: Build standalone with npm target
|
||||
logStep("STANDALONE", "Building standalone package for npm")
|
||||
const productionFlag = prod ? " --production" : ""
|
||||
run(`node esbuild.mjs --standalone${productionFlag}`)
|
||||
run("node scripts/package-standalone.mjs --target=npm")
|
||||
|
||||
// Step 6: Verify telemetry injection (prod only)
|
||||
if (prod) {
|
||||
verifyTelemetryInjection()
|
||||
}
|
||||
|
||||
// Step 7: Print summary
|
||||
printNpmBuildSummary()
|
||||
|
||||
logSuccess(`npm package built (${stage})`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build all surfaces with specified options
|
||||
*/
|
||||
async function buildAll(surface: Surface, platform: PlatformGroup, prod: boolean): Promise<void> {
|
||||
const stage = prod ? "prod" : "dev"
|
||||
|
||||
logSeparator()
|
||||
log(`Building: surface=${surface}, platform=${platform}, stage=${stage}`, colors.bright)
|
||||
logSeparator()
|
||||
|
||||
if (surface === "all") {
|
||||
// Build vscode and jetbrains in parallel, then cli
|
||||
// We need to be careful with shared resources (protos, webview)
|
||||
// So we build shared dependencies first, then parallelize
|
||||
|
||||
logStep("SHARED", "Building shared dependencies")
|
||||
await buildProtos()
|
||||
await buildProtosGo()
|
||||
await buildWebview()
|
||||
|
||||
// Now we can build vscode and jetbrains in parallel
|
||||
logStep("PARALLEL", "Building VS Code and JetBrains in parallel")
|
||||
|
||||
const vscodePromise = (async () => {
|
||||
const productionFlag = prod ? " --production" : ""
|
||||
run(`node esbuild.mjs${productionFlag}`)
|
||||
logSuccess(`VS Code extension built (${stage})`)
|
||||
})()
|
||||
|
||||
const jetbrainsPromise = (async () => {
|
||||
// Prepare dist-standalone directory
|
||||
const distDir = path.join(ROOT_DIR, "dist-standalone")
|
||||
const extensionDir = path.join(distDir, "extension")
|
||||
fs.mkdirSync(extensionDir, { recursive: true })
|
||||
fs.copyFileSync(path.join(ROOT_DIR, "package.json"), path.join(extensionDir, "package.json"))
|
||||
|
||||
const productionFlag = prod ? " --production" : ""
|
||||
run(`node esbuild.mjs --standalone${productionFlag}`)
|
||||
run("node scripts/package-standalone.mjs")
|
||||
logSuccess(`JetBrains standalone package built (${stage})`)
|
||||
})()
|
||||
|
||||
await Promise.all([vscodePromise, jetbrainsPromise])
|
||||
|
||||
// Build CLI (includes both unix and windows for "all" surface)
|
||||
await buildCli(["unix", "windows"], prod)
|
||||
} else if (surface === "vscode") {
|
||||
await buildVscode(prod)
|
||||
} else if (surface === "jetbrains") {
|
||||
await buildJetbrains(prod)
|
||||
} else if (surface === "cli") {
|
||||
const platforms: PlatformGroup[] = platform === "all" ? ["all"] : [platform]
|
||||
await buildCli(platforms, prod)
|
||||
} else if (surface === "npm") {
|
||||
await buildNpm(prod)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset build state for new stage
|
||||
*/
|
||||
function resetBuildState(): void {
|
||||
buildState.protos = false
|
||||
buildState.protosGo = false
|
||||
buildState.webview = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point
|
||||
*/
|
||||
async function main(): Promise<void> {
|
||||
const args = parseArgs()
|
||||
|
||||
logSeparator()
|
||||
log("Cline Build Orchestrator", colors.bright)
|
||||
logSeparator()
|
||||
|
||||
const startTime = Date.now()
|
||||
|
||||
try {
|
||||
if (args.allStages) {
|
||||
// Build both dev and prod
|
||||
log("\nBuilding all stages (dev + prod)...", colors.cyan)
|
||||
|
||||
// Dev build
|
||||
await buildAll(args.surface, args.platform, false)
|
||||
|
||||
// Reset build state for prod build
|
||||
resetBuildState()
|
||||
|
||||
// Prod build
|
||||
await buildAll(args.surface, args.platform, true)
|
||||
} else {
|
||||
await buildAll(args.surface, args.platform, args.prod)
|
||||
}
|
||||
|
||||
const duration = ((Date.now() - startTime) / 1000).toFixed(2)
|
||||
logSeparator()
|
||||
log(`Build completed successfully in ${duration}s`, colors.green)
|
||||
logSeparator()
|
||||
} catch (error) {
|
||||
const duration = ((Date.now() - startTime) / 1000).toFixed(2)
|
||||
logSeparator()
|
||||
logError(`Build failed after ${duration}s`)
|
||||
logError((error as Error).message)
|
||||
logSeparator()
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
+28
-23
@@ -22,14 +22,7 @@ echo ""
|
||||
# Always rebuild CLI to ensure latest changes
|
||||
echo -e "${CYAN}→${NC} ${DIM}Rebuilding CLI binaries...${NC}"
|
||||
cd "$PROJECT_ROOT"
|
||||
rm -rf "$PROJECT_ROOT/cli/bin"
|
||||
if command -v go >/dev/null 2>&1; then
|
||||
GO_BIN_DIR="$(go env GOPATH 2>/dev/null)/bin"
|
||||
if [ -d "$GO_BIN_DIR" ]; then
|
||||
export PATH="$GO_BIN_DIR:$PATH"
|
||||
fi
|
||||
fi
|
||||
if npm run compile-cli; then
|
||||
if npm run build:cli:all-platforms 2>&1 | grep -E "(built|error|Error)" || true; then
|
||||
echo -e "${GREEN}✓${NC} CLI binaries rebuilt"
|
||||
else
|
||||
echo -e "${YELLOW}⚠${NC} CLI build failed - aborting install"
|
||||
@@ -64,18 +57,14 @@ fi
|
||||
# Create installation directory
|
||||
mkdir -p "$INSTALL_DIR/bin"
|
||||
|
||||
# Copy standalone package first (cline-core.js, wasm files, etc.)
|
||||
rsync -a --exclude='bin' "$PROJECT_ROOT/dist-standalone/" "$INSTALL_DIR/"
|
||||
# Copy standalone package first (includes node_modules, cline-core.js, etc.)
|
||||
cp -r "$PROJECT_ROOT/dist-standalone/" "$INSTALL_DIR/"
|
||||
|
||||
# Install runtime dependencies (grpc-health-check, better-sqlite3, etc.)
|
||||
# These are external dependencies not bundled into cline-core.js
|
||||
echo -e "${CYAN}→${NC} ${DIM}Installing runtime dependencies...${NC}"
|
||||
cd "$PROJECT_ROOT/standalone/runtime-files"
|
||||
npm install --silent 2>/dev/null || npm install
|
||||
rm -rf "$INSTALL_DIR/node_modules"
|
||||
cp -r node_modules "$INSTALL_DIR/"
|
||||
cp -r vscode "$INSTALL_DIR/node_modules/"
|
||||
cd "$PROJECT_ROOT"
|
||||
# Check if OS is windows
|
||||
WIN=false
|
||||
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "win32" ]]; then
|
||||
WIN=true
|
||||
fi
|
||||
|
||||
# Detect platform for native modules
|
||||
os=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
@@ -84,17 +73,29 @@ if [[ "$arch" == "aarch64" ]]; then arch="arm64"; fi
|
||||
if [[ "$arch" == "x86_64" ]]; then arch="x64"; fi
|
||||
platform="$os-$arch"
|
||||
|
||||
# Manually set platform to windows, as windows is tricky
|
||||
if [[ "$WIN" = true ]]; then
|
||||
platform="win-$arch"
|
||||
fi
|
||||
|
||||
echo -e "${CYAN}→${NC} ${DIM}Detected platform as $platform${NC}"
|
||||
|
||||
# Copy platform-specific native modules (like better-sqlite3)
|
||||
if [ -d "$PROJECT_ROOT/dist-standalone/binaries/$platform/node_modules" ]; then
|
||||
echo -e "${CYAN}→${NC} ${DIM}Installing platform-specific modules for $platform${NC}"
|
||||
cp -r "$PROJECT_ROOT/dist-standalone/binaries/$platform/node_modules/"* "$INSTALL_DIR/node_modules/" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
|
||||
# Copy binaries (this will create/overwrite the bin directory)
|
||||
mkdir -p "$INSTALL_DIR/bin"
|
||||
cp "$PROJECT_ROOT/cli/bin/cline" "$INSTALL_DIR/bin/"
|
||||
cp "$PROJECT_ROOT/cli/bin/cline-host" "$INSTALL_DIR/bin/"
|
||||
|
||||
if [[ "$WIN" = true ]]; then
|
||||
cp "$PROJECT_ROOT/cli/bin/cline-windows-amd64.exe" "$INSTALL_DIR/bin/cline.exe"
|
||||
cp "$PROJECT_ROOT/cli/bin/cline-host-windows-amd64.exe" "$INSTALL_DIR/bin/cline-host.exe"
|
||||
else
|
||||
cp "$PROJECT_ROOT/cli/bin/cline" "$INSTALL_DIR/bin/"
|
||||
cp "$PROJECT_ROOT/cli/bin/cline-host" "$INSTALL_DIR/bin/"
|
||||
fi
|
||||
# Use system Node.js (symlink to avoid copying large binary)
|
||||
if command -v node >/dev/null 2>&1; then
|
||||
ln -sf "$(which node)" "$INSTALL_DIR/bin/node"
|
||||
@@ -112,7 +113,11 @@ chmod +x "$INSTALL_DIR/bin/node" 2>/dev/null || true
|
||||
# Rebuild better-sqlite3 for system Node.js version
|
||||
echo -e "${CYAN}→${NC} ${DIM}Rebuilding native modules for Node.js $(node --version)...${NC}"
|
||||
cd "$INSTALL_DIR"
|
||||
npm rebuild better-sqlite3 > /dev/null 2>&1
|
||||
npm rebuild better-sqlite3
|
||||
|
||||
mkdir -p "$INSTALL_DIR/dist-standalone/node_modules/better-sqlite3"
|
||||
cp -r "$INSTALL_DIR/node_modules/." "$INSTALL_DIR/dist-standalone/node_modules/better-sqlite3/"
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
echo -e "${GREEN}✓${NC} Native modules rebuilt"
|
||||
|
||||
|
||||
+21
-3
@@ -23,6 +23,13 @@ FORCE_INSTALL="${FORCE_INSTALL:-false}"
|
||||
os=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
arch=$(uname -m)
|
||||
|
||||
# Check if OS is windows
|
||||
WIN=false
|
||||
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "win32" ]]; then
|
||||
WIN=true
|
||||
INSTALL_DIR="${CLINE_INSTALL_DIR:-$LOCALAPPDATA/.cline/cli}"
|
||||
fi
|
||||
|
||||
# Normalize architecture names
|
||||
if [[ "$arch" == "aarch64" ]]; then
|
||||
arch="arm64"
|
||||
@@ -47,8 +54,11 @@ case "$os" in
|
||||
platform="linux-$arch"
|
||||
;;
|
||||
*)
|
||||
[[ "$WIN" == true ]] || {
|
||||
echo -e "${RED}${BOLD}ERROR${NC} ${RED}Unsupported OS: $os${NC}" >&2
|
||||
exit 1
|
||||
}
|
||||
platform="win-$arch"
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -369,9 +379,17 @@ install_cline() {
|
||||
|
||||
# Extract package
|
||||
print_step "Extracting package"
|
||||
if ! tar -xzf "$package_file" -C "$INSTALL_DIR" --strip-components=0; then
|
||||
print_error "Failed to extract package"
|
||||
exit 1
|
||||
if ! tar -xzf "$package_file" -C "$INSTALL_DIR" --strip-components=0; then
|
||||
if [[ "$WIN" == true ]]; then
|
||||
echo -e "${RED}${BOLD}TAR command failed. Attempting backup method...${NC}"
|
||||
if ! unzip -oq "$package_file" -d "$INSTALL_DIR"; then
|
||||
print_error "Failed to extract package"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
print_error "Failed to extract package"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Make binaries executable
|
||||
|
||||
@@ -0,0 +1,584 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
/**
|
||||
* Unified installer for Cline
|
||||
*
|
||||
* Supports local (development) installation, and standard (production) installation
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx scripts/install.ts [--local] [options]
|
||||
*
|
||||
* Examples:
|
||||
* npx tsx scripts/install.ts
|
||||
* npx tsx scripts/install.ts --local
|
||||
* npx tsx scripts/install.ts 3.42.1
|
||||
*/
|
||||
|
||||
import { execSync } from "node:child_process"
|
||||
import fs from "node:fs/promises"
|
||||
import { homedir } from "node:os"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
const ROOT_DIR = path.resolve(__dirname, "..")
|
||||
|
||||
interface ParsedArgs {
|
||||
local: boolean
|
||||
version: string
|
||||
}
|
||||
|
||||
interface RunOptions {
|
||||
cwd?: string
|
||||
env?: NodeJS.ProcessEnv
|
||||
silent?: boolean
|
||||
}
|
||||
|
||||
interface Platform {
|
||||
name: "aix" | "darwin" | "freebsd" | "linux" | "openbsd" | "sunos" | "win" | "android" | "haiku" | "cygwin" | "netbsd"
|
||||
arch: "arm" | "arm64" | "ia32" | "loong64" | "mips" | "mipsel" | "ppc" | "ppc64" | "riscv64" | "s390" | "s390x" | "x64"
|
||||
}
|
||||
|
||||
const SupportedPlatforms: Platform[] = [
|
||||
{ name: "darwin", arch: "arm64" },
|
||||
{ name: "darwin", arch: "x64" },
|
||||
{ name: "linux", arch: "arm64" },
|
||||
{ name: "linux", arch: "x64" },
|
||||
{ name: "win", arch: "x64" },
|
||||
]
|
||||
|
||||
// Colors for terminal output
|
||||
const colors = {
|
||||
reset: "\x1b[0m",
|
||||
bright: "\x1b[1m",
|
||||
red: "\x1b[31m",
|
||||
green: "\x1b[32m",
|
||||
yellow: "\x1b[33m",
|
||||
blue: "\x1b[34m",
|
||||
cyan: "\x1b[36m",
|
||||
magenta: "\x1b[35m",
|
||||
} as const
|
||||
|
||||
function log(message: string, color: string = colors.reset): void {
|
||||
console.log(`${color}${message}${colors.reset}`)
|
||||
}
|
||||
|
||||
function logStep(step: string, message: string): void {
|
||||
log(`\n${colors.bright}[${step}]${colors.reset} ${message}`, colors.cyan)
|
||||
}
|
||||
|
||||
function logSuccess(message: string): void {
|
||||
log(` ✓ ${message}`, colors.green)
|
||||
}
|
||||
|
||||
function logError(message: string): void {
|
||||
log(` ✗ ${message}`, colors.red)
|
||||
}
|
||||
|
||||
function logWarning(message: string): void {
|
||||
log(` ! ${message}`, colors.yellow)
|
||||
}
|
||||
|
||||
function logSeparator(): void {
|
||||
log("=".repeat(60), colors.bright)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse command line arguments
|
||||
*/
|
||||
function parseArgs(): ParsedArgs {
|
||||
const args = process.argv.slice(2)
|
||||
const result: ParsedArgs = {
|
||||
local: false,
|
||||
version: "latest",
|
||||
}
|
||||
|
||||
for (const arg of args) {
|
||||
if (arg === "--local") {
|
||||
result.local = true
|
||||
} else if (arg === "--help" || arg === "-h") {
|
||||
console.log(`
|
||||
Unified Installer for Cline
|
||||
|
||||
Usage:
|
||||
npx tsx scripts/install.ts [--local]
|
||||
|
||||
Options:
|
||||
--local Install using local build instead of production build
|
||||
`)
|
||||
process.exit(0)
|
||||
} else {
|
||||
// Assume arg is version and attempt to set it. Create validator later
|
||||
result.version = arg
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a command synchronously with cross-platform support
|
||||
*/
|
||||
function run(cmd: string, opts: RunOptions = {}): void {
|
||||
const { cwd = ROOT_DIR, env = process.env, silent = false } = opts
|
||||
|
||||
if (!silent) {
|
||||
log(` $ ${cmd}`, colors.yellow)
|
||||
}
|
||||
|
||||
try {
|
||||
execSync(cmd, {
|
||||
cwd,
|
||||
env,
|
||||
stdio: silent ? "pipe" : "inherit",
|
||||
})
|
||||
} catch {
|
||||
throw new Error(`Command failed: ${cmd}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a command and return the output
|
||||
*/
|
||||
function runCapture(cmd: string, opts: { cwd?: string } = {}): string | null {
|
||||
const { cwd = ROOT_DIR } = opts
|
||||
|
||||
try {
|
||||
return execSync(cmd, {
|
||||
cwd,
|
||||
encoding: "utf8",
|
||||
}).trim()
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function detectOS(): Promise<Platform> {
|
||||
const sanitizedPlatform = (() => {
|
||||
switch (process.platform) {
|
||||
case "win32":
|
||||
return "win"
|
||||
default:
|
||||
return process.platform
|
||||
}
|
||||
})()
|
||||
return { name: sanitizedPlatform, arch: process.arch }
|
||||
}
|
||||
|
||||
function ValidateOS({ name, arch }: Platform) {
|
||||
return new Promise((resolve) => {
|
||||
const isSupported = !!SupportedPlatforms.find((platform) => {
|
||||
if (platform.arch === arch) {
|
||||
if (platform.name === name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
})
|
||||
if (isSupported) {
|
||||
resolve(true)
|
||||
} else {
|
||||
throw new Error(`Unsupported Operating System: ${name}-${arch}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function getInstallDirectory({ name }: Platform) {
|
||||
switch (name) {
|
||||
case "win":
|
||||
return (
|
||||
(process.env.APPDATA && path.join(process.env.APPDATA, ".cline", ".cli")) ||
|
||||
path.join(homedir(), "AppData", "Roaming", ".cline", ".cli")
|
||||
)
|
||||
case "darwin":
|
||||
case "linux":
|
||||
return (
|
||||
(process.env.XDG_CONFIG_HOME && path.join(process.env.XDG_CONFIG_HOME, ".cline", ".cli")) ||
|
||||
path.join(homedir(), ".cline", ".")
|
||||
)
|
||||
default:
|
||||
throw new Error(`Unhandled operating system: ${name}\n\nPlease report this error to support@cline.bot or on github!`)
|
||||
}
|
||||
}
|
||||
|
||||
async function rebuildCLI({ name }: Platform) {
|
||||
try {
|
||||
// Build for current platform
|
||||
const buildFor = name === "win" ? "windows" : "unix"
|
||||
log(`Building explicitly for ${buildFor}`)
|
||||
execSync(`npm run build:cli:${buildFor}`, { stdio: "inherit" })
|
||||
} catch (error) {
|
||||
throw new Error(`Error building CLI binaries: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function rebuildClineCore() {
|
||||
try {
|
||||
// Build standalone package
|
||||
execSync(`npm run compile-standalone`, { stdio: "inherit" })
|
||||
} catch (error) {
|
||||
throw new Error(`Error building Cline Core: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function removePreviousCLI({ name }: Platform, installDirectory: string) {
|
||||
switch (name) {
|
||||
// biome-ignore lint/suspicious/noFallthroughSwitchClause: Only windows install is deprecated. This shouldn't be a factor, as we're adding support, but it's future-proofing.
|
||||
case "win":
|
||||
// Check if a previous installation exists at old install location
|
||||
const oldInstallDir = path.join(homedir(), ".cline")
|
||||
const deprecatedInstallDir = await (async () => {
|
||||
try {
|
||||
await fs.access(oldInstallDir)
|
||||
return true
|
||||
} catch (_error) {
|
||||
return false
|
||||
}
|
||||
})()
|
||||
if (deprecatedInstallDir) {
|
||||
logWarning(
|
||||
"Deprecated installation of Cline CLI detected. Migration will be attempted, however data loss may occur.",
|
||||
)
|
||||
log("Copying old files...", colors.yellow)
|
||||
try {
|
||||
await fs.cp(oldInstallDir, path.resolve(installDirectory, "../"), { recursive: true })
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to copy old files: ${error}`)
|
||||
}
|
||||
try {
|
||||
log("Removing old directory...", colors.yellow)
|
||||
await fs.rm(oldInstallDir, { recursive: true, force: true })
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Unable to perform migration. The following error was returned: \n\n${error}\n\nPlease manually move the .cline folder located at ${oldInstallDir} to ${path.resolve(installDirectory, "..")}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
case "darwin":
|
||||
case "linux":
|
||||
log("Removing existing installation for clean install, if necessary", colors.yellow)
|
||||
const previousInstall = await (async () => {
|
||||
try {
|
||||
await fs.access(installDirectory)
|
||||
return true
|
||||
} catch (_error) {
|
||||
return false
|
||||
}
|
||||
})()
|
||||
if (previousInstall) {
|
||||
try {
|
||||
await fs.rm(installDirectory, { recursive: true, force: true })
|
||||
} catch (error) {
|
||||
throw new Error(`Unable to remove previous installation: ${error}`)
|
||||
}
|
||||
} else {
|
||||
log("No previous cline installation detected.", colors.green)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureDirectory(directory: string) {
|
||||
const dirExists = await (async () => {
|
||||
try {
|
||||
await fs.access(directory)
|
||||
return true
|
||||
} catch (_error) {
|
||||
return false
|
||||
}
|
||||
})()
|
||||
if (dirExists) {
|
||||
return
|
||||
} else {
|
||||
try {
|
||||
await fs.mkdir(directory, { recursive: true })
|
||||
} catch (error) {
|
||||
throw new Error(`Unable to create installation directory: ${error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function copyStandalone(installDirectory: string) {
|
||||
const standaloneDir: string = path.resolve(ROOT_DIR, "dist-standalone")
|
||||
try {
|
||||
await fs.cp(standaloneDir, path.resolve(installDirectory, "dist-standalone"), { recursive: true })
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to copy standalone package files: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function copyPlatformModules({ name, arch }: Platform, installDirectory: string) {
|
||||
const moduleDir: string = path.resolve(ROOT_DIR, "dist-standalone", "binaries", `${name}-${arch}`, "node_modules")
|
||||
try {
|
||||
await fs.cp(moduleDir, path.resolve(installDirectory, "dist-standalone", "node_modules"), { recursive: true })
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to copy platform module files for ${name}-${arch}: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function copyBinaries({ name, arch }: Platform, installDirectory: string) {
|
||||
try {
|
||||
switch (name) {
|
||||
case "win":
|
||||
await fs.cp(
|
||||
path.resolve(ROOT_DIR, "cli", "bin", `cline-windows-${arch}.exe`),
|
||||
path.resolve(installDirectory, "bin", "cline.exe"),
|
||||
)
|
||||
await fs.cp(
|
||||
path.resolve(ROOT_DIR, "cli", "bin", `cline-host-windows-${arch}.exe`),
|
||||
path.resolve(installDirectory, "bin", "cline-host.exe"),
|
||||
)
|
||||
break
|
||||
case "darwin":
|
||||
case "linux":
|
||||
await fs.cp(
|
||||
path.resolve(ROOT_DIR, "cli", "bin", `cline-${name}-${arch}`),
|
||||
path.resolve(installDirectory, "bin", "cline"),
|
||||
)
|
||||
await fs.cp(
|
||||
path.resolve(ROOT_DIR, "cli", "bin", `cline-host-${name}-${arch}`),
|
||||
path.resolve(installDirectory, "bin", "cline-host"),
|
||||
)
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to copy platform module files for ${name}-${arch}: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
function linkSystemNode(installDirectory: string): Promise<string> {
|
||||
return new Promise(async (resolve) => {
|
||||
try {
|
||||
await fs.cp(path.resolve(process.argv[0]), path.resolve(installDirectory, "bin", "node"))
|
||||
const nodev = execSync("node -v", { cwd: path.resolve(installDirectory, "bin") }).toString()
|
||||
resolve(nodev)
|
||||
} catch (error: any) {
|
||||
if (error.code === "EEXIST") {
|
||||
const nodev = execSync("node -v", { cwd: path.resolve(installDirectory, "bin") }).toString()
|
||||
resolve(nodev)
|
||||
return
|
||||
}
|
||||
throw new Error(`Unable to copy node: ${error}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function makeExecutable({ name }: Platform, installDirectory: string) {
|
||||
try {
|
||||
switch (name) {
|
||||
case "win":
|
||||
await fs.chmod(path.resolve(installDirectory, "bin", "cline.exe"), 0x755)
|
||||
await fs.chmod(path.resolve(installDirectory, "bin", "cline-host.exe"), 0x755)
|
||||
break
|
||||
case "darwin":
|
||||
case "linux":
|
||||
await fs.chmod(path.resolve(installDirectory, "bin", "cline"), 0x755)
|
||||
await fs.chmod(path.resolve(installDirectory, "bin", "cline-host"), 0x755)
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to set permissions for files: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function rebuildNativeModules(installDirectory: string) {
|
||||
try {
|
||||
execSync("npm rebuild better-sqlite3", { cwd: installDirectory })
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to rebuild modules: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function configurePATH({ name }: Platform, installDirectory: string) {
|
||||
try {
|
||||
const absolutePath = path.resolve(installDirectory, "bin")
|
||||
|
||||
if (name === "win") {
|
||||
try {
|
||||
// Get current user PATH
|
||||
const stdout = execSync(`powershell -Command "[Environment]::GetEnvironmentVariable('Path', 'User')"`, {
|
||||
encoding: "utf-8",
|
||||
})
|
||||
|
||||
const currentPath = stdout.trim()
|
||||
|
||||
// Check if already in PATH
|
||||
if (currentPath.split(";").some((p) => p.toLowerCase() === absolutePath.toLowerCase())) {
|
||||
log("Directory already in PATH", colors.green)
|
||||
return
|
||||
}
|
||||
|
||||
// Add to PATH
|
||||
const newPath = currentPath ? `${currentPath};${absolutePath}` : absolutePath
|
||||
|
||||
execSync(`powershell -Command "[Environment]::SetEnvironmentVariable('Path', '${newPath}', 'User')"`)
|
||||
|
||||
log("Added to PATH. Restart your terminal for changes to take effect.", colors.green)
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to add to Windows PATH: ${error}`)
|
||||
}
|
||||
} else {
|
||||
// Unix-like (Linux, macOS, etc.)
|
||||
|
||||
const shellConfigFiles = [
|
||||
path.join(homedir(), ".bashrc"),
|
||||
path.join(homedir(), ".bash_profile"),
|
||||
path.join(homedir(), ".zshrc"),
|
||||
path.join(homedir(), ".profile"),
|
||||
]
|
||||
|
||||
const exportLine = `\nexport PATH="$PATH:${absolutePath}"\n`
|
||||
|
||||
try {
|
||||
// Determine which shell config file to use
|
||||
let targetFile: string | null = null
|
||||
|
||||
for (const file of shellConfigFiles) {
|
||||
try {
|
||||
await fs.access(file)
|
||||
targetFile = file
|
||||
break
|
||||
} catch (_error) {}
|
||||
}
|
||||
|
||||
// Default to .bashrc if none exist
|
||||
if (!targetFile) {
|
||||
targetFile = path.join(homedir(), ".bashrc")
|
||||
}
|
||||
|
||||
// Check if already present
|
||||
const content = await fs.readFile(targetFile, "utf-8")
|
||||
|
||||
if (content.includes(`PATH="$PATH:${absolutePath}"`)) {
|
||||
log("Directory already in PATH", colors.green)
|
||||
return
|
||||
}
|
||||
|
||||
// Append to file
|
||||
fs.appendFile(targetFile, exportLine)
|
||||
|
||||
log(`Added to PATH in ${targetFile}. Run 'source ${targetFile}' or restart your terminal.`, colors.green)
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to configure PATH: ${error}`)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to configure PATH: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function installLocal() {
|
||||
logStep("1", "Checking operating system...")
|
||||
// Detect OS
|
||||
const platform: Platform = await detectOS()
|
||||
logStep("2", "Validating compatibility...")
|
||||
// Validate support
|
||||
await ValidateOS(platform)
|
||||
logSuccess(`Operating System: ${platform.name}-${platform.arch} is supported.`)
|
||||
// Get install directory
|
||||
logStep("3", "Getting install directory...")
|
||||
const installDirectory = await getInstallDirectory(platform)
|
||||
logSuccess(`Set install directory to: ${installDirectory}`)
|
||||
// Rebuild the binaries
|
||||
logStep("4", "Rebuilding binaries...")
|
||||
await rebuildCLI(platform)
|
||||
logSuccess("Binaries built.")
|
||||
// Rebuild standalone package
|
||||
logStep("5", "Rebuilding standalone package...")
|
||||
await rebuildClineCore()
|
||||
logSuccess("Standalone Package rebuilt.")
|
||||
// Remove existing Cline installation
|
||||
logStep("6", "Remove previous Cline CLI Installation")
|
||||
await removePreviousCLI(platform, installDirectory)
|
||||
// Create install directory
|
||||
logStep("7", "Ensuring install directory exists")
|
||||
await ensureDirectory(path.resolve(installDirectory, "bin"))
|
||||
logSuccess(`Validated install directory: ${installDirectory}`)
|
||||
// Copy standalone package first
|
||||
logStep("8", "Copying standalone package")
|
||||
await copyStandalone(installDirectory)
|
||||
logSuccess("Standalone package copied.")
|
||||
// Copy platform-specific modules
|
||||
logStep("9", `Installing platform-specific modules for ${platform.name}-${platform.arch}`)
|
||||
await copyPlatformModules(platform, installDirectory)
|
||||
logSuccess("Modules copied.")
|
||||
// Copy binaries
|
||||
logStep("10", "Copying platform binaries")
|
||||
await copyBinaries(platform, installDirectory)
|
||||
logSuccess("Binaries copied successfully.")
|
||||
// Use system node (via symlink)
|
||||
logStep("11", "Linking system node to Cline")
|
||||
const systemNodeVersion = await linkSystemNode(installDirectory)
|
||||
logSuccess("Link successful.")
|
||||
// Make binaries executable
|
||||
logStep("12", "Ensuring binaries are executable")
|
||||
await makeExecutable(platform, installDirectory)
|
||||
logSuccess("Files are executable.")
|
||||
// Rebuild better-sqlite3 for system node.js
|
||||
logStep("13", `Rebuilding native modules for Node.js version ${systemNodeVersion}`)
|
||||
await rebuildNativeModules(installDirectory)
|
||||
logSuccess("Native modules rebuilt.")
|
||||
// Configure system PATH
|
||||
logStep("14", "Configuring system PATH")
|
||||
await configurePATH(platform, installDirectory)
|
||||
logSuccess("Linked Cline CLI to PATH.")
|
||||
// Installation Complete
|
||||
logSeparator()
|
||||
log("Cline CLI has been installed!", colors.green)
|
||||
log("Now you're Cooking with Cline CLI!", colors.magenta)
|
||||
logSeparator()
|
||||
}
|
||||
|
||||
async function installProd() {
|
||||
// Get install directory
|
||||
// Set github Repo
|
||||
// Get requested version, default to latest
|
||||
// Check prerequisites
|
||||
// Check rate limit
|
||||
// Get requested release
|
||||
// Show info
|
||||
// Remove existing Cline installation
|
||||
// Download package
|
||||
// Inflate package to install directory
|
||||
// Validate
|
||||
// Configure system PATH
|
||||
// Installation Complete
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point
|
||||
*/
|
||||
async function main(): Promise<void> {
|
||||
const args = parseArgs()
|
||||
|
||||
logSeparator()
|
||||
log("Cline Installer", colors.bright)
|
||||
logSeparator()
|
||||
|
||||
const startTime = Date.now()
|
||||
|
||||
try {
|
||||
if (args.local) {
|
||||
// Build both dev and prod
|
||||
log("\nInstalling for Local Development...", colors.cyan)
|
||||
|
||||
// Dev Install
|
||||
await installLocal()
|
||||
} else {
|
||||
await installProd()
|
||||
}
|
||||
|
||||
const duration = ((Date.now() - startTime) / 1000).toFixed(2)
|
||||
logSeparator()
|
||||
log(`Installed successfully in ${duration}s`, colors.green)
|
||||
logSeparator()
|
||||
} catch (error) {
|
||||
const duration = ((Date.now() - startTime) / 1000).toFixed(2)
|
||||
logSeparator()
|
||||
logError(`Install failed after ${duration}s`)
|
||||
logError((error as Error).message)
|
||||
logSeparator()
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -54,6 +54,390 @@ async function installNodeDependencies() {
|
||||
fs.renameSync(`${BUILD_DIR}/vscode`, `${BUILD_DIR}/node_modules/vscode`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy CLI binaries (cline and cline-host) for all platforms
|
||||
* The Go binaries are cross-compiled for darwin/linux arm64/amd64
|
||||
*/
|
||||
async function copyCliBinaries() {
|
||||
console.log("Copying CLI binaries for all platforms...")
|
||||
|
||||
const platforms = [
|
||||
{ os: "darwin", arch: "arm64" },
|
||||
{ os: "darwin", arch: "amd64" },
|
||||
{ os: "linux", arch: "amd64" },
|
||||
{ os: "linux", arch: "arm64" },
|
||||
{ os: "win32", arch: "amd64" },
|
||||
]
|
||||
|
||||
const binDir = path.join(BUILD_DIR, "bin")
|
||||
|
||||
// Create bin directory
|
||||
fs.mkdirSync(binDir, { recursive: true })
|
||||
|
||||
// Copy all platform-specific binaries
|
||||
for (const { os, arch } of platforms) {
|
||||
const platformSuffix = `${os}-${arch}`
|
||||
|
||||
// Copy cline binary
|
||||
const clineSource = path.join(CLI_BINARIES_DIR, `cline-${platformSuffix}`)
|
||||
const clineDest = path.join(binDir, `cline-${platformSuffix}`)
|
||||
|
||||
if (!fs.existsSync(clineSource)) {
|
||||
console.error(`Error: CLI binary not found at ${clineSource}`)
|
||||
console.error(`Please run: npm run compile-cli`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await cpr(clineSource, clineDest)
|
||||
fs.chmodSync(clineDest, 0o755)
|
||||
console.log(`✓ cline-${platformSuffix} copied`)
|
||||
|
||||
// Copy cline-host binary
|
||||
const hostSource = path.join(CLI_BINARIES_DIR, `cline-host-${platformSuffix}`)
|
||||
const hostDest = path.join(binDir, `cline-host-${platformSuffix}`)
|
||||
|
||||
if (!fs.existsSync(hostSource)) {
|
||||
console.error(`Error: CLI binary not found at ${hostSource}`)
|
||||
console.error(`Please run: npm run compile-cli`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await cpr(hostSource, hostDest)
|
||||
fs.chmodSync(hostDest, 0o755)
|
||||
console.log(`✓ cline-host-${platformSuffix} copied`)
|
||||
}
|
||||
|
||||
console.log(`✓ All platform binaries copied to ${binDir}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy proto descriptors directory
|
||||
* The proto/descriptor_set.pb file is needed by cline-core for gRPC reflection
|
||||
*/
|
||||
async function copyProtoDescriptors() {
|
||||
console.log("Copying proto descriptors...")
|
||||
|
||||
const protoSource = "proto"
|
||||
const protoDest = path.join(BUILD_DIR, "proto")
|
||||
|
||||
// Check if proto directory exists
|
||||
if (!fs.existsSync(protoSource)) {
|
||||
console.error(`Error: proto directory not found at ${protoSource}`)
|
||||
console.error(`Please ensure the proto files have been generated`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Check if descriptor_set.pb exists
|
||||
const descriptorPath = path.join(protoSource, "descriptor_set.pb")
|
||||
if (!fs.existsSync(descriptorPath)) {
|
||||
console.error(`Error: proto/descriptor_set.pb not found at ${descriptorPath}`)
|
||||
console.error(`Please run: npm run protos`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Copy the entire proto directory
|
||||
await cpr(protoSource, protoDest)
|
||||
|
||||
console.log(`✓ Proto descriptors copied to ${protoDest}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy ripgrep binary for the current platform
|
||||
* Ripgrep is needed by cline-core for file searching
|
||||
*/
|
||||
async function copyRipgrepBinary() {
|
||||
const currentPlatform = getCurrentPlatform()
|
||||
const binaryName = currentPlatform.startsWith("win") ? "rg.exe" : "rg"
|
||||
const ripgrepBinarySource = path.join(RIPGREP_BINARIES_DIR, currentPlatform, binaryName)
|
||||
const ripgrepBinaryDest = path.join(BUILD_DIR, binaryName)
|
||||
|
||||
console.log(`Copying ripgrep binary for ${currentPlatform}...`)
|
||||
|
||||
// Check if ripgrep binaries exist, download if missing
|
||||
if (!fs.existsSync(ripgrepBinarySource)) {
|
||||
console.log(`Ripgrep binary not found, downloading...`)
|
||||
try {
|
||||
execSync("npm run download-ripgrep", { stdio: "inherit" })
|
||||
} catch (error) {
|
||||
console.error(`Error downloading ripgrep: ${error.message}`)
|
||||
console.error(`Please run: npm run download-ripgrep`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Check again after download
|
||||
if (!fs.existsSync(ripgrepBinarySource)) {
|
||||
console.error(`Error: Ripgrep binary still not found at ${ripgrepBinarySource}`)
|
||||
console.error(`Download may have failed. Please run: npm run download-ripgrep`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Copy ripgrep binary to the root of dist-standalone (where cline-core.js is)
|
||||
await cpr(ripgrepBinarySource, ripgrepBinaryDest)
|
||||
|
||||
// Make it executable (Unix only)
|
||||
if (!currentPlatform.startsWith("win")) {
|
||||
fs.chmodSync(ripgrepBinaryDest, 0o755)
|
||||
}
|
||||
|
||||
console.log(`✓ Ripgrep binary copied to ${ripgrepBinaryDest}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a VERSION file with build metadata
|
||||
*/
|
||||
async function createVersionFile() {
|
||||
const packageJson = JSON.parse(fs.readFileSync("package.json", "utf8"))
|
||||
const version = packageJson.version
|
||||
const platform = getCurrentPlatform()
|
||||
const buildDate = new Date().toISOString()
|
||||
|
||||
const versionInfo = {
|
||||
version,
|
||||
platform,
|
||||
buildDate,
|
||||
nodeVersion: TARGET_NODE_VERSION,
|
||||
}
|
||||
|
||||
const versionPath = path.join(BUILD_DIR, "VERSION.txt")
|
||||
fs.writeFileSync(versionPath, JSON.stringify(versionInfo, null, 2))
|
||||
|
||||
console.log(`✓ VERSION file created: ${version} (${platform})`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy NPM package files (package.json, README.md, and man page) from cli/ directory
|
||||
*/
|
||||
async function createNpmPackageFiles() {
|
||||
console.log("Copying NPM package files...")
|
||||
|
||||
// Copy package.json from cli/ directory
|
||||
const packageJsonSource = path.join("cli", "package.json")
|
||||
const packageJsonDest = path.join(BUILD_DIR, "package.json")
|
||||
|
||||
if (!fs.existsSync(packageJsonSource)) {
|
||||
console.error(`Error: NPM package.json not found at ${packageJsonSource}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await cpr(packageJsonSource, packageJsonDest)
|
||||
console.log(`✓ package.json copied from ${packageJsonSource}`)
|
||||
|
||||
// Copy README.md from cli/ directory
|
||||
const readmeSource = path.join("cli", "README.md")
|
||||
const readmeDest = path.join(BUILD_DIR, "README.md")
|
||||
|
||||
if (!fs.existsSync(readmeSource)) {
|
||||
console.error(`Error: NPM README.md not found at ${readmeSource}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await cpr(readmeSource, readmeDest)
|
||||
console.log(`✓ README.md copied from ${readmeSource}`)
|
||||
|
||||
// Copy man page from cli/man/ directory
|
||||
const manPageSource = path.join("cli", "man", "cline.1")
|
||||
const manDir = path.join(BUILD_DIR, "man")
|
||||
const manPageDest = path.join(manDir, "cline.1")
|
||||
|
||||
if (!fs.existsSync(manPageSource)) {
|
||||
console.error(`Error: Man page not found at ${manPageSource}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Create man directory if it doesn't exist
|
||||
fs.mkdirSync(manDir, { recursive: true })
|
||||
|
||||
await cpr(manPageSource, manPageDest)
|
||||
console.log(`✓ Man page copied from ${manPageSource}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create fake_node_modules directory with vscode stub
|
||||
* This directory will be added to NODE_PATH so Node.js can find the vscode module
|
||||
* without npm interfering with the real node_modules directory
|
||||
*/
|
||||
async function createFakeNodeModules() {
|
||||
console.log("Creating fake_node_modules with vscode stub...")
|
||||
|
||||
const vscodeSource = path.join(BUILD_DIR, "node_modules", "vscode")
|
||||
const fakeNodeModulesDir = path.join(BUILD_DIR, "fake_node_modules")
|
||||
const vscodeDest = path.join(fakeNodeModulesDir, "vscode")
|
||||
|
||||
if (!fs.existsSync(vscodeSource)) {
|
||||
console.error(`Error: vscode stub module not found at ${vscodeSource}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Create fake_node_modules directory
|
||||
fs.mkdirSync(fakeNodeModulesDir, { recursive: true })
|
||||
|
||||
// Copy vscode stub into fake_node_modules
|
||||
await cpr(vscodeSource, vscodeDest)
|
||||
|
||||
console.log(`✓ fake_node_modules/vscode created at ${vscodeDest}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create .npmignore file to ensure necessary files are included
|
||||
*/
|
||||
async function createNpmIgnoreFile() {
|
||||
console.log("Creating .npmignore file...")
|
||||
|
||||
// Create .npmignore that excludes build artifacts
|
||||
// Note: proto/ directory is NOT excluded because proto/descriptor_set.pb is needed at runtime
|
||||
const npmignoreContent = `# Exclude build artifacts and unnecessary files
|
||||
binaries/
|
||||
ripgrep-binaries/
|
||||
standalone.zip
|
||||
cline-core.js.map
|
||||
package-lock.json
|
||||
tree-sitter*.wasm
|
||||
node_modules/vscode
|
||||
`
|
||||
|
||||
const npmignorePath = path.join(BUILD_DIR, ".npmignore")
|
||||
fs.writeFileSync(npmignorePath, npmignoreContent)
|
||||
|
||||
console.log(`✓ .npmignore created`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create postinstall script for NPM package
|
||||
* This script selects the correct platform-specific binary and creates symlinks
|
||||
*/
|
||||
async function createPostinstallScript() {
|
||||
console.log("Creating postinstall script...")
|
||||
|
||||
const postinstallScript = `#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
// Detect current platform and architecture
|
||||
function getPlatformInfo() {
|
||||
const platform = os.platform();
|
||||
const arch = os.arch();
|
||||
|
||||
// Map Node.js arch names to Go arch names
|
||||
let goArch = arch;
|
||||
if (arch === 'x64') {
|
||||
goArch = 'amd64';
|
||||
}
|
||||
|
||||
let goPlatform = platform;
|
||||
|
||||
return { platform: goPlatform, arch: goArch };
|
||||
}
|
||||
|
||||
// Setup platform-specific binaries
|
||||
function setupBinaries() {
|
||||
const { platform, arch } = getPlatformInfo();
|
||||
const platformSuffix = \`\${platform}-\${arch}\`;
|
||||
|
||||
console.log(\`Setting up Cline CLI for \${platformSuffix}...\`);
|
||||
|
||||
const binDir = path.join(__dirname, 'bin');
|
||||
|
||||
// Check if platform-specific binaries exist
|
||||
const clineSource = path.join(binDir, \`cline-\${platformSuffix}\`);
|
||||
const clineHostSource = path.join(binDir, \`cline-host-\${platformSuffix}\`);
|
||||
|
||||
if (!fs.existsSync(clineSource)) {
|
||||
console.error(\`Error: Binary not found for platform \${platformSuffix}\`);
|
||||
console.error(\`Expected: \${clineSource}\`);
|
||||
console.error(\`Supported platforms: darwin-arm64, darwin-amd64, linux-amd64, linux-arm64\`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(clineHostSource)) {
|
||||
console.error(\`Error: Binary not found for platform \${platformSuffix}\`);
|
||||
console.error(\`Expected: \${clineHostSource}\`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Create symlinks or copies to the generic names
|
||||
const clineTarget = path.join(binDir, 'cline');
|
||||
const clineHostTarget = path.join(binDir, 'cline-host');
|
||||
|
||||
// Remove existing files if they exist
|
||||
[clineTarget, clineHostTarget].forEach(target => {
|
||||
if (fs.existsSync(target)) {
|
||||
try {
|
||||
fs.unlinkSync(target);
|
||||
} catch (e) {
|
||||
console.warn(\`Warning: Could not remove existing file \${target}: \${e.message}\`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// On Unix, create symlinks; on Windows, copy files
|
||||
if (platform === 'win32') {
|
||||
// Windows: copy files
|
||||
fs.copyFileSync(clineSource, clineTarget);
|
||||
fs.copyFileSync(clineHostSource, clineHostTarget);
|
||||
console.log('✓ Copied platform-specific binaries');
|
||||
} else {
|
||||
// Unix: create symlinks
|
||||
fs.symlinkSync(path.basename(clineSource), clineTarget);
|
||||
fs.symlinkSync(path.basename(clineHostSource), clineHostTarget);
|
||||
console.log('✓ Created symlinks to platform-specific binaries');
|
||||
|
||||
// Make binaries executable
|
||||
try {
|
||||
fs.chmodSync(clineSource, 0o755);
|
||||
fs.chmodSync(clineHostSource, 0o755);
|
||||
fs.chmodSync(clineTarget, 0o755);
|
||||
fs.chmodSync(clineHostTarget, 0o755);
|
||||
} catch (error) {
|
||||
console.warn(\`Warning: Could not set executable permissions: \${error.message}\`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check ripgrep binary
|
||||
const rgBinary = platform === 'win32' ? 'rg.exe' : 'rg';
|
||||
const rgPath = path.join(__dirname, rgBinary);
|
||||
|
||||
if (!fs.existsSync(rgPath)) {
|
||||
console.error(\`Error: ripgrep binary not found at \${rgPath}\`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Make ripgrep executable (Unix only)
|
||||
if (platform !== 'win32') {
|
||||
try {
|
||||
fs.chmodSync(rgPath, 0o755);
|
||||
} catch (error) {
|
||||
console.warn(\`Warning: Could not set ripgrep executable permissions: \${error.message}\`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✓ Cline CLI installation complete');
|
||||
console.log('');
|
||||
console.log('Usage:');
|
||||
console.log(' cline - Start Cline CLI');
|
||||
console.log(' cline-host - Start Cline host service');
|
||||
console.log('');
|
||||
console.log('Documentation: https://docs.cline.bot');
|
||||
}
|
||||
|
||||
try {
|
||||
setupBinaries();
|
||||
} catch (error) {
|
||||
console.error(\`Installation failed: \${error.message}\`);
|
||||
console.error('Please report this issue at: https://github.com/cline/cline/issues');
|
||||
process.exit(1);
|
||||
}
|
||||
`
|
||||
|
||||
const postinstallPath = path.join(BUILD_DIR, "postinstall.js")
|
||||
fs.writeFileSync(postinstallPath, postinstallScript)
|
||||
fs.chmodSync(postinstallPath, 0o755)
|
||||
|
||||
console.log(`✓ postinstall.js created`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads prebuilt binaries for each platform for the modules that include binaries. It uses `npx prebuild-install`
|
||||
* to download the binary.
|
||||
|
||||
@@ -63,7 +63,7 @@ echo ""
|
||||
|
||||
# Test 4: Check for platform support
|
||||
echo "Test 4: Platform Support Check"
|
||||
platforms=("darwin-x64" "darwin-arm64" "linux-x64")
|
||||
platforms=("darwin-x64" "darwin-arm64" "linux-x64" "win-amd64")
|
||||
for platform in "${platforms[@]}"; do
|
||||
if grep -q "$platform" scripts/install.sh; then
|
||||
echo " ✅ PASS: Platform '$platform' supported"
|
||||
|
||||
Reference in New Issue
Block a user