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