Compare commits

...

22 Commits

Author SHA1 Message Date
Valquaint 570003eaa6 Merge branch 'cli-windows' of github.com:cline/cline into cli-windows 2026-01-19 10:43:37 -08:00
Valquaint d4733706d8 Fix typo in install path 2026-01-19 10:40:38 -08:00
Valquaint a727d5c347 Updated branch to be in line with main. Added new install script, initial testing. 2026-01-19 10:40:38 -08:00
Valquaint ae306adc42 Updated run-unix and w32 to fix pathing. Updated install-local. 2026-01-19 10:40:37 -08:00
Max Paulus 🥪 2f3002d215 fix build-proto escaping 2026-01-19 10:40:37 -08:00
Max Paulus 🥪 41404e1a13 create one large typescript build file 2026-01-19 10:40:34 -08:00
Valquaint d6244efda1 CLI Updates. Added additional platform support. Needs additional debugging. 2026-01-19 10:34:55 -08:00
Valquaint be410c2927 Updated run-unix and w32 to fix pathing. Updated install-local. 2026-01-19 10:30:20 -08:00
Max Paulus 🥪 6c67a0c8fc fix build-proto escaping 2026-01-19 10:29:21 -08:00
Max Paulus 🥪 31e3c082ec create one large typescript build file 2026-01-19 10:29:17 -08:00
Valquaint a75f4a63c8 CLI Updates. Added additional platform support. Needs additional debugging. 2026-01-19 10:23:21 -08:00
Valquaint 0b059cf36f Fix typo in install path 2026-01-19 10:17:52 -08:00
Valquaint 7699c52abf Updated branch to be in line with main. Added new install script, initial testing. 2026-01-13 16:30:47 -08:00
Valquaint 24281d98e1 Merge branch 'cli-windows' of github.com:cline/cline into cli-windows 2026-01-13 16:28:22 -08:00
Valquaint 0af7d0911d Updated run-unix and w32 to fix pathing. Updated install-local. 2026-01-13 16:24:27 -08:00
Max Paulus 🥪 83af73a5f9 fix build-proto escaping 2026-01-13 16:24:27 -08:00
Max Paulus 🥪 9d875e6222 create one large typescript build file 2026-01-13 16:24:07 -08:00
Valquaint 125e2ad01f CLI Updates. Added additional platform support. Needs additional debugging. 2026-01-13 16:22:11 -08:00
Valquaint fd46c97825 Updated run-unix and w32 to fix pathing. Updated install-local. 2026-01-08 10:52:29 -08:00
Max Paulus 🥪 7426513245 fix build-proto escaping 2026-01-06 13:35:24 -08:00
Max Paulus 🥪 f49a856ee4 create one large typescript build file 2026-01-06 13:32:39 -08:00
Valquaint 24568fed0d CLI Updates. Added additional platform support. Needs additional debugging. 2026-01-06 08:11:15 -08:00
20 changed files with 8542 additions and 3886 deletions
+1
View File
@@ -5,6 +5,7 @@ node_modules
tmp
.vscode-test/
*.vsix
/pkg
.DS_Store
.idea
+7 -1
View File
@@ -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
View File
@@ -59,7 +59,8 @@
},
"os": [
"darwin",
"linux"
"linux",
"win32"
],
"cpu": [
"x64",
-272
View File
@@ -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
}
+282
View File
@@ -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
}
+289
View File
@@ -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
}
-23
View File
@@ -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",
+33
View File
@@ -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}
}
+41
View File
@@ -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}
}
+6100 -3551
View File
File diff suppressed because it is too large Load Diff
+19 -3
View File
@@ -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",
+1
View File
@@ -25,6 +25,7 @@ cd cli
# Define target platforms for cross-compilation
PLATFORMS=(
"windows/amd64"
"darwin/arm64"
"darwin/amd64"
"linux/amd64"
+24 -7
View File
@@ -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..."))
+1 -1
View File
@@ -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")
+724
View File
@@ -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
View File
@@ -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
View File
@@ -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
+584
View File
@@ -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()
+384
View File
@@ -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.
+1 -1
View File
@@ -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"