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