1package cmd
2
3import (
4 "context"
5 "fmt"
6 "io"
7 "log/slog"
8 "os"
9 "time"
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
22var rootCmd = &cobra.Command{
23 Use: "crush",
24 Short: "Terminal-based AI assistant for software development",
25 Long: `Crush is a powerful terminal-based AI assistant that helps with software development tasks.
26It provides an interactive chat interface with AI capabilities, code analysis, and LSP integration
27to assist developers in writing, debugging, and understanding code directly from the terminal.`,
28 Example: `
29 # Run in interactive mode
30 crush
31
32 # Run with debug logging
33 crush -d
34
35 # Run with debug slog.in a specific directory
36 crush -d -c /path/to/project
37
38 # Print version
39 crush -v
40
41 # Run a single non-interactive prompt
42 crush -p "Explain the use of context in Go"
43
44 # Run a single non-interactive prompt with JSON output format
45 crush -p "Explain the use of context in Go" -f json
46
47 # Run in dangerous mode (auto-accept all permissions)
48 crush -y
49 `,
50 RunE: func(cmd *cobra.Command, args []string) error {
51 // Load the config
52 // XXX: Handle errors.
53 debug, _ := cmd.Flags().GetBool("debug")
54 cwd, _ := cmd.Flags().GetString("cwd")
55 prompt, _ := cmd.Flags().GetString("prompt")
56 quiet, _ := cmd.Flags().GetBool("quiet")
57 yolo, _ := cmd.Flags().GetBool("yolo")
58
59 if cwd != "" {
60 err := os.Chdir(cwd)
61 if err != nil {
62 return fmt.Errorf("failed to change directory: %v", err)
63 }
64 }
65 if cwd == "" {
66 c, err := os.Getwd()
67 if err != nil {
68 return fmt.Errorf("failed to get current working directory: %v", err)
69 }
70 cwd = c
71 }
72
73 cfg, err := config.Init(cwd, debug)
74 if err != nil {
75 return err
76 }
77 cfg.Options.SkipPermissionsRequests = yolo
78
79 ctx := cmd.Context()
80
81 // Connect to DB; this will also run migrations.
82 conn, err := db.Connect(ctx, cfg.Options.DataDirectory)
83 if err != nil {
84 return err
85 }
86
87 slog.Info("Initing...")
88 now := time.Now()
89 app, err := app.New(ctx, conn, cfg)
90 if err != nil {
91 slog.Error(fmt.Sprintf("Failed to create app instance: %v", err))
92 return err
93 }
94 defer app.Shutdown()
95 slog.Info("Init done", "took", time.Since(now).String())
96
97 prompt, err = maybePrependStdin(prompt)
98 if err != nil {
99 slog.Error(fmt.Sprintf("Failed to read from stdin: %v", err))
100 return err
101 }
102
103 // Non-interactive mode.
104 if prompt != "" {
105 // Run non-interactive flow using the App method
106 return app.RunNonInteractive(ctx, prompt, quiet)
107 }
108
109 // Set up the TUI.
110 program := tea.NewProgram(
111 tui.New(app),
112 tea.WithAltScreen(),
113 tea.WithContext(ctx),
114 tea.WithMouseCellMotion(), // Use cell motion instead of all motion to reduce event flooding
115 tea.WithFilter(tui.MouseEventFilter), // Filter mouse events based on focus state
116 )
117
118 go app.Subscribe(program)
119
120 if _, err := program.Run(); err != nil {
121 slog.Error(fmt.Sprintf("TUI run error: %v", err))
122 return fmt.Errorf("TUI error: %v", err)
123 }
124 return nil
125 },
126}
127
128func Execute() {
129 if err := fang.Execute(
130 context.Background(),
131 rootCmd,
132 fang.WithVersion(version.Version),
133 fang.WithNotifySignal(os.Interrupt),
134 ); err != nil {
135 os.Exit(1)
136 }
137}
138
139func init() {
140 rootCmd.PersistentFlags().StringP("cwd", "c", "", "Current working directory")
141
142 rootCmd.Flags().BoolP("help", "h", false, "Help")
143 rootCmd.Flags().BoolP("debug", "d", false, "Debug")
144 rootCmd.Flags().StringP("prompt", "p", "", "Prompt to run in non-interactive mode")
145 rootCmd.Flags().BoolP("yolo", "y", false, "Automatically accept all permissions (dangerous mode)")
146
147 // Add quiet flag to hide spinner in non-interactive mode
148 rootCmd.Flags().BoolP("quiet", "q", false, "Hide spinner in non-interactive mode")
149}
150
151func maybePrependStdin(prompt string) (string, error) {
152 if term.IsTerminal(os.Stdin.Fd()) {
153 return prompt, nil
154 }
155 fi, err := os.Stdin.Stat()
156 if err != nil {
157 return prompt, err
158 }
159 if fi.Mode()&os.ModeNamedPipe == 0 {
160 return prompt, nil
161 }
162 bts, err := io.ReadAll(os.Stdin)
163 if err != nil {
164 return prompt, err
165 }
166 return string(bts) + "\n\n" + prompt, nil
167}