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