@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
package/.npmrc.tmp
ADDED
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
|
+
}
|