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		// XXX: Handle errors.
 52		debug, _ := cmd.Flags().GetBool("debug")
 53		cwd, _ := cmd.Flags().GetString("cwd")
 54		prompt, _ := cmd.Flags().GetString("prompt")
 55		quiet, _ := cmd.Flags().GetBool("quiet")
 56		yolo, _ := cmd.Flags().GetBool("yolo")
 57
 58		if cwd != "" {
 59			err := os.Chdir(cwd)
 60			if err != nil {
 61				return fmt.Errorf("failed to change directory: %v", err)
 62			}
 63		}
 64		if cwd == "" {
 65			c, err := os.Getwd()
 66			if err != nil {
 67				return fmt.Errorf("failed to get current working directory: %v", err)
 68			}
 69			cwd = c
 70		}
 71
 72		cfg, err := config.Init(cwd, debug)
 73		if err != nil {
 74			return err
 75		}
 76		cfg.Options.SkipPermissionsRequests = yolo
 77
 78		ctx := cmd.Context()
 79
 80		// Connect to DB; this will also run migrations.
 81		conn, err := db.Connect(ctx, cfg.Options.DataDirectory)
 82		if err != nil {
 83			return err
 84		}
 85
 86		app, err := app.New(ctx, conn, cfg)
 87		if err != nil {
 88			slog.Error(fmt.Sprintf("Failed to create app instance: %v", err))
 89			return err
 90		}
 91		defer app.Shutdown()
 92
 93		prompt, err = maybePrependStdin(prompt)
 94		if err != nil {
 95			slog.Error(fmt.Sprintf("Failed to read from stdin: %v", err))
 96			return err
 97		}
 98
 99		// Non-interactive mode.
100		if prompt != "" {
101			// Run non-interactive flow using the App method
102			return app.RunNonInteractive(ctx, prompt, quiet)
103		}
104
105		// Set up the TUI.
106		program := tea.NewProgram(
107			tui.New(app),
108			tea.WithAltScreen(),
109			tea.WithContext(ctx),
110			tea.WithMouseCellMotion(),            // Use cell motion instead of all motion to reduce event flooding
111			tea.WithFilter(tui.MouseEventFilter), // Filter mouse events based on focus state
112		)
113
114		go app.Subscribe(program)
115
116		if _, err := program.Run(); err != nil {
117			slog.Error(fmt.Sprintf("TUI run error: %v", err))
118			return fmt.Errorf("TUI error: %v", err)
119		}
120		return nil
121	},
122}
123
124func Execute() {
125	if err := fang.Execute(
126		context.Background(),
127		rootCmd,
128		fang.WithVersion(version.Version),
129		fang.WithNotifySignal(os.Interrupt),
130	); err != nil {
131		os.Exit(1)
132	}
133}
134
135func init() {
136	rootCmd.PersistentFlags().StringP("cwd", "c", "", "Current working directory")
137
138	rootCmd.Flags().BoolP("help", "h", false, "Help")
139	rootCmd.Flags().BoolP("debug", "d", false, "Debug")
140	rootCmd.Flags().StringP("prompt", "p", "", "Prompt to run in non-interactive mode")
141	rootCmd.Flags().BoolP("yolo", "y", false, "Automatically accept all permissions (dangerous mode)")
142
143	// Add quiet flag to hide spinner in non-interactive mode
144	rootCmd.Flags().BoolP("quiet", "q", false, "Hide spinner in non-interactive mode")
145}
146
147func maybePrependStdin(prompt string) (string, error) {
148	if term.IsTerminal(os.Stdin.Fd()) {
149		return prompt, nil
150	}
151	fi, err := os.Stdin.Stat()
152	if err != nil {
153		return prompt, err
154	}
155	if fi.Mode()&os.ModeNamedPipe == 0 {
156		return prompt, nil
157	}
158	bts, err := io.ReadAll(os.Stdin)
159	if err != nil {
160		return prompt, err
161	}
162	return string(bts) + "\n\n" + prompt, nil
163}