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