1package cmd
2
3import (
4 "context"
5 "fmt"
6 "io"
7 "log/slog"
8 "os"
9 "path/filepath"
10 "strconv"
11
12 tea "github.com/charmbracelet/bubbletea/v2"
13 "github.com/charmbracelet/crush/internal/app"
14 "github.com/charmbracelet/crush/internal/config"
15 "github.com/charmbracelet/crush/internal/db"
16 "github.com/charmbracelet/crush/internal/event"
17 "github.com/charmbracelet/crush/internal/tui"
18 "github.com/charmbracelet/crush/internal/version"
19 "github.com/charmbracelet/fang"
20 "github.com/charmbracelet/x/term"
21 "github.com/spf13/cobra"
22)
23
24func init() {
25 rootCmd.PersistentFlags().StringP("cwd", "c", "", "Current working directory")
26 rootCmd.PersistentFlags().StringP("data-dir", "D", "", "Custom crush data directory")
27 rootCmd.PersistentFlags().BoolP("debug", "d", false, "Debug")
28
29 rootCmd.Flags().BoolP("help", "h", false, "Help")
30 rootCmd.Flags().BoolP("yolo", "y", false, "Automatically accept all permissions (dangerous mode)")
31
32 rootCmd.AddCommand(runCmd)
33 rootCmd.AddCommand(updateProvidersCmd)
34}
35
36var rootCmd = &cobra.Command{
37 Use: "crush",
38 Short: "Terminal-based AI assistant for software development",
39 Long: `Crush is a powerful terminal-based AI assistant that helps with software development tasks.
40It provides an interactive chat interface with AI capabilities, code analysis, and LSP integration
41to assist developers in writing, debugging, and understanding code directly from the terminal.`,
42 Example: `
43# Run in interactive mode
44crush
45
46# Run with debug logging
47crush -d
48
49# Run with debug logging in a specific directory
50crush -d -c /path/to/project
51
52# Run with custom data directory
53crush -D /path/to/custom/.crush
54
55# Print version
56crush -v
57
58# Run a single non-interactive prompt
59crush run "Explain the use of context in Go"
60
61# Run in dangerous mode (auto-accept all permissions)
62crush -y
63 `,
64 RunE: func(cmd *cobra.Command, args []string) error {
65 app, err := setupApp(cmd)
66 if err != nil {
67 return err
68 }
69 defer app.Shutdown()
70
71 event.AppInitialized()
72
73 // Set up the TUI.
74 program := tea.NewProgram(
75 tui.New(app),
76 tea.WithAltScreen(),
77 tea.WithContext(cmd.Context()),
78 tea.WithMouseCellMotion(), // Use cell motion instead of all motion to reduce event flooding
79 tea.WithFilter(tui.MouseEventFilter), // Filter mouse events based on focus state
80 )
81
82 go app.Subscribe(program)
83
84 if _, err := program.Run(); err != nil {
85 event.Error(err)
86 slog.Error("TUI run error", "error", err)
87 return fmt.Errorf("TUI error: %v", err)
88 }
89 return nil
90 },
91 PostRun: func(cmd *cobra.Command, args []string) {
92 event.AppExited()
93 },
94}
95
96func Execute() {
97 if err := fang.Execute(
98 context.Background(),
99 rootCmd,
100 fang.WithVersion(version.Version),
101 fang.WithNotifySignal(os.Interrupt),
102 ); err != nil {
103 os.Exit(1)
104 }
105}
106
107// setupApp handles the common setup logic for both interactive and non-interactive modes.
108// It returns the app instance, config, cleanup function, and any error.
109func setupApp(cmd *cobra.Command) (*app.App, error) {
110 debug, _ := cmd.Flags().GetBool("debug")
111 yolo, _ := cmd.Flags().GetBool("yolo")
112 dataDir, _ := cmd.Flags().GetString("data-dir")
113 ctx := cmd.Context()
114
115 cwd, err := ResolveCwd(cmd)
116 if err != nil {
117 return nil, err
118 }
119
120 cfg, err := config.Init(cwd, dataDir, debug)
121 if err != nil {
122 return nil, err
123 }
124
125 if cfg.Permissions == nil {
126 cfg.Permissions = &config.Permissions{}
127 }
128 cfg.Permissions.SkipRequests = yolo
129
130 if err := createDotCrushDir(cfg.Options.DataDirectory); err != nil {
131 return nil, err
132 }
133
134 // Connect to DB; this will also run migrations.
135 conn, err := db.Connect(ctx, cfg.Options.DataDirectory)
136 if err != nil {
137 return nil, err
138 }
139
140 appInstance, err := app.New(ctx, conn, cfg)
141 if err != nil {
142 slog.Error("Failed to create app instance", "error", err)
143 return nil, err
144 }
145
146 if shouldEnableMetrics() {
147 event.Init()
148 }
149
150 return appInstance, nil
151}
152
153func shouldEnableMetrics() bool {
154 if v, _ := strconv.ParseBool(os.Getenv("CRUSH_DISABLE_METRICS")); v {
155 return false
156 }
157 if v, _ := strconv.ParseBool(os.Getenv("DO_NOT_TRACK")); v {
158 return false
159 }
160 if config.Get().Options.DisableMetrics {
161 return false
162 }
163 return true
164}
165
166func MaybePrependStdin(prompt string) (string, error) {
167 if term.IsTerminal(os.Stdin.Fd()) {
168 return prompt, nil
169 }
170 fi, err := os.Stdin.Stat()
171 if err != nil {
172 return prompt, err
173 }
174 if fi.Mode()&os.ModeNamedPipe == 0 {
175 return prompt, nil
176 }
177 bts, err := io.ReadAll(os.Stdin)
178 if err != nil {
179 return prompt, err
180 }
181 return string(bts) + "\n\n" + prompt, nil
182}
183
184func ResolveCwd(cmd *cobra.Command) (string, error) {
185 cwd, _ := cmd.Flags().GetString("cwd")
186 if cwd != "" {
187 err := os.Chdir(cwd)
188 if err != nil {
189 return "", fmt.Errorf("failed to change directory: %v", err)
190 }
191 return cwd, nil
192 }
193 cwd, err := os.Getwd()
194 if err != nil {
195 return "", fmt.Errorf("failed to get current working directory: %v", err)
196 }
197 return cwd, nil
198}
199
200func createDotCrushDir(dir string) error {
201 if err := os.MkdirAll(dir, 0o700); err != nil {
202 return fmt.Errorf("failed to create data directory: %q %w", dir, err)
203 }
204
205 gitIgnorePath := filepath.Join(dir, ".gitignore")
206 if _, err := os.Stat(gitIgnorePath); os.IsNotExist(err) {
207 if err := os.WriteFile(gitIgnorePath, []byte("*\n"), 0o644); err != nil {
208 return fmt.Errorf("failed to create .gitignore file: %q %w", gitIgnorePath, err)
209 }
210 }
211
212 return nil
213}