@caretive/caret-cli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.npmrc.tmp +2 -0
- package/README.md +72 -0
- package/cmd/cline/main.go +348 -0
- package/cmd/cline-host/main.go +71 -0
- package/e2e/default_update_test.go +154 -0
- package/e2e/helpers_test.go +378 -0
- package/e2e/main_test.go +47 -0
- package/e2e/mixed_stress_test.go +120 -0
- package/e2e/sqlite_helper.go +161 -0
- package/e2e/start_list_test.go +178 -0
- package/go.mod +64 -0
- package/go.sum +162 -0
- package/man/cline.1 +331 -0
- package/man/cline.1.md +332 -0
- package/package.json +54 -0
- package/pkg/cli/auth/auth_cline_provider.go +285 -0
- package/pkg/cli/auth/auth_menu.go +323 -0
- package/pkg/cli/auth/auth_subscription.go +130 -0
- package/pkg/cli/auth/byo_quick_setup.go +247 -0
- package/pkg/cli/auth/models_cline.go +141 -0
- package/pkg/cli/auth/models_list_fetch.go +156 -0
- package/pkg/cli/auth/models_list_static.go +69 -0
- package/pkg/cli/auth/providers_byo.go +184 -0
- package/pkg/cli/auth/providers_list.go +517 -0
- package/pkg/cli/auth/update_api_configurations.go +647 -0
- package/pkg/cli/auth/wizard_byo.go +764 -0
- package/pkg/cli/auth/wizard_byo_bedrock.go +193 -0
- package/pkg/cli/auth/wizard_byo_oca.go +366 -0
- package/pkg/cli/auth.go +43 -0
- package/pkg/cli/clerror/cline_error.go +187 -0
- package/pkg/cli/config/manager.go +208 -0
- package/pkg/cli/config/settings_renderer.go +198 -0
- package/pkg/cli/config.go +152 -0
- package/pkg/cli/display/ansi.go +27 -0
- package/pkg/cli/display/banner.go +211 -0
- package/pkg/cli/display/deduplicator.go +95 -0
- package/pkg/cli/display/markdown_renderer.go +139 -0
- package/pkg/cli/display/renderer.go +304 -0
- package/pkg/cli/display/segment_streamer.go +212 -0
- package/pkg/cli/display/streaming.go +134 -0
- package/pkg/cli/display/system_renderer.go +269 -0
- package/pkg/cli/display/tool_renderer.go +455 -0
- package/pkg/cli/display/tool_result_parser.go +371 -0
- package/pkg/cli/display/typewriter.go +210 -0
- package/pkg/cli/doctor.go +65 -0
- package/pkg/cli/global/cline-clients.go +501 -0
- package/pkg/cli/global/global.go +113 -0
- package/pkg/cli/global/registry.go +304 -0
- package/pkg/cli/handlers/ask_handlers.go +339 -0
- package/pkg/cli/handlers/handler.go +130 -0
- package/pkg/cli/handlers/say_handlers.go +521 -0
- package/pkg/cli/instances.go +506 -0
- package/pkg/cli/logs.go +382 -0
- package/pkg/cli/output/coordinator.go +167 -0
- package/pkg/cli/output/input_model.go +497 -0
- package/pkg/cli/sqlite/locks.go +366 -0
- package/pkg/cli/task/history_handler.go +72 -0
- package/pkg/cli/task/input_handler.go +577 -0
- package/pkg/cli/task/manager.go +1283 -0
- package/pkg/cli/task/settings_parser.go +754 -0
- package/pkg/cli/task/stream_coordinator.go +60 -0
- package/pkg/cli/task.go +675 -0
- package/pkg/cli/terminal/keyboard.go +695 -0
- package/pkg/cli/tui/HELP_WANTED.md +1 -0
- package/pkg/cli/types/history.go +17 -0
- package/pkg/cli/types/messages.go +329 -0
- package/pkg/cli/types/state.go +59 -0
- package/pkg/cli/updater/updater.go +409 -0
- package/pkg/cli/version.go +43 -0
- package/pkg/common/constants.go +6 -0
- package/pkg/common/schema.go +54 -0
- package/pkg/common/types.go +54 -0
- package/pkg/common/utils.go +185 -0
- package/pkg/generated/field_overrides.go +39 -0
- package/pkg/generated/providers.go +1584 -0
- package/pkg/hostbridge/diff.go +351 -0
- package/pkg/hostbridge/disabled/watch.go +39 -0
- package/pkg/hostbridge/disabled/window.go +63 -0
- package/pkg/hostbridge/disabled/workspace.go +66 -0
- package/pkg/hostbridge/env.go +166 -0
- package/pkg/hostbridge/grpc_server.go +113 -0
- package/pkg/hostbridge/simple.go +43 -0
- package/pkg/hostbridge/simple_workspace.go +85 -0
- package/pkg/hostbridge/window.go +129 -0
- package/scripts/publish-caret-cli.sh +39 -0
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
package global
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"os"
|
|
7
|
+
"os/exec"
|
|
8
|
+
"path"
|
|
9
|
+
"path/filepath"
|
|
10
|
+
"syscall"
|
|
11
|
+
"time"
|
|
12
|
+
|
|
13
|
+
"github.com/cline/cli/pkg/common"
|
|
14
|
+
"github.com/cline/grpc-go/cline"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
// ClineClients manages Cline instances using the new registry system
|
|
18
|
+
type ClineClients struct {
|
|
19
|
+
registry *ClientRegistry
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// NewClineClients creates a new ClineClients instance
|
|
23
|
+
func NewClineClients(configPath string) *ClineClients {
|
|
24
|
+
registry := NewClientRegistry(configPath)
|
|
25
|
+
return &ClineClients{
|
|
26
|
+
registry: registry,
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Initialize performs cleanup of stale instances
|
|
31
|
+
func (c *ClineClients) Initialize(ctx context.Context) error {
|
|
32
|
+
// Clean up stale entries (direct SQLite operations)
|
|
33
|
+
_ = c.registry.CleanupStaleInstances(ctx)
|
|
34
|
+
|
|
35
|
+
return nil
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// StartNewInstance starts a new Cline instance and waits for cline-core to self-register
|
|
39
|
+
func (c *ClineClients) StartNewInstance(ctx context.Context) (*common.CoreInstanceInfo, error) {
|
|
40
|
+
// Find available ports
|
|
41
|
+
corePort, hostPort, err := common.FindAvailablePortPair()
|
|
42
|
+
if err != nil {
|
|
43
|
+
return nil, fmt.Errorf("failed to find available ports: %w", err)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if Config.Verbose {
|
|
47
|
+
fmt.Printf("Starting new Cline instance on ports %d (core) and %d (host bridge)\n", corePort, hostPort)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Start cline-host first
|
|
51
|
+
hostCmd, err := startClineHost(hostPort, corePort)
|
|
52
|
+
if err != nil {
|
|
53
|
+
return nil, fmt.Errorf("failed to start cline-host: %w", err)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Start cline-core (it will register itself in SQLite locks database)
|
|
57
|
+
coreCmd, err := startClineCore(corePort, hostPort)
|
|
58
|
+
if err != nil {
|
|
59
|
+
// Clean up host process if core fails to start
|
|
60
|
+
if hostCmd != nil && hostCmd.Process != nil {
|
|
61
|
+
hostCmd.Process.Kill()
|
|
62
|
+
}
|
|
63
|
+
return nil, fmt.Errorf("failed to start cline-core: %w", err)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fullAddress := fmt.Sprintf("localhost:%d", corePort)
|
|
67
|
+
if Config.Verbose {
|
|
68
|
+
fmt.Println("Waiting for services to start and self-register in SQLite...")
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Use RetryOperation to wait for instance to be ready
|
|
72
|
+
var instance *common.CoreInstanceInfo
|
|
73
|
+
err = common.RetryOperation(12, 5*time.Second, func() error {
|
|
74
|
+
// Check if instance registered itself in SQLite
|
|
75
|
+
foundInstance, err := c.registry.GetInstance(fullAddress)
|
|
76
|
+
if err != nil || foundInstance == nil {
|
|
77
|
+
return fmt.Errorf("instance not found in registry: %v", err)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Verify instance is healthy
|
|
81
|
+
if !common.IsInstanceHealthy(ctx, fullAddress) {
|
|
82
|
+
return fmt.Errorf("instance is registered but not healthy")
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Success - store the instance for return
|
|
86
|
+
instance = foundInstance
|
|
87
|
+
return nil
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
if err != nil {
|
|
91
|
+
// Clean up both processes on failure
|
|
92
|
+
if coreCmd != nil && coreCmd.Process != nil {
|
|
93
|
+
fmt.Printf("Cleaning up core process (PID: %d)\n", coreCmd.Process.Pid)
|
|
94
|
+
coreCmd.Process.Kill()
|
|
95
|
+
}
|
|
96
|
+
if hostCmd != nil && hostCmd.Process != nil {
|
|
97
|
+
fmt.Printf("Cleaning up host process (PID: %d)\n", hostCmd.Process.Pid)
|
|
98
|
+
hostCmd.Process.Kill()
|
|
99
|
+
}
|
|
100
|
+
return nil, fmt.Errorf("failed to start instance: %w", err)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if Config.Verbose {
|
|
104
|
+
fmt.Println("Services started and registered successfully!")
|
|
105
|
+
fmt.Printf(" Address: %s\n", instance.Address)
|
|
106
|
+
fmt.Printf(" Core Port: %d\n", instance.CorePort())
|
|
107
|
+
fmt.Printf(" Host Bridge Port: %d\n", instance.HostPort())
|
|
108
|
+
fmt.Printf(" Process PID: %d\n", coreCmd.Process.Pid)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// If this is the first instance, set it as default
|
|
112
|
+
instances := c.registry.ListInstances()
|
|
113
|
+
if err := c.registry.EnsureDefaultInstance(instances); err != nil {
|
|
114
|
+
if Config.Verbose {
|
|
115
|
+
fmt.Printf("Warning: Failed to set default instance: %v\n", err)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return instance, nil
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// StartNewInstanceAtPort starts a new Cline instance at the specified port and waits for self-registration
|
|
123
|
+
func (c *ClineClients) StartNewInstanceAtPort(ctx context.Context, corePort int) (*common.CoreInstanceInfo, error) {
|
|
124
|
+
// Find available host port (core port + 1000)
|
|
125
|
+
hostPort := corePort + 1000
|
|
126
|
+
coreAddress := fmt.Sprintf("localhost:%d", corePort)
|
|
127
|
+
|
|
128
|
+
// Check if the specified core port is available
|
|
129
|
+
if common.IsInstanceHealthy(ctx, coreAddress) {
|
|
130
|
+
return nil, fmt.Errorf("port %d is already in use by another Cline instance", corePort)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if Config.Verbose {
|
|
134
|
+
fmt.Printf("Starting new Cline instance on ports %d (core) and %d (host bridge)\n", corePort, hostPort)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Start cline-host first
|
|
138
|
+
hostCmd, err := startClineHost(hostPort, corePort)
|
|
139
|
+
if err != nil {
|
|
140
|
+
return nil, fmt.Errorf("failed to start cline-host: %w", err)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Start cline-core (it will register itself in SQLite locks database)
|
|
144
|
+
coreCmd, err := startClineCore(corePort, hostPort)
|
|
145
|
+
if err != nil {
|
|
146
|
+
// Clean up host process if core fails to start
|
|
147
|
+
if hostCmd != nil && hostCmd.Process != nil {
|
|
148
|
+
hostCmd.Process.Kill()
|
|
149
|
+
}
|
|
150
|
+
return nil, fmt.Errorf("failed to start cline-core: %w", err)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
fullAddress := fmt.Sprintf("localhost:%d", corePort)
|
|
154
|
+
if Config.Verbose {
|
|
155
|
+
fmt.Println("Waiting for services to start and self-register in SQLite...")
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Use RetryOperation to wait for instance to be ready
|
|
159
|
+
var instance *common.CoreInstanceInfo
|
|
160
|
+
err = common.RetryOperation(12, 5*time.Second, func() error {
|
|
161
|
+
// Check if instance registered itself in SQLite
|
|
162
|
+
foundInstance, err := c.registry.GetInstance(fullAddress)
|
|
163
|
+
if err != nil || foundInstance == nil {
|
|
164
|
+
return fmt.Errorf("instance not found in registry: %v", err)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Verify instance is healthy
|
|
168
|
+
if !common.IsInstanceHealthy(ctx, fullAddress) {
|
|
169
|
+
return fmt.Errorf("instance is registered but not healthy")
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Success - store the instance for return
|
|
173
|
+
instance = foundInstance
|
|
174
|
+
return nil
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
if err != nil {
|
|
178
|
+
// Clean up both processes on failure
|
|
179
|
+
if coreCmd != nil && coreCmd.Process != nil {
|
|
180
|
+
fmt.Printf("Cleaning up core process (PID: %d)\n", coreCmd.Process.Pid)
|
|
181
|
+
coreCmd.Process.Kill()
|
|
182
|
+
}
|
|
183
|
+
if hostCmd != nil && hostCmd.Process != nil {
|
|
184
|
+
fmt.Printf("Cleaning up host process (PID: %d)\n", hostCmd.Process.Pid)
|
|
185
|
+
hostCmd.Process.Kill()
|
|
186
|
+
}
|
|
187
|
+
return nil, fmt.Errorf("failed to start instance at port %d: %w", corePort, err)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if Config.Verbose {
|
|
191
|
+
fmt.Println("Services started and registered successfully!")
|
|
192
|
+
fmt.Printf(" Address: %s\n", instance.Address)
|
|
193
|
+
fmt.Printf(" Core Port: %d\n", instance.CorePort())
|
|
194
|
+
fmt.Printf(" Host Bridge Port: %d\n", instance.HostPort())
|
|
195
|
+
fmt.Printf(" Process PID: %d\n", coreCmd.Process.Pid)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// If this is the first instance, set it as default
|
|
199
|
+
instances := c.registry.ListInstances()
|
|
200
|
+
if err := c.registry.EnsureDefaultInstance(instances); err != nil {
|
|
201
|
+
if Config.Verbose {
|
|
202
|
+
fmt.Printf("Warning: Failed to set default instance: %v\n", err)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return instance, nil
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// GetRegistry returns the client registry
|
|
210
|
+
func (c *ClineClients) GetRegistry() *ClientRegistry {
|
|
211
|
+
return c.registry
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// EnsureInstanceAtAddress ensures an instance exists at the given address, starting one if needed
|
|
215
|
+
func (c *ClineClients) EnsureInstanceAtAddress(ctx context.Context, address string) error {
|
|
216
|
+
// Expect host:port everywhere
|
|
217
|
+
normalized := address
|
|
218
|
+
if normalized == "" {
|
|
219
|
+
normalized = fmt.Sprintf("localhost:%d", common.DEFAULT_CLINE_CORE_PORT)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Check if instance already exists at this address
|
|
223
|
+
if c.registry.HasInstanceAtAddress(normalized) {
|
|
224
|
+
return nil
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Parse host:port
|
|
228
|
+
host, port, err := common.ParseHostPort(normalized)
|
|
229
|
+
if err != nil {
|
|
230
|
+
return fmt.Errorf("invalid address format %s", address)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Use IPv6-compatible localhost detection
|
|
234
|
+
if common.IsLocalAddress(host) {
|
|
235
|
+
_, err := c.StartNewInstanceAtPort(ctx, port)
|
|
236
|
+
if err != nil {
|
|
237
|
+
return fmt.Errorf("failed to start new instance at %s: %w", normalized, err)
|
|
238
|
+
}
|
|
239
|
+
return nil
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return fmt.Errorf("cannot start remote instance at %s", normalized)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
func startClineHost(hostPort, corePort int) (*exec.Cmd, error) {
|
|
246
|
+
if Config.Verbose {
|
|
247
|
+
fmt.Printf("Starting cline-host on port %d\n", hostPort)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Get the directory where the cline binary is located
|
|
251
|
+
execPath, err := os.Executable()
|
|
252
|
+
if err != nil {
|
|
253
|
+
return nil, fmt.Errorf("failed to get executable path: %w", err)
|
|
254
|
+
}
|
|
255
|
+
binDir := path.Dir(execPath)
|
|
256
|
+
clineHostPath := path.Join(binDir, "cline-host")
|
|
257
|
+
|
|
258
|
+
// Start the cline-host process
|
|
259
|
+
cmd := exec.Command(clineHostPath,
|
|
260
|
+
"--verbose",
|
|
261
|
+
"--port", fmt.Sprintf("%d", hostPort))
|
|
262
|
+
|
|
263
|
+
// Create logs directory in ~/.cline/logs
|
|
264
|
+
logsDir := path.Join(Config.ConfigPath, "logs")
|
|
265
|
+
if err := os.MkdirAll(logsDir, 0755); err != nil {
|
|
266
|
+
return nil, fmt.Errorf("failed to create logs directory: %w", err)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Create timestamped log file
|
|
270
|
+
timestamp := time.Now().Format("2006-01-02-15-04-05")
|
|
271
|
+
logFileName := fmt.Sprintf("cline-host-%s-localhost-%d.log", timestamp, hostPort)
|
|
272
|
+
logFilePath := path.Join(logsDir, logFileName)
|
|
273
|
+
logFile, err := os.Create(logFilePath)
|
|
274
|
+
if err != nil {
|
|
275
|
+
return nil, fmt.Errorf("failed to create log file: %w", err)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Redirect stdout and stderr to log file
|
|
279
|
+
cmd.Stdout = logFile
|
|
280
|
+
cmd.Stderr = logFile
|
|
281
|
+
|
|
282
|
+
// Put the child process in a new process group so Ctrl+C doesn't kill it
|
|
283
|
+
cmd.SysProcAttr = &syscall.SysProcAttr{
|
|
284
|
+
Setpgid: true,
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if err := cmd.Start(); err != nil {
|
|
288
|
+
logFile.Close()
|
|
289
|
+
return nil, fmt.Errorf("failed to start cline-host: %w", err)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if Config.Verbose {
|
|
293
|
+
fmt.Printf("Started cline-host (PID: %d)\n", cmd.Process.Pid)
|
|
294
|
+
fmt.Printf("Logging cline-host output to: %s\n", logFilePath)
|
|
295
|
+
}
|
|
296
|
+
return cmd, nil
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// KillInstanceByAddress kills a Cline instance by its address
|
|
300
|
+
func KillInstanceByAddress(ctx context.Context, registry *ClientRegistry, address string) error {
|
|
301
|
+
// Check if the instance exists in the registry
|
|
302
|
+
_, err := registry.GetInstance(address)
|
|
303
|
+
if err != nil {
|
|
304
|
+
return fmt.Errorf("instance %s not found in registry", address)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if Config.Verbose {
|
|
308
|
+
fmt.Printf("Killing instance: %s\n", address)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Get gRPC client and process info
|
|
312
|
+
client, err := registry.GetClient(ctx, address)
|
|
313
|
+
if err != nil {
|
|
314
|
+
return fmt.Errorf("failed to connect to instance %s: %w", address, err)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
processInfo, err := client.State.GetProcessInfo(ctx, &cline.EmptyRequest{})
|
|
318
|
+
if err != nil {
|
|
319
|
+
return fmt.Errorf("failed to get process info for instance %s: %w", address, err)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
pid := int(processInfo.ProcessId)
|
|
323
|
+
if Config.Verbose {
|
|
324
|
+
fmt.Printf("Terminating process PID %d...\n", pid)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Kill the process
|
|
328
|
+
if err := syscall.Kill(pid, syscall.SIGTERM); err != nil {
|
|
329
|
+
return fmt.Errorf("failed to kill process %d: %w", pid, err)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Wait for the instance to remove itself from registry
|
|
333
|
+
if Config.Verbose {
|
|
334
|
+
fmt.Printf("Waiting for instance to clean up registry entry...\n")
|
|
335
|
+
}
|
|
336
|
+
for i := 0; i < 5; i++ {
|
|
337
|
+
time.Sleep(1 * time.Second)
|
|
338
|
+
if !registry.HasInstanceAtAddress(address) {
|
|
339
|
+
if Config.Verbose {
|
|
340
|
+
fmt.Printf("Instance %s successfully killed and removed from registry.\n", address)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Update default instance if needed
|
|
344
|
+
instances, err := registry.ListInstancesCleaned(ctx)
|
|
345
|
+
if err == nil && len(instances) > 0 {
|
|
346
|
+
// ensureDefaultInstance logic will handle setting a new default
|
|
347
|
+
defaultInstance := registry.GetDefaultInstance()
|
|
348
|
+
if defaultInstance == address || defaultInstance == "" {
|
|
349
|
+
if len(instances) > 0 {
|
|
350
|
+
if err := registry.SetDefaultInstance(instances[0].Address); err == nil {
|
|
351
|
+
if Config.Verbose {
|
|
352
|
+
fmt.Printf("Updated default instance to: %s\n", instances[0].Address)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return nil
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return fmt.Errorf("instance killed but failed to remove itself from registry within 5 seconds")
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
func startClineCore(corePort, hostPort int) (*exec.Cmd, error) {
|
|
367
|
+
if Config.Verbose {
|
|
368
|
+
fmt.Printf("Starting cline-core on port %d (with hostbridge on %d)\n", corePort, hostPort)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Get the executable path and resolve symlinks (for npm global installs)
|
|
372
|
+
execPath, err := os.Executable()
|
|
373
|
+
if err != nil {
|
|
374
|
+
return nil, fmt.Errorf("failed to get executable path: %w", err)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Resolve symlinks to get the real path
|
|
378
|
+
// For npm global installs, execPath might be a symlink like:
|
|
379
|
+
// /opt/homebrew/bin/cline -> /opt/homebrew/lib/node_modules/cline/bin/cline
|
|
380
|
+
realPath, err := filepath.EvalSymlinks(execPath)
|
|
381
|
+
if err != nil {
|
|
382
|
+
// If we can't resolve symlinks, fall back to the original path
|
|
383
|
+
realPath = execPath
|
|
384
|
+
if Config.Verbose {
|
|
385
|
+
fmt.Printf("Warning: Could not resolve symlinks for %s: %v\n", execPath, err)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
binDir := path.Dir(realPath)
|
|
390
|
+
installDir := path.Dir(binDir)
|
|
391
|
+
clineCorePath := path.Join(installDir, "cline-core.js")
|
|
392
|
+
|
|
393
|
+
if Config.Verbose {
|
|
394
|
+
fmt.Printf("Executable path: %s\n", execPath)
|
|
395
|
+
if realPath != execPath {
|
|
396
|
+
fmt.Printf("Real path (after resolving symlinks): %s\n", realPath)
|
|
397
|
+
}
|
|
398
|
+
fmt.Printf("Bin directory: %s\n", binDir)
|
|
399
|
+
fmt.Printf("Install directory: %s\n", installDir)
|
|
400
|
+
fmt.Printf("Looking for cline-core.js at: %s\n", clineCorePath)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Check if cline-core.js exists at the primary location
|
|
404
|
+
var finalClineCorePath string
|
|
405
|
+
var finalInstallDir string
|
|
406
|
+
if _, err := os.Stat(clineCorePath); os.IsNotExist(err) {
|
|
407
|
+
// Development mode: Try ../../dist-standalone/cline-core.js
|
|
408
|
+
// This handles the case where we're running from cli/bin/cline
|
|
409
|
+
devClineCorePath := path.Join(binDir, "..", "..", "dist-standalone", "cline-core.js")
|
|
410
|
+
devInstallDir := path.Join(binDir, "..", "..", "dist-standalone")
|
|
411
|
+
|
|
412
|
+
if Config.Verbose {
|
|
413
|
+
fmt.Printf("Primary location not found, trying development path: %s\n", devClineCorePath)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if _, err := os.Stat(devClineCorePath); os.IsNotExist(err) {
|
|
417
|
+
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)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
finalClineCorePath = devClineCorePath
|
|
421
|
+
finalInstallDir = devInstallDir
|
|
422
|
+
if Config.Verbose {
|
|
423
|
+
fmt.Printf("Using development mode: cline-core.js found at %s\n", finalClineCorePath)
|
|
424
|
+
}
|
|
425
|
+
} else {
|
|
426
|
+
finalClineCorePath = clineCorePath
|
|
427
|
+
finalInstallDir = installDir
|
|
428
|
+
if Config.Verbose {
|
|
429
|
+
fmt.Printf("Using production mode: cline-core.js found at %s\n", finalClineCorePath)
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Create logs directory in ~/.cline/logs
|
|
434
|
+
logsDir := path.Join(Config.ConfigPath, "logs")
|
|
435
|
+
if err := os.MkdirAll(logsDir, 0755); err != nil {
|
|
436
|
+
return nil, fmt.Errorf("failed to create logs directory: %w", err)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Create timestamped log file
|
|
440
|
+
timestamp := time.Now().Format("2006-01-02-15-04-05")
|
|
441
|
+
logFileName := fmt.Sprintf("cline-core-%s-localhost-%d.log", timestamp, corePort)
|
|
442
|
+
logFilePath := path.Join(logsDir, logFileName)
|
|
443
|
+
logFile, err := os.Create(logFilePath)
|
|
444
|
+
if err != nil {
|
|
445
|
+
return nil, fmt.Errorf("failed to create log file: %w", err)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Start the cline-core process with --config flag using system node
|
|
449
|
+
args := []string{finalClineCorePath,
|
|
450
|
+
"--port", fmt.Sprintf("%d", corePort),
|
|
451
|
+
"--host-bridge-port", fmt.Sprintf("%d", hostPort),
|
|
452
|
+
"--config", Config.ConfigPath}
|
|
453
|
+
|
|
454
|
+
if Config.Verbose {
|
|
455
|
+
fmt.Printf("Using system node\n")
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
cmd := exec.Command("node", args...)
|
|
459
|
+
|
|
460
|
+
// Set working directory to installation root
|
|
461
|
+
cmd.Dir = finalInstallDir
|
|
462
|
+
|
|
463
|
+
// Redirect stdout and stderr to log file
|
|
464
|
+
cmd.Stdout = logFile
|
|
465
|
+
cmd.Stderr = logFile
|
|
466
|
+
|
|
467
|
+
// Put the child process in a new process group so Ctrl+C doesn't kill it
|
|
468
|
+
cmd.SysProcAttr = &syscall.SysProcAttr{
|
|
469
|
+
Setpgid: true,
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Set environment variables with NODE_PATH for both real and fake node_modules
|
|
473
|
+
// The fake node_modules contains the vscode stub that can't be in the real node_modules
|
|
474
|
+
env := os.Environ()
|
|
475
|
+
realNodeModules := path.Join(finalInstallDir, "node_modules")
|
|
476
|
+
fakeNodeModules := path.Join(finalInstallDir, "fake_node_modules")
|
|
477
|
+
nodePath := fmt.Sprintf("%s%c%s", realNodeModules, os.PathListSeparator, fakeNodeModules)
|
|
478
|
+
|
|
479
|
+
env = append(env,
|
|
480
|
+
fmt.Sprintf("NODE_PATH=%s", nodePath),
|
|
481
|
+
"GRPC_TRACE=all",
|
|
482
|
+
"GRPC_VERBOSITY=DEBUG",
|
|
483
|
+
"NODE_ENV=development",
|
|
484
|
+
)
|
|
485
|
+
cmd.Env = env
|
|
486
|
+
|
|
487
|
+
if Config.Verbose {
|
|
488
|
+
fmt.Printf("NODE_PATH set to: %s\n", nodePath)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if err := cmd.Start(); err != nil {
|
|
492
|
+
logFile.Close()
|
|
493
|
+
return nil, fmt.Errorf("failed to start cline-core: %w", err)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if Config.Verbose {
|
|
497
|
+
fmt.Printf("Started cline-core (PID: %d)\n", cmd.Process.Pid)
|
|
498
|
+
fmt.Printf("Logging cline-core output to: %s\n", logFilePath)
|
|
499
|
+
}
|
|
500
|
+
return cmd, nil
|
|
501
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
package global
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"os"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
|
|
9
|
+
"github.com/charmbracelet/lipgloss"
|
|
10
|
+
"github.com/cline/cli/pkg/common"
|
|
11
|
+
"github.com/cline/grpc-go/client"
|
|
12
|
+
"github.com/muesli/termenv"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
type Port uint16
|
|
16
|
+
|
|
17
|
+
type GlobalConfig struct {
|
|
18
|
+
ConfigPath string
|
|
19
|
+
Verbose bool
|
|
20
|
+
OutputFormat string
|
|
21
|
+
CoreAddress string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
var (
|
|
25
|
+
Config *GlobalConfig
|
|
26
|
+
Clients *ClineClients
|
|
27
|
+
|
|
28
|
+
// Version info - set at build time via ldflags
|
|
29
|
+
// Version is the Cline Core version (from root package.json)
|
|
30
|
+
Version = "dev"
|
|
31
|
+
// CliVersion is the CLI package version (from cli/package.json)
|
|
32
|
+
CliVersion = "dev"
|
|
33
|
+
Commit = "unknown"
|
|
34
|
+
Date = "unknown"
|
|
35
|
+
BuiltBy = "unknown"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
func InitializeGlobalConfig(cfg *GlobalConfig) error {
|
|
39
|
+
if cfg.ConfigPath == "" {
|
|
40
|
+
homeDir, err := os.UserHomeDir()
|
|
41
|
+
if err != nil {
|
|
42
|
+
return fmt.Errorf("failed to get home directory: %w", err)
|
|
43
|
+
}
|
|
44
|
+
cfg.ConfigPath = filepath.Join(homeDir, ".cline")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Ensure .cline directory exists
|
|
48
|
+
if err := os.MkdirAll(cfg.ConfigPath, 0755); err != nil {
|
|
49
|
+
return fmt.Errorf("failed to create config directory: %w", err)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Configure lipgloss color profile based on output format
|
|
53
|
+
if cfg.OutputFormat == "plain" {
|
|
54
|
+
lipgloss.SetColorProfile(termenv.Ascii) // NO COLOR mode
|
|
55
|
+
}
|
|
56
|
+
// Otherwise lipgloss auto-detects terminal capabilities (default behavior)
|
|
57
|
+
|
|
58
|
+
Config = cfg
|
|
59
|
+
Clients = NewClineClients(cfg.ConfigPath)
|
|
60
|
+
|
|
61
|
+
// Initialize the clients registry
|
|
62
|
+
ctx := context.Background()
|
|
63
|
+
if err := Clients.Initialize(ctx); err != nil {
|
|
64
|
+
return fmt.Errorf("failed to initialize clients: %w", err)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return nil
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// GetDefaultClient returns a client for the default instance or the address override
|
|
71
|
+
func GetDefaultClient(ctx context.Context) (*client.ClineClient, error) {
|
|
72
|
+
if Config.CoreAddress != "" && Config.CoreAddress != fmt.Sprintf("localhost:%d", common.DEFAULT_CLINE_CORE_PORT) {
|
|
73
|
+
// User specified a specific address, use that
|
|
74
|
+
return Clients.GetRegistry().GetClient(ctx, Config.CoreAddress)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Use the default instance from registry
|
|
78
|
+
return Clients.GetRegistry().GetDefaultClient(ctx)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// GetClientForAddress returns a client for a specific address
|
|
82
|
+
func GetClientForAddress(ctx context.Context, address string) (*client.ClineClient, error) {
|
|
83
|
+
return Clients.GetRegistry().GetClient(ctx, address)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// EnsureDefaultInstance ensures a default instance exists
|
|
87
|
+
func EnsureDefaultInstance(ctx context.Context) error {
|
|
88
|
+
if Clients == nil {
|
|
89
|
+
return fmt.Errorf("global clients not initialized")
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
registry := Clients.GetRegistry()
|
|
93
|
+
|
|
94
|
+
// First, check if there are any instances already registered in SQLite
|
|
95
|
+
instances := registry.ListInstances()
|
|
96
|
+
|
|
97
|
+
// Use the registry's EnsureDefaultInstance to auto-set first instance as default if needed
|
|
98
|
+
if err := registry.EnsureDefaultInstance(instances); err != nil {
|
|
99
|
+
return fmt.Errorf("failed to ensure default from existing instances: %w", err)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Now check if we have a default set
|
|
103
|
+
if registry.GetDefaultInstance() == "" {
|
|
104
|
+
// No instances exist, start a new one
|
|
105
|
+
// Note: StartNewInstance will automatically set it as default since it's the first instance
|
|
106
|
+
_, err := Clients.StartNewInstance(ctx)
|
|
107
|
+
if err != nil {
|
|
108
|
+
return fmt.Errorf("failed to start new default instance: %w", err)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return nil
|
|
113
|
+
}
|