@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
package/.npmrc.tmp ADDED
@@ -0,0 +1,2 @@
1
+ //registry.npmjs.org/:_authToken=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
2
+ @caretive:registry=https://registry.npmjs.org/
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # Caret CLI
2
+
3
+ ```
4
+ /_____/\ /_/\ /_______/\/__/\ /__/\ /_____/\
5
+ \:::__\/ \:\ \ \__.::._\/\::\_\\ \ \\::::_\/_
6
+ \:\ \ __\:\ \ \::\ \ \:. `-\ \ \\:\/___/\
7
+ \:\ \/_/\\:\ \____ _\::\ \__\:. _ \ \\::___\/_
8
+ \:\_\ \ \\:\/___/\/__\::\__/\\. \`-\ \ \\:\____/\
9
+ \_____\/ \_____\/\________\/ \__\/ \__\/ \_____\/
10
+ ```
11
+
12
+ Autonomous coding agent CLI for Caretive - capable of creating/editing files, running commands, using the browser, and more, with Caret mode prompts/branding.
13
+
14
+ ## Installation
15
+
16
+ Install Caret CLI globally using npm:
17
+
18
+ ```bash
19
+ npm install -g @caretive/caret-cli
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ```bash
25
+ caret
26
+ ```
27
+
28
+ This will start the Caret CLI interface where you can interact with the autonomous coding agent using Caret prompts/branding.
29
+
30
+ ## Features
31
+
32
+ - **Autonomous Coding**: AI-powered code generation, editing, and refactoring
33
+ - **File Operations**: Create, read, update, and delete files and directories
34
+ - **Command Execution**: Run shell commands and scripts
35
+ - **Browser Automation**: Interact with web pages and applications
36
+ - **Multi-Model Support**: Works with Anthropic Claude, OpenAI GPT, and other AI models
37
+ - **MCP Integration**: Extensible through Model Context Protocol servers
38
+ - **Project Understanding**: Analyzes codebases to provide context-aware assistance
39
+
40
+ ## Requirements
41
+
42
+ - Node.js 18.0.0 or higher
43
+ - Supported platforms: macOS, Linux. Windows soon
44
+ - Supported architectures: x64, arm64
45
+
46
+ ## Configuration
47
+
48
+ Cline can be configured through:
49
+
50
+ - Environment variables
51
+ - Configuration files
52
+ - Command-line arguments
53
+
54
+ See the [main documentation](https://cline.bot) for detailed configuration options.
55
+
56
+ ## Links
57
+
58
+ - **Website**: [https://cline.bot](https://cline.bot)
59
+ - **Documentation**: [https://docs.cline.bot](https://docs.cline.bot)
60
+ - **GitHub**: [https://github.com/cline/cline](https://github.com/cline/cline)
61
+ - **VSCode Extension**: Available in the VSCode Marketplace
62
+ - **JetBrains Extension**: Available in the JetBrains Marketplace
63
+
64
+ ## License
65
+
66
+ Apache-2.0 - see [LICENSE](https://github.com/cline/cline/blob/main/LICENSE) for details.
67
+
68
+ ## Support
69
+
70
+ - Report issues: [GitHub Issues](https://github.com/cline/cline/issues)
71
+ - Community: [GitHub Discussions](https://github.com/cline/cline/discussions)
72
+ - Documentation: [docs.cline.bot](https://docs.cline.bot)
@@ -0,0 +1,348 @@
1
+ package main
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "fmt"
7
+ "io"
8
+ "os"
9
+ "strings"
10
+
11
+ "github.com/charmbracelet/huh"
12
+ "github.com/charmbracelet/lipgloss"
13
+ "github.com/cline/cli/pkg/cli"
14
+ "github.com/cline/cli/pkg/cli/auth"
15
+ "github.com/cline/cli/pkg/cli/display"
16
+ "github.com/cline/cli/pkg/cli/global"
17
+ "github.com/cline/cli/pkg/common"
18
+ "github.com/cline/grpc-go/cline"
19
+ "github.com/spf13/cobra"
20
+ )
21
+
22
+ var (
23
+ coreAddress string
24
+ verbose bool
25
+ outputFormat string
26
+
27
+ // Task creation flags (for root command)
28
+ images []string
29
+ files []string
30
+ mode string
31
+ settings []string
32
+ yolo bool
33
+ oneshot bool
34
+ )
35
+
36
+ func main() {
37
+ rootCmd := &cobra.Command{
38
+ Use: "caret [prompt]", // CARET MODIFICATION: brand/command name
39
+ Short: "Caret CLI - AI-powered coding assistant", // CARET MODIFICATION
40
+ Long: `A command-line interface for interacting with Caret AI coding assistant. // CARET MODIFICATION
41
+
42
+ Start a new task by providing a prompt:
43
+ caret "Create a new Python script that prints hello world"
44
+
45
+ Or pipe a prompt via stdin:
46
+ echo "Create a todo app" | caret
47
+ cat prompt.txt | caret --yolo
48
+
49
+ Or run with no arguments to enter interactive mode:
50
+ caret
51
+
52
+ This CLI also provides task management, configuration, and monitoring capabilities.
53
+
54
+ For detailed documentation including all commands, options, and examples,
55
+ see the manual page: man caret`, // CARET MODIFICATION
56
+ Args: cobra.ArbitraryArgs,
57
+ PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
58
+ if outputFormat != "rich" && outputFormat != "json" && outputFormat != "plain" {
59
+ return fmt.Errorf("invalid output format '%s': must be one of 'rich', 'json', or 'plain'", outputFormat)
60
+ }
61
+
62
+ return global.InitializeGlobalConfig(&global.GlobalConfig{
63
+ Verbose: verbose,
64
+ OutputFormat: outputFormat,
65
+ CoreAddress: coreAddress,
66
+ })
67
+ },
68
+ RunE: func(cmd *cobra.Command, args []string) error {
69
+ ctx := cmd.Context()
70
+
71
+ var instanceAddress string
72
+
73
+ // If --address flag not provided, start instance BEFORE getting prompt
74
+ if !cmd.Flags().Changed("address") {
75
+ if global.Config.Verbose {
76
+ fmt.Println("Starting new Cline instance...")
77
+ }
78
+ instance, err := global.Clients.StartNewInstance(ctx)
79
+ if err != nil {
80
+ return fmt.Errorf("failed to start new instance: %w", err)
81
+ }
82
+ instanceAddress = instance.Address
83
+ if global.Config.Verbose {
84
+ fmt.Printf("Started instance at %s\n\n", instanceAddress)
85
+ }
86
+
87
+ // Set up cleanup on exit
88
+ defer func() {
89
+ if global.Config.Verbose {
90
+ fmt.Println("\nCleaning up instance...")
91
+ }
92
+ registry := global.Clients.GetRegistry()
93
+ if err := global.KillInstanceByAddress(context.Background(), registry, instanceAddress); err != nil {
94
+ if global.Config.Verbose {
95
+ fmt.Printf("Warning: Failed to clean up instance: %v\n", err)
96
+ }
97
+ }
98
+ }()
99
+
100
+ // Check if user has credentials configured
101
+ if !isUserReadyToUse(ctx, instanceAddress) {
102
+ // Create renderer for welcome messages
103
+ renderer := display.NewRenderer(global.Config.OutputFormat)
104
+ fmt.Printf("\n%s\n\n", renderer.Dim("Hey there! Looks like you're new here. Let's get you set up"))
105
+
106
+ if err := auth.HandleAuthMenuNoArgs(ctx); err != nil {
107
+ // Check if user cancelled - exit cleanly
108
+ if err == huh.ErrUserAborted {
109
+ return nil
110
+ }
111
+ return fmt.Errorf("auth setup failed: %w", err)
112
+ }
113
+
114
+ // Re-check after auth wizard
115
+ if !isUserReadyToUse(ctx, instanceAddress) {
116
+ return fmt.Errorf("credentials still not configured - please run 'cline auth' to complete setup")
117
+ }
118
+
119
+ fmt.Printf("\n%s\n\n", renderer.Dim("✓ Setup complete, you can now use the Cline CLI"))
120
+ }
121
+ } else {
122
+ // User specified --address flag, use that
123
+ instanceAddress = coreAddress
124
+ }
125
+
126
+ // Get content from both args and stdin
127
+ prompt, err := getContentFromStdinAndArgs(args)
128
+ if err != nil {
129
+ return fmt.Errorf("failed to read prompt: %w", err)
130
+ }
131
+
132
+ // If no prompt from args or stdin, show interactive input
133
+ if prompt == "" {
134
+ // Pass the mode flag to banner so it shows correct mode
135
+ prompt, err = promptForInitialTask(ctx, instanceAddress, mode)
136
+ if err != nil {
137
+ // Check if user cancelled - exit cleanly without error
138
+ if err == huh.ErrUserAborted {
139
+ return nil
140
+ }
141
+ return err
142
+ }
143
+ if prompt == "" {
144
+ return fmt.Errorf("prompt required")
145
+ }
146
+ }
147
+
148
+ // If oneshot mode, force plan mode and yolo
149
+ if oneshot {
150
+ mode = "plan"
151
+ yolo = true
152
+ }
153
+
154
+ return cli.CreateAndFollowTask(ctx, prompt, cli.TaskOptions{
155
+ Images: images,
156
+ Files: files,
157
+ Mode: mode,
158
+ Settings: settings,
159
+ Yolo: yolo,
160
+ Address: instanceAddress,
161
+ Verbose: verbose,
162
+ })
163
+ },
164
+ }
165
+
166
+ rootCmd.PersistentFlags().StringVar(&coreAddress, "address", fmt.Sprintf("localhost:%d", common.DEFAULT_CLINE_CORE_PORT), "Cline Core gRPC address")
167
+ rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
168
+ rootCmd.PersistentFlags().StringVarP(&outputFormat, "output-format", "F", "rich", "output format (rich|json|plain)")
169
+
170
+ // Task creation flags (only apply when using root command with prompt)
171
+ rootCmd.Flags().StringSliceVarP(&images, "image", "i", nil, "attach image files")
172
+ rootCmd.Flags().StringSliceVarP(&files, "file", "f", nil, "attach files")
173
+ rootCmd.Flags().StringVarP(&mode, "mode", "m", "plan", "mode (act|plan) - defaults to plan")
174
+ rootCmd.Flags().StringSliceVarP(&settings, "setting", "s", nil, "task settings (key=value format)")
175
+ rootCmd.Flags().BoolVarP(&yolo, "yolo", "y", false, "enable yolo mode (non-interactive)")
176
+ rootCmd.Flags().BoolVar(&yolo, "no-interactive", false, "enable yolo mode (non-interactive)")
177
+ rootCmd.Flags().BoolVarP(&oneshot, "oneshot", "o", false, "full autonomous mode")
178
+
179
+ rootCmd.AddCommand(cli.NewTaskCommand())
180
+ rootCmd.AddCommand(cli.NewInstanceCommand())
181
+ rootCmd.AddCommand(cli.NewConfigCommand())
182
+ rootCmd.AddCommand(cli.NewVersionCommand())
183
+ rootCmd.AddCommand(cli.NewAuthCommand())
184
+ rootCmd.AddCommand(cli.NewLogsCommand())
185
+ // rootCmd.AddCommand(cli.NewDoctorCommand()) // Disabled for now
186
+
187
+ if err := rootCmd.ExecuteContext(context.Background()); err != nil {
188
+ os.Exit(1)
189
+ }
190
+ }
191
+
192
+ func promptForInitialTask(ctx context.Context, instanceAddress, modeFlag string) (string, error) {
193
+ // Show session banner before the initial input
194
+ showSessionBanner(ctx, instanceAddress, modeFlag)
195
+
196
+ var prompt string
197
+
198
+ // Create custom theme with mode-colored cursor and title
199
+ theme := huh.ThemeCharm()
200
+
201
+ // Set cursor and title color based on mode
202
+ modeColor := lipgloss.Color("3") // Yellow for plan
203
+ if modeFlag == "act" {
204
+ modeColor = lipgloss.Color("39") // Blue for act
205
+ }
206
+
207
+ theme.Focused.TextInput.Cursor = theme.Focused.TextInput.Cursor.Foreground(modeColor)
208
+ theme.Focused.Title = theme.Focused.Title.Foreground(modeColor)
209
+
210
+ form := huh.NewForm(
211
+ huh.NewGroup(
212
+ huh.NewText().
213
+ Title("Start a new Cline task").
214
+ Description("What would you like Cline to help you with?").
215
+ Placeholder("e.g., Create a REST API with authentication...").
216
+ Lines(5).
217
+ Value(&prompt),
218
+ ),
219
+ ).WithWidth(48).WithTheme(theme)
220
+
221
+ err := form.Run()
222
+ if err != nil {
223
+ // Check if user cancelled with Control-C
224
+ if err == huh.ErrUserAborted {
225
+ // Return a special error that indicates clean cancellation
226
+ // This allows deferred cleanup to run
227
+ return "", huh.ErrUserAborted
228
+ }
229
+ return "", err
230
+ }
231
+
232
+ return strings.TrimSpace(prompt), nil
233
+ }
234
+
235
+ // showSessionBanner displays session info before initial prompt
236
+ func showSessionBanner(ctx context.Context, instanceAddress, modeFlag string) {
237
+ bannerInfo := display.BannerInfo{
238
+ Version: global.CliVersion,
239
+ Mode: modeFlag, // Use the mode from command flag, not state
240
+ }
241
+
242
+ // If mode is empty, default to "plan"
243
+ if bannerInfo.Mode == "" {
244
+ bannerInfo.Mode = "plan"
245
+ }
246
+
247
+ // Get current working directory (this is what Cline will use)
248
+ if cwd, err := os.Getwd(); err == nil {
249
+ bannerInfo.Workdir = cwd
250
+ }
251
+
252
+ // Get provider/model using auth functions (same logic as auth menu)
253
+ manager, err := cli.NewTaskManagerForAddress(ctx, instanceAddress)
254
+ if err == nil {
255
+ if providerList, err := auth.GetProviderConfigurations(ctx, manager); err == nil {
256
+ // Show provider/model for the mode we'll be using
257
+ var providerDisplay *auth.ProviderDisplay
258
+ if bannerInfo.Mode == "plan" && providerList.PlanProvider != nil {
259
+ providerDisplay = providerList.PlanProvider
260
+ } else if bannerInfo.Mode == "act" && providerList.ActProvider != nil {
261
+ providerDisplay = providerList.ActProvider
262
+ }
263
+
264
+ if providerDisplay != nil {
265
+ bannerInfo.Provider = auth.GetProviderIDForEnum(providerDisplay.Provider)
266
+ bannerInfo.ModelID = providerDisplay.ModelID
267
+ }
268
+ }
269
+ }
270
+
271
+ // Render and display banner
272
+ banner := display.RenderSessionBanner(bannerInfo)
273
+ fmt.Println(banner)
274
+ fmt.Println() // Extra spacing before form
275
+ }
276
+
277
+ // isUserReadyToUse checks if the user has completed initial setup
278
+ // Returns true if welcomeViewCompleted flag is set OR user is authenticated
279
+ // Matches extension logic: welcomeViewCompleted = Boolean(globalState.welcomeViewCompleted || user?.uid)
280
+ func isUserReadyToUse(ctx context.Context, instanceAddress string) bool {
281
+ manager, err := cli.NewTaskManagerForAddress(ctx, instanceAddress)
282
+ if err != nil {
283
+ return false
284
+ }
285
+
286
+ // Get state
287
+ state, err := manager.GetClient().State.GetLatestState(ctx, &cline.EmptyRequest{})
288
+ if err != nil {
289
+ return false
290
+ }
291
+
292
+ // Parse state JSON
293
+ stateMap := make(map[string]interface{})
294
+ if err := json.Unmarshal([]byte(state.StateJson), &stateMap); err != nil {
295
+ return false
296
+ }
297
+
298
+ // Check 1: welcomeViewCompleted flag
299
+ if welcomeCompleted, ok := stateMap["welcomeViewCompleted"].(bool); ok && welcomeCompleted {
300
+ return true
301
+ }
302
+
303
+ // Check 2: Is user authenticated? (matches extension's || user?.uid check)
304
+ if userInfo, ok := stateMap["userInfo"].(map[string]interface{}); ok {
305
+ if uid, ok := userInfo["uid"].(string); ok && uid != "" {
306
+ return true
307
+ }
308
+ }
309
+
310
+ return false
311
+ }
312
+
313
+ // getContentFromStdinAndArgs reads content from both command line args and stdin, and combines them
314
+ func getContentFromStdinAndArgs(args []string) (string, error) {
315
+ var content strings.Builder
316
+
317
+ // Add command line args first (if any)
318
+ if len(args) > 0 {
319
+ content.WriteString(strings.Join(args, " "))
320
+ }
321
+
322
+ // Check if stdin has data
323
+ stat, err := os.Stdin.Stat()
324
+ if err != nil {
325
+ return "", fmt.Errorf("failed to stat stdin: %w", err)
326
+ }
327
+
328
+ // Check if data is being piped to stdin
329
+ if (stat.Mode() & os.ModeCharDevice) == 0 {
330
+ // Only try to read if there's actually data available
331
+ if stat.Size() > 0 {
332
+ stdinBytes, err := io.ReadAll(os.Stdin)
333
+ if err != nil {
334
+ return "", fmt.Errorf("failed to read from stdin: %w", err)
335
+ }
336
+
337
+ stdinContent := strings.TrimSpace(string(stdinBytes))
338
+ if stdinContent != "" {
339
+ if content.Len() > 0 {
340
+ content.WriteString(" ")
341
+ }
342
+ content.WriteString(stdinContent)
343
+ }
344
+ }
345
+ }
346
+
347
+ return content.String(), nil
348
+ }
@@ -0,0 +1,71 @@
1
+ package main
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "log"
7
+ "os"
8
+ "os/signal"
9
+ "syscall"
10
+
11
+ "github.com/spf13/cobra"
12
+
13
+ "github.com/cline/cli/pkg/hostbridge"
14
+ )
15
+
16
+ var (
17
+ port int
18
+ verbose bool
19
+ )
20
+
21
+ func main() {
22
+ rootCmd := &cobra.Command{
23
+ Use: "cline-host",
24
+ Short: "Cline Host Bridge Service",
25
+ Long: `A simple host bridge service that provides host operations for Cline Core.`,
26
+ RunE: runServer,
27
+ }
28
+
29
+ rootCmd.Flags().IntVarP(&port, "port", "p", 51052, "port to listen on")
30
+ rootCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "verbose logging")
31
+
32
+ if err := rootCmd.Execute(); err != nil {
33
+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
34
+ os.Exit(1)
35
+ }
36
+ }
37
+
38
+ func runServer(cmd *cobra.Command, args []string) error {
39
+ ctx := cmd.Context()
40
+
41
+ // Create gRPC hostbridge service
42
+ service := hostbridge.NewGrpcServer(port, verbose)
43
+
44
+ // Handle graceful shutdown
45
+ ctx, cancel := context.WithCancel(ctx)
46
+ defer cancel()
47
+
48
+ go func() {
49
+ sigChan := make(chan os.Signal, 1)
50
+ signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
51
+ <-sigChan
52
+
53
+ if verbose {
54
+ log.Println("Shutting down hostbridge server...")
55
+ }
56
+
57
+ cancel()
58
+ }()
59
+
60
+ // Start server
61
+ if verbose {
62
+ log.Printf("Starting Cline Host Bridge on port %d", port)
63
+ }
64
+
65
+ // Run the service
66
+ if err := service.Start(ctx); err != nil {
67
+ return fmt.Errorf("failed to run service: %w", err)
68
+ }
69
+
70
+ return nil
71
+ }
@@ -0,0 +1,154 @@
1
+ package e2e
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "os"
7
+ "path/filepath"
8
+ "syscall"
9
+ "testing"
10
+
11
+ "github.com/cline/cli/pkg/common"
12
+ )
13
+
14
+ // 2. Multi-instance start: default_instance remains the first started.
15
+ func TestMultiInstanceDefaultUnchanged(t *testing.T) {
16
+ _ = setTempClineDir(t)
17
+ ctx, cancel := context.WithTimeout(context.Background(), longTimeout)
18
+ defer cancel()
19
+
20
+ // Start first instance and wait healthy
21
+ _ = mustRunCLI(ctx, t, "instance", "new")
22
+ out1 := listInstancesJSON(ctx, t)
23
+ if len(out1.CoreInstances) != 1 {
24
+ t.Fatalf("expected 1 instance, got %d", len(out1.CoreInstances))
25
+ }
26
+ firstAddr := out1.CoreInstances[0].Address
27
+ waitForAddressHealthy(t, firstAddr, defaultTimeout)
28
+
29
+ // Start second instance
30
+ _ = mustRunCLI(ctx, t, "instance", "new")
31
+ out2 := listInstancesJSON(ctx, t)
32
+ if len(out2.CoreInstances) < 2 {
33
+ t.Fatalf("expected at least 2 instances, got %d", len(out2.CoreInstances))
34
+ }
35
+
36
+ // Default should remain the first started address
37
+ if out2.DefaultInstance != firstAddr {
38
+ t.Fatalf("default changed; expected %s, got %s", firstAddr, out2.DefaultInstance)
39
+ }
40
+ }
41
+
42
+ // 6. Default.json update after removal of current default
43
+ func TestDefaultJsonUpdateAfterRemoval(t *testing.T) {
44
+ _ = setTempClineDir(t)
45
+ ctx, cancel := context.WithTimeout(context.Background(), longTimeout)
46
+ defer cancel()
47
+
48
+ // Start two instances
49
+ _ = mustRunCLI(ctx, t, "instance", "new")
50
+ _ = mustRunCLI(ctx, t, "instance", "new")
51
+
52
+ out := listInstancesJSON(ctx, t)
53
+ if len(out.CoreInstances) < 2 {
54
+ t.Fatalf("expected at least 2 instances, got %d", len(out.CoreInstances))
55
+ }
56
+
57
+ // Choose second as new default
58
+ target := out.CoreInstances[1]
59
+ waitForAddressHealthy(t, target.Address, defaultTimeout)
60
+
61
+ // Set as default
62
+ _ = mustRunCLI(ctx, t, "instance", "use", target.Address)
63
+
64
+ // Verify default switched
65
+ out = listInstancesJSON(ctx, t)
66
+ if out.DefaultInstance != target.Address {
67
+ t.Fatalf("default_instance not updated to %s (got %s)", target.Address, out.DefaultInstance)
68
+ }
69
+
70
+ // Kill the default instance using runtime PID discovery
71
+ corePID := getCorePID(t, target.Address)
72
+ if corePID <= 0 {
73
+ t.Fatalf("could not find PID for core process at %s", target.Address)
74
+ }
75
+ t.Logf("Killing cline-core process PID %d for instance %s", corePID, target.Address)
76
+ if err := syscall.Kill(corePID, syscall.SIGKILL); err != nil {
77
+ t.Fatalf("kill pid %d: %v", corePID, err)
78
+ }
79
+
80
+ // Wait for removal
81
+ waitForAddressRemoved(t, target.Address, longTimeout)
82
+
83
+ // Clean up dangling host process (SIGKILL leaves these behind by design)
84
+ t.Logf("Cleaning up dangling host process on port %d", target.HostPort())
85
+ findAndKillHostProcess(t, target.HostPort())
86
+
87
+ // Ensure default_instance updated to another available instance (or removed if none remain)
88
+ out = listInstancesJSON(ctx, t)
89
+
90
+ // If there are instances left, default_instance must be one of them
91
+ if len(out.CoreInstances) > 0 {
92
+ found := false
93
+ for _, it := range out.CoreInstances {
94
+ if out.DefaultInstance == it.Address {
95
+ found = true
96
+ break
97
+ }
98
+ }
99
+ if !found {
100
+ t.Fatalf("default_instance %s not set to an existing instance after removal", out.DefaultInstance)
101
+ }
102
+ } else {
103
+ // No instances remain; cli-default-instance.json should be removed
104
+ clineDir := getClineDir(t)
105
+ defPath := filepath.Join(clineDir, common.SETTINGS_SUBFOLDER, "settings", "cli-default-instance.json")
106
+ if _, err := os.Stat(defPath); err == nil {
107
+ t.Fatalf("expected cli-default-instance.json removed when no instances remain")
108
+ }
109
+ }
110
+
111
+ // Also verify cli-default-instance.json on disk reflects the in-memory default (if any)
112
+ clineDir := getClineDir(t)
113
+ defPath := filepath.Join(clineDir, common.SETTINGS_SUBFOLDER, "settings", "cli-default-instance.json")
114
+ if len(out.CoreInstances) > 0 {
115
+ raw, err := os.ReadFile(defPath)
116
+ if err != nil {
117
+ t.Fatalf("read cli-default-instance.json: %v", err)
118
+ }
119
+ var tmp struct {
120
+ DefaultInstance string `json:"default_instance"`
121
+ }
122
+ if err := json.Unmarshal(raw, &tmp); err != nil {
123
+ t.Fatalf("unmarshal cli-default-instance.json: %v", err)
124
+ }
125
+ if tmp.DefaultInstance != out.DefaultInstance {
126
+ t.Fatalf("cli-default-instance.json mismatch: file=%s list=%s", tmp.DefaultInstance, out.DefaultInstance)
127
+ }
128
+ }
129
+ }
130
+
131
+ // 11. SQLite database missing (edge): list succeeds and returns empty set
132
+ func TestRegistryDirMissingEdge(t *testing.T) {
133
+ clineDir := setTempClineDir(t)
134
+
135
+ // Remove the settings directory entirely (which contains locks.db)
136
+ settingsDir := filepath.Join(clineDir, common.SETTINGS_SUBFOLDER)
137
+ if err := os.RemoveAll(settingsDir); err != nil {
138
+ t.Fatalf("RemoveAll(%s): %v", common.SETTINGS_SUBFOLDER, err)
139
+ }
140
+
141
+ // Listing should succeed and return empty results
142
+ ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
143
+ defer cancel()
144
+ out := listInstancesJSON(ctx, t)
145
+ if len(out.CoreInstances) != 0 {
146
+ t.Fatalf("expected 0 instances after removing %s dir, got %d", common.SETTINGS_SUBFOLDER, len(out.CoreInstances))
147
+ }
148
+
149
+ // Ensure cli-default-instance.json not present
150
+ defPath := filepath.Join(clineDir, common.SETTINGS_SUBFOLDER, "settings", "cli-default-instance.json")
151
+ if _, err := os.Stat(defPath); err == nil {
152
+ t.Fatalf("expected no cli-default-instance.json after removing %s dir", common.SETTINGS_SUBFOLDER)
153
+ }
154
+ }