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		prompt := strings.Join(args, " ")
109
110		prompt, err = maybePrependStdin(prompt)
111		if err != nil {
112			slog.Error("Failed to read from stdin", "error", err)
113			return err
114		}
115
116		if prompt == "" {
117			return fmt.Errorf("no prompt provided")
118		}
119
120		// Run non-interactive flow using the App method
121		return app.RunNonInteractive(cmd.Context(), prompt, quiet)
122	},
123}
124
125func Execute() {
126	if err := fang.Execute(
127		context.Background(),
128		rootCmd,
129		fang.WithVersion(version.Version),
130		fang.WithNotifySignal(os.Interrupt),
131	); err != nil {
132		os.Exit(1)
133	}
134}
135
136// setupApp handles the common setup logic for both interactive and non-interactive modes.
137// It returns the app instance, config, cleanup function, and any error.
138func setupApp(cmd *cobra.Command) (*app.App, error) {
139	debug, _ := cmd.Flags().GetBool("debug")
140	yolo, _ := cmd.Flags().GetBool("yolo")
141	ctx := cmd.Context()
142
143	cwd, err := resolveCwd(cmd)
144	if err != nil {
145		return nil, err
146	}
147
148	cfg, err := config.Init(cwd, debug)
149	if err != nil {
150		return nil, err
151	}
152
153	if cfg.Permissions == nil {
154		cfg.Permissions = &config.Permissions{}
155	}
156	cfg.Permissions.SkipRequests = yolo
157
158	// Connect to DB; this will also run migrations.
159	conn, err := db.Connect(ctx, cfg.Options.DataDirectory)
160	if err != nil {
161		return nil, err
162	}
163
164	appInstance, err := app.New(ctx, conn, cfg)
165	if err != nil {
166		slog.Error("Failed to create app instance", "error", err)
167		return nil, err
168	}
169
170	return appInstance, nil
171}
172
173func maybePrependStdin(prompt string) (string, error) {
174	if term.IsTerminal(os.Stdin.Fd()) {
175		return prompt, nil
176	}
177	fi, err := os.Stdin.Stat()
178	if err != nil {
179		return prompt, err
180	}
181	if fi.Mode()&os.ModeNamedPipe == 0 {
182		return prompt, nil
183	}
184	bts, err := io.ReadAll(os.Stdin)
185	if err != nil {
186		return prompt, err
187	}
188	return string(bts) + "\n\n" + prompt, nil
189}
190
191func resolveCwd(cmd *cobra.Command) (string, error) {
192	cwd, _ := cmd.Flags().GetString("cwd")
193	if cwd != "" {
194		err := os.Chdir(cwd)
195		if err != nil {
196			return "", fmt.Errorf("failed to change directory: %v", err)
197		}
198		return cwd, nil
199	}
200	cwd, err := os.Getwd()
201	if err != nil {
202		return "", fmt.Errorf("failed to get current working directory: %v", err)
203	}
204	return cwd, nil
205}