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