on 58dfed5 help sign in
crush/internal/cmd/root.go
205 lines 5.1 kB history raw
1package cmd
3import (
4 "context"
5 "fmt"
6 "io"
7 "log/slog"
8 "os"
9 "strings"
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"
22func init() {
23 rootCmd.PersistentFlags().StringP("cwd", "c", "", "Current working directory")
24 rootCmd.PersistentFlags().BoolP("debug", "d", false, "Debug")
26 rootCmd.Flags().BoolP("help", "h", false, "Help")
27 rootCmd.Flags().BoolP("yolo", "y", false, "Automatically accept all permissions (dangerous mode)")
29 runCmd.Flags().BoolP("quiet", "q", false, "Hide spinner")
30 rootCmd.AddCommand(runCmd)
33var rootCmd = &cobra.Command{
34 Use: "crush",
35 Short: "Terminal-based AI assistant for software development",
36 Long: `Crush is a powerful terminal-based AI assistant that helps with software development tasks.
37It provides an interactive chat interface with AI capabilities, code analysis, and LSP integration
38to assist developers in writing, debugging, and understanding code directly from the terminal.`,
39 Example: `
40# Run in interactive mode
41crush
43# Run with debug logging
44crush -d
46# Run with debug logging in a specific directory
47crush -d -c /path/to/project
49# Print version
50crush -v
52# Run a single non-interactive prompt
53crush run "Explain the use of context in Go"
55# Run in dangerous mode (auto-accept all permissions)
56crush -y
57 `,
58 RunE: func(cmd *cobra.Command, args []string) error {
59 app, err := setupApp(cmd)
60 if err != nil {
61 return err
62 }
63 defer app.Shutdown()
65 // Set up the TUI.
66 program := tea.NewProgram(
67 tui.New(app),
68 tea.WithAltScreen(),
69 tea.WithContext(cmd.Context()),
70 tea.WithMouseCellMotion(), // Use cell motion instead of all motion to reduce event flooding
71 tea.WithFilter(tui.MouseEventFilter), // Filter mouse events based on focus state
72 )
74 go app.Subscribe(program)
76 if _, err := program.Run(); err != nil {
77 slog.Error("TUI run error", "error", err)
78 return fmt.Errorf("TUI error: %v", err)
79 }
80 return nil
81 },
84var runCmd = &cobra.Command{
85 Use: "run [prompt...]",
86 Short: "Run a single non-interactive prompt",
87 Long: `Run a single prompt in non-interactive mode and exit.
88The prompt can be provided as arguments or piped from stdin.`,
89 Example: `
90# Run a simple prompt
91crush run Explain the use of context in Go
93# Pipe input from stdin
94echo "What is this code doing?" | crush run
96# Run with quiet mode (no spinner)
97crush run -q "Generate a README for this project"
98 `,
99 RunE: func(cmd *cobra.Command, args []string) error {
100 quiet, _ := cmd.Flags().GetBool("quiet")
102 app, err := setupApp(cmd)
103 if err != nil {
104 return err
105 }
106 defer app.Shutdown()
108 prompt := strings.Join(args, " ")
110 prompt, err = maybePrependStdin(prompt)
111 if err != nil {
112 slog.Error("Failed to read from stdin", "error", err)
113 return err
114 }
116 if prompt == "" {
117 return fmt.Errorf("no prompt provided")
118 }
120 // Run non-interactive flow using the App method
121 return app.RunNonInteractive(cmd.Context(), prompt, quiet)
122 },
125func Execute() {
126 if err := fang.Execute(
127 context.Background(),
128 rootCmd,
129 fang.WithVersion(version.Version),
130 fang.WithNotifySignal(os.Interrupt),
131 ); err != nil {
132 os.Exit(1)
133 }
136// setupApp handles the common setup logic for both interactive and non-interactive modes.
137// It returns the app instance, config, cleanup function, and any error.
138func setupApp(cmd *cobra.Command) (*app.App, error) {
139 debug, _ := cmd.Flags().GetBool("debug")
140 yolo, _ := cmd.Flags().GetBool("yolo")
141 ctx := cmd.Context()
143 cwd, err := resolveCwd(cmd)
144 if err != nil {
145 return nil, err
146 }
148 cfg, err := config.Init(cwd, debug)
149 if err != nil {
150 return nil, err
151 }
153 if cfg.Permissions == nil {
154 cfg.Permissions = &config.Permissions{}
155 }
156 cfg.Permissions.SkipRequests = yolo
158 // Connect to DB; this will also run migrations.
159 conn, err := db.Connect(ctx, cfg.Options.DataDirectory)
160 if err != nil {
161 return nil, err
162 }
164 appInstance, err := app.New(ctx, conn, cfg)
165 if err != nil {
166 slog.Error("Failed to create app instance", "error", err)
167 return nil, err
168 }
170 return appInstance, nil
173func maybePrependStdin(prompt string) (string, error) {
174 if term.IsTerminal(os.Stdin.Fd()) {
175 return prompt, nil
176 }
177 fi, err := os.Stdin.Stat()
178 if err != nil {
179 return prompt, err
180 }
181 if fi.Mode()&os.ModeNamedPipe == 0 {
182 return prompt, nil
183 }
184 bts, err := io.ReadAll(os.Stdin)
185 if err != nil {
186 return prompt, err
187 }
188 return string(bts) + "\n\n" + prompt, nil
191func resolveCwd(cmd *cobra.Command) (string, error) {
192 cwd, _ := cmd.Flags().GetString("cwd")
193 if cwd != "" {
194 err := os.Chdir(cwd)
195 if err != nil {
196 return "", fmt.Errorf("failed to change directory: %v", err)
197 }
198 return cwd, nil
199 }
200 cwd, err := os.Getwd()
201 if err != nil {
202 return "", fmt.Errorf("failed to get current working directory: %v", err)
203 }
204 return cwd, nil