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