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