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