12 tea "github.com/charmbracelet/bubbletea/v2"
13 "github.com/charmbracelet/crush/internal/app"
14 "github.com/charmbracelet/crush/internal/config"
15 "github.com/charmbracelet/crush/internal/db"
16 "github.com/charmbracelet/crush/internal/llm/agent"
17 "github.com/charmbracelet/crush/internal/log"
18 "github.com/charmbracelet/crush/internal/tui"
19 "github.com/charmbracelet/crush/internal/version"
20 "github.com/charmbracelet/fang"
21 "github.com/charmbracelet/x/term"
22 "github.com/spf13/cobra"
25var rootCmd = &cobra.Command{
27 Short: "Terminal-based AI assistant for software development",
28 Long: `Crush is a powerful terminal-based AI assistant that helps with software development tasks.
29It provides an interactive chat interface with AI capabilities, code analysis, and LSP integration
30to assist developers in writing, debugging, and understanding code directly from the terminal.`,
32 # Run in interactive mode
35 # Run with debug logging
38 # Run with debug slog.in a specific directory
39 crush -d -c /path/to/project
44 # Run a single non-interactive prompt
45 crush -p "Explain the use of context in Go"
47 # Run a single non-interactive prompt with JSON output format
48 crush -p "Explain the use of context in Go" -f json
50 RunE: func(cmd *cobra.Command, args []string) error {
52 debug, _ := cmd.Flags().GetBool("debug")
53 cwd, _ := cmd.Flags().GetString("cwd")
54 prompt, _ := cmd.Flags().GetString("prompt")
55 quiet, _ := cmd.Flags().GetBool("quiet")
60 return fmt.Errorf("failed to change directory: %v", err)
66 return fmt.Errorf("failed to get current working directory: %v", err)
71 cfg, err := config.Init(cwd, debug)
76 // Use the context from the command which includes signal handling
79 // Connect DB, this will also run migrations
80 conn, err := db.Connect(ctx, cfg.Options.DataDirectory)
85 app, err := app.New(ctx, conn, cfg)
87 slog.Error(fmt.Sprintf("Failed to create app instance: %v", err))
91 // Set up shutdown handling that works for both normal exit and signal interruption
92 var shutdownOnce sync.Once
94 shutdownOnce.Do(func() {
95 slog.Info("Shutting down application")
101 // Handle context cancellation (from signals) in a goroutine
104 slog.Info("Context cancelled, initiating shutdown")
108 // Initialize MCP tools early for both modes
109 initMCPTools(ctx, app, cfg)
111 prompt, err = maybePrependStdin(prompt)
113 slog.Error(fmt.Sprintf("Failed to read from stdin: %v", err))
117 // Non-interactive mode
119 // Run non-interactive flow using the App method
120 return app.RunNonInteractive(ctx, prompt, quiet)
124 program := tea.NewProgram(
127 tea.WithKeyReleases(),
128 tea.WithUniformKeyLayout(),
129 tea.WithMouseCellMotion(), // Use cell motion instead of all motion to reduce event flooding
130 tea.WithFilter(tui.MouseEventFilter), // Filter mouse events based on focus state
133 go app.Subscribe(program)
135 if _, err := program.Run(); err != nil {
136 slog.Error(fmt.Sprintf("TUI run error: %v", err))
137 return fmt.Errorf("TUI error: %v", err)
143func initMCPTools(ctx context.Context, app *app.App, cfg *config.Config) {
145 defer log.RecoverPanic("MCP-goroutine", nil)
147 // Create a context with timeout for the initial MCP tools fetch
148 ctxWithTimeout, cancel := context.WithTimeout(ctx, 30*time.Second)
151 // Set this up once with proper error handling
152 agent.GetMcpTools(ctxWithTimeout, app.Permissions, cfg)
153 slog.Info("MCP message handling goroutine exiting")
157func Execute(ctx context.Context) {
158 if err := fang.Execute(
161 fang.WithVersion(version.Version),
168 rootCmd.PersistentFlags().StringP("cwd", "c", "", "Current working directory")
170 rootCmd.Flags().BoolP("help", "h", false, "Help")
171 rootCmd.Flags().BoolP("debug", "d", false, "Debug")
172 rootCmd.Flags().StringP("prompt", "p", "", "Prompt to run in non-interactive mode")
174 // Add quiet flag to hide spinner in non-interactive mode
175 rootCmd.Flags().BoolP("quiet", "q", false, "Hide spinner in non-interactive mode")
178func maybePrependStdin(prompt string) (string, error) {
179 if term.IsTerminal(os.Stdin.Fd()) {
182 fi, err := os.Stdin.Stat()
186 if fi.Mode()&os.ModeNamedPipe == 0 {
189 bts, err := io.ReadAll(os.Stdin)
193 return string(bts) + "\n\n" + prompt, nil
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list