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