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