1package cmd
  2
  3import (
  4	"context"
  5	"fmt"
  6	"io"
  7	"log/slog"
  8	"os"
  9	"strings"
 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/tui"
 16	"github.com/charmbracelet/crush/internal/version"
 17	"github.com/charmbracelet/fang"
 18	"github.com/charmbracelet/x/term"
 19	"github.com/spf13/cobra"
 20)
 21
 22func init() {
 23	rootCmd.PersistentFlags().StringP("cwd", "c", "", "Current working directory")
 24	rootCmd.PersistentFlags().BoolP("debug", "d", false, "Debug")
 25
 26	rootCmd.Flags().BoolP("help", "h", false, "Help")
 27	rootCmd.Flags().BoolP("yolo", "y", false, "Automatically accept all permissions (dangerous mode)")
 28
 29	runCmd.Flags().BoolP("quiet", "q", false, "Hide spinner")
 30	rootCmd.AddCommand(runCmd)
 31}
 32
 33var rootCmd = &cobra.Command{
 34	Use:   "crush",
 35	Short: "Terminal-based AI assistant for software development",
 36	Long: `Crush is a powerful terminal-based AI assistant that helps with software development tasks.
 37It provides an interactive chat interface with AI capabilities, code analysis, and LSP integration
 38to assist developers in writing, debugging, and understanding code directly from the terminal.`,
 39	Example: `
 40# Run in interactive mode
 41crush
 42
 43# Run with debug logging
 44crush -d
 45
 46# Run with debug logging in a specific directory
 47crush -d -c /path/to/project
 48
 49# Print version
 50crush -v
 51
 52# Run a single non-interactive prompt
 53crush run "Explain the use of context in Go"
 54
 55# Run in dangerous mode (auto-accept all permissions)
 56crush -y
 57  `,
 58	RunE: func(cmd *cobra.Command, args []string) error {
 59		app, err := setupApp(cmd)
 60		if err != nil {
 61			return err
 62		}
 63		defer app.Shutdown()
 64
 65		// Set up the TUI.
 66		program := tea.NewProgram(
 67			tui.New(app),
 68			tea.WithAltScreen(),
 69			tea.WithContext(cmd.Context()),
 70			tea.WithMouseCellMotion(),            // Use cell motion instead of all motion to reduce event flooding
 71			tea.WithFilter(tui.MouseEventFilter), // Filter mouse events based on focus state
 72		)
 73
 74		go app.Subscribe(program)
 75
 76		if _, err := program.Run(); err != nil {
 77			slog.Error("TUI run error", "error", err)
 78			return fmt.Errorf("TUI error: %v", err)
 79		}
 80		return nil
 81	},
 82}
 83
 84var runCmd = &cobra.Command{
 85	Use:   "run [prompt...]",
 86	Short: "Run a single non-interactive prompt",
 87	Long: `Run a single prompt in non-interactive mode and exit.
 88The prompt can be provided as arguments or piped from stdin.`,
 89	Example: `
 90# Run a simple prompt
 91crush run Explain the use of context in Go
 92
 93# Pipe input from stdin
 94echo "What is this code doing?" | crush run
 95
 96# Run with quiet mode (no spinner)
 97crush run -q "Generate a README for this project"
 98  `,
 99	RunE: func(cmd *cobra.Command, args []string) error {
100		quiet, _ := cmd.Flags().GetBool("quiet")
101
102		app, err := setupApp(cmd)
103		if err != nil {
104			return err
105		}
106		defer app.Shutdown()
107
108		if !app.Config().IsConfigured() {
109			return fmt.Errorf("no providers configured - please run 'crush' to set up a provider interactively")
110		}
111
112		prompt := strings.Join(args, " ")
113
114		prompt, err = maybePrependStdin(prompt)
115		if err != nil {
116			slog.Error("Failed to read from stdin", "error", err)
117			return err
118		}
119
120		if prompt == "" {
121			return fmt.Errorf("no prompt provided")
122		}
123
124		// Run non-interactive flow using the App method
125		return app.RunNonInteractive(cmd.Context(), prompt, quiet)
126	},
127}
128
129func Execute() {
130	if err := fang.Execute(
131		context.Background(),
132		rootCmd,
133		fang.WithVersion(version.Version),
134		fang.WithNotifySignal(os.Interrupt),
135	); err != nil {
136		os.Exit(1)
137	}
138}
139
140// setupApp handles the common setup logic for both interactive and non-interactive modes.
141// It returns the app instance, config, cleanup function, and any error.
142func setupApp(cmd *cobra.Command) (*app.App, error) {
143	debug, _ := cmd.Flags().GetBool("debug")
144	yolo, _ := cmd.Flags().GetBool("yolo")
145	ctx := cmd.Context()
146
147	cwd, err := resolveCwd(cmd)
148	if err != nil {
149		return nil, err
150	}
151
152	cfg, err := config.Init(cwd, debug)
153	if err != nil {
154		return nil, err
155	}
156
157	if cfg.Permissions == nil {
158		cfg.Permissions = &config.Permissions{}
159	}
160	cfg.Permissions.SkipRequests = yolo
161
162	// Connect to DB; this will also run migrations.
163	conn, err := db.Connect(ctx, cfg.Options.DataDirectory)
164	if err != nil {
165		return nil, err
166	}
167
168	appInstance, err := app.New(ctx, conn, cfg)
169	if err != nil {
170		slog.Error("Failed to create app instance", "error", err)
171		return nil, err
172	}
173
174	return appInstance, nil
175}
176
177func maybePrependStdin(prompt string) (string, error) {
178	if term.IsTerminal(os.Stdin.Fd()) {
179		return prompt, nil
180	}
181	fi, err := os.Stdin.Stat()
182	if err != nil {
183		return prompt, err
184	}
185	if fi.Mode()&os.ModeNamedPipe == 0 {
186		return prompt, nil
187	}
188	bts, err := io.ReadAll(os.Stdin)
189	if err != nil {
190		return prompt, err
191	}
192	return string(bts) + "\n\n" + prompt, nil
193}
194
195func resolveCwd(cmd *cobra.Command) (string, error) {
196	cwd, _ := cmd.Flags().GetString("cwd")
197	if cwd != "" {
198		err := os.Chdir(cwd)
199		if err != nil {
200			return "", fmt.Errorf("failed to change directory: %v", err)
201		}
202		return cwd, nil
203	}
204	cwd, err := os.Getwd()
205	if err != nil {
206		return "", fmt.Errorf("failed to get current working directory: %v", err)
207	}
208	return cwd, nil
209}