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 # Start interactive session with initial prompt
47 crush -i "Explain the use of context in Go"
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 // XXX: Handle errors.
55 debug, _ := cmd.Flags().GetBool("debug")
56 cwd, _ := cmd.Flags().GetString("cwd")
57 prompt, _ := cmd.Flags().GetString("prompt")
58 initial, _ := cmd.Flags().GetString("initial")
59 quiet, _ := cmd.Flags().GetBool("quiet")
60 yolo, _ := cmd.Flags().GetBool("yolo")
61
62 if cwd != "" {
63 err := os.Chdir(cwd)
64 if err != nil {
65 return fmt.Errorf("failed to change directory: %v", err)
66 }
67 }
68 if cwd == "" {
69 c, err := os.Getwd()
70 if err != nil {
71 return fmt.Errorf("failed to get current working directory: %v", err)
72 }
73 cwd = c
74 }
75
76 cfg, err := config.Init(cwd, debug)
77 if err != nil {
78 return err
79 }
80 if cfg.Permissions == nil {
81 cfg.Permissions = &config.Permissions{}
82 }
83 cfg.Permissions.SkipRequests = yolo
84
85 ctx := cmd.Context()
86
87 // Connect to DB; this will also run migrations.
88 conn, err := db.Connect(ctx, cfg.Options.DataDirectory)
89 if err != nil {
90 return err
91 }
92
93 app, err := app.New(ctx, conn, cfg)
94 if err != nil {
95 slog.Error("Failed to create app instance", "error", err)
96 return err
97 }
98 defer app.Shutdown()
99
100 prompt, err = maybePrependStdin(prompt)
101 if err != nil {
102 slog.Error("Failed to read from stdin", "error", err)
103 return err
104 }
105
106 // Non-interactive mode.
107 if prompt != "" {
108 // Run non-interactive flow using the App method
109 return app.RunNonInteractive(ctx, prompt, quiet)
110 }
111
112 // Set up the TUI.
113 program := tea.NewProgram(
114 tui.New(app, initial),
115 tea.WithAltScreen(),
116 tea.WithContext(ctx),
117 tea.WithMouseCellMotion(), // Use cell motion instead of all motion to reduce event flooding
118 tea.WithFilter(tui.MouseEventFilter), // Filter mouse events based on focus state
119 )
120
121 go app.Subscribe(program)
122
123 if _, err := program.Run(); err != nil {
124 slog.Error("TUI run error", "error", err)
125 return fmt.Errorf("TUI error: %v", err)
126 }
127 return nil
128 },
129}
130
131func Execute() {
132 if err := fang.Execute(
133 context.Background(),
134 rootCmd,
135 fang.WithVersion(version.Version),
136 fang.WithNotifySignal(os.Interrupt),
137 ); err != nil {
138 os.Exit(1)
139 }
140}
141
142func init() {
143 rootCmd.PersistentFlags().StringP("cwd", "c", "", "Current working directory")
144
145 rootCmd.Flags().BoolP("help", "h", false, "Help")
146 rootCmd.Flags().BoolP("debug", "d", false, "Debug")
147 rootCmd.Flags().StringP("prompt", "p", "", "Prompt to run in non-interactive mode")
148 rootCmd.Flags().StringP("initial", "i", "", "Initial prompt to start interactive session with")
149 rootCmd.Flags().BoolP("yolo", "y", false, "Automatically accept all permissions (dangerous mode)")
150
151 // Add quiet flag to hide spinner in non-interactive mode
152 rootCmd.Flags().BoolP("quiet", "q", false, "Hide spinner in non-interactive mode")
153}
154
155func maybePrependStdin(prompt string) (string, error) {
156 if term.IsTerminal(os.Stdin.Fd()) {
157 return prompt, nil
158 }
159 fi, err := os.Stdin.Stat()
160 if err != nil {
161 return prompt, err
162 }
163 if fi.Mode()&os.ModeNamedPipe == 0 {
164 return prompt, nil
165 }
166 bts, err := io.ReadAll(os.Stdin)
167 if err != nil {
168 return prompt, err
169 }
170 return string(bts) + "\n\n" + prompt, nil
171}