root.go

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