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 )
110
111 go app.Subscribe(program)
112
113 if _, err := program.Run(); err != nil {
114 slog.Error(fmt.Sprintf("TUI run error: %v", err))
115 return fmt.Errorf("TUI error: %v", err)
116 }
117 return nil
118 },
119}
120
121func Execute() {
122 if err := fang.Execute(
123 context.Background(),
124 rootCmd,
125 fang.WithVersion(version.Version),
126 fang.WithNotifySignal(os.Interrupt),
127 ); err != nil {
128 os.Exit(1)
129 }
130}
131
132func init() {
133 rootCmd.PersistentFlags().StringP("cwd", "c", "", "Current working directory")
134
135 rootCmd.Flags().BoolP("help", "h", false, "Help")
136 rootCmd.Flags().BoolP("debug", "d", false, "Debug")
137 rootCmd.Flags().StringP("prompt", "p", "", "Prompt to run in non-interactive mode")
138 rootCmd.Flags().BoolP("yolo", "y", false, "Automatically accept all permissions (dangerous mode)")
139
140 // Add quiet flag to hide spinner in non-interactive mode
141 rootCmd.Flags().BoolP("quiet", "q", false, "Hide spinner in non-interactive mode")
142}
143
144func maybePrependStdin(prompt string) (string, error) {
145 if term.IsTerminal(os.Stdin.Fd()) {
146 return prompt, nil
147 }
148 fi, err := os.Stdin.Stat()
149 if err != nil {
150 return prompt, err
151 }
152 if fi.Mode()&os.ModeNamedPipe == 0 {
153 return prompt, nil
154 }
155 bts, err := io.ReadAll(os.Stdin)
156 if err != nil {
157 return prompt, err
158 }
159 return string(bts) + "\n\n" + prompt, nil
160}