@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.
Files changed (85) hide show
  1. package/.npmrc.tmp +2 -0
  2. package/README.md +72 -0
  3. package/cmd/cline/main.go +348 -0
  4. package/cmd/cline-host/main.go +71 -0
  5. package/e2e/default_update_test.go +154 -0
  6. package/e2e/helpers_test.go +378 -0
  7. package/e2e/main_test.go +47 -0
  8. package/e2e/mixed_stress_test.go +120 -0
  9. package/e2e/sqlite_helper.go +161 -0
  10. package/e2e/start_list_test.go +178 -0
  11. package/go.mod +64 -0
  12. package/go.sum +162 -0
  13. package/man/cline.1 +331 -0
  14. package/man/cline.1.md +332 -0
  15. package/package.json +54 -0
  16. package/pkg/cli/auth/auth_cline_provider.go +285 -0
  17. package/pkg/cli/auth/auth_menu.go +323 -0
  18. package/pkg/cli/auth/auth_subscription.go +130 -0
  19. package/pkg/cli/auth/byo_quick_setup.go +247 -0
  20. package/pkg/cli/auth/models_cline.go +141 -0
  21. package/pkg/cli/auth/models_list_fetch.go +156 -0
  22. package/pkg/cli/auth/models_list_static.go +69 -0
  23. package/pkg/cli/auth/providers_byo.go +184 -0
  24. package/pkg/cli/auth/providers_list.go +517 -0
  25. package/pkg/cli/auth/update_api_configurations.go +647 -0
  26. package/pkg/cli/auth/wizard_byo.go +764 -0
  27. package/pkg/cli/auth/wizard_byo_bedrock.go +193 -0
  28. package/pkg/cli/auth/wizard_byo_oca.go +366 -0
  29. package/pkg/cli/auth.go +43 -0
  30. package/pkg/cli/clerror/cline_error.go +187 -0
  31. package/pkg/cli/config/manager.go +208 -0
  32. package/pkg/cli/config/settings_renderer.go +198 -0
  33. package/pkg/cli/config.go +152 -0
  34. package/pkg/cli/display/ansi.go +27 -0
  35. package/pkg/cli/display/banner.go +211 -0
  36. package/pkg/cli/display/deduplicator.go +95 -0
  37. package/pkg/cli/display/markdown_renderer.go +139 -0
  38. package/pkg/cli/display/renderer.go +304 -0
  39. package/pkg/cli/display/segment_streamer.go +212 -0
  40. package/pkg/cli/display/streaming.go +134 -0
  41. package/pkg/cli/display/system_renderer.go +269 -0
  42. package/pkg/cli/display/tool_renderer.go +455 -0
  43. package/pkg/cli/display/tool_result_parser.go +371 -0
  44. package/pkg/cli/display/typewriter.go +210 -0
  45. package/pkg/cli/doctor.go +65 -0
  46. package/pkg/cli/global/cline-clients.go +501 -0
  47. package/pkg/cli/global/global.go +113 -0
  48. package/pkg/cli/global/registry.go +304 -0
  49. package/pkg/cli/handlers/ask_handlers.go +339 -0
  50. package/pkg/cli/handlers/handler.go +130 -0
  51. package/pkg/cli/handlers/say_handlers.go +521 -0
  52. package/pkg/cli/instances.go +506 -0
  53. package/pkg/cli/logs.go +382 -0
  54. package/pkg/cli/output/coordinator.go +167 -0
  55. package/pkg/cli/output/input_model.go +497 -0
  56. package/pkg/cli/sqlite/locks.go +366 -0
  57. package/pkg/cli/task/history_handler.go +72 -0
  58. package/pkg/cli/task/input_handler.go +577 -0
  59. package/pkg/cli/task/manager.go +1283 -0
  60. package/pkg/cli/task/settings_parser.go +754 -0
  61. package/pkg/cli/task/stream_coordinator.go +60 -0
  62. package/pkg/cli/task.go +675 -0
  63. package/pkg/cli/terminal/keyboard.go +695 -0
  64. package/pkg/cli/tui/HELP_WANTED.md +1 -0
  65. package/pkg/cli/types/history.go +17 -0
  66. package/pkg/cli/types/messages.go +329 -0
  67. package/pkg/cli/types/state.go +59 -0
  68. package/pkg/cli/updater/updater.go +409 -0
  69. package/pkg/cli/version.go +43 -0
  70. package/pkg/common/constants.go +6 -0
  71. package/pkg/common/schema.go +54 -0
  72. package/pkg/common/types.go +54 -0
  73. package/pkg/common/utils.go +185 -0
  74. package/pkg/generated/field_overrides.go +39 -0
  75. package/pkg/generated/providers.go +1584 -0
  76. package/pkg/hostbridge/diff.go +351 -0
  77. package/pkg/hostbridge/disabled/watch.go +39 -0
  78. package/pkg/hostbridge/disabled/window.go +63 -0
  79. package/pkg/hostbridge/disabled/workspace.go +66 -0
  80. package/pkg/hostbridge/env.go +166 -0
  81. package/pkg/hostbridge/grpc_server.go +113 -0
  82. package/pkg/hostbridge/simple.go +43 -0
  83. package/pkg/hostbridge/simple_workspace.go +85 -0
  84. package/pkg/hostbridge/window.go +129 -0
  85. 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
+ }