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
 21func init() {
 22	rootCmd.PersistentFlags().StringP("cwd", "c", "", "Current working directory")
 23	rootCmd.PersistentFlags().BoolP("debug", "d", false, "Debug")
 24
 25	rootCmd.Flags().BoolP("help", "h", false, "Help")
 26	rootCmd.Flags().BoolP("yolo", "y", false, "Automatically accept all permissions (dangerous mode)")
 27
 28	rootCmd.AddCommand(runCmd)
 29}
 30
 31var rootCmd = &cobra.Command{
 32	Use:   "crush",
 33	Short: "Terminal-based AI assistant for software development",
 34	Long: `Crush is a powerful terminal-based AI assistant that helps with software development tasks.
 35It provides an interactive chat interface with AI capabilities, code analysis, and LSP integration
 36to assist developers in writing, debugging, and understanding code directly from the terminal.`,
 37	Example: `
 38# Run in interactive mode
 39crush
 40
 41# Run with debug logging
 42crush -d
 43
 44# Run with debug logging in a specific directory
 45crush -d -c /path/to/project
 46
 47# Print version
 48crush -v
 49
 50# Run a single non-interactive prompt
 51crush run "Explain the use of context in Go"
 52
 53# Run in dangerous mode (auto-accept all permissions)
 54crush -y
 55  `,
 56	RunE: func(cmd *cobra.Command, args []string) error {
 57		app, err := setupApp(cmd)
 58		if err != nil {
 59			return err
 60		}
 61		defer app.Shutdown()
 62
 63		// Set up the TUI.
 64		program := tea.NewProgram(
 65			tui.New(app),
 66			tea.WithAltScreen(),
 67			tea.WithContext(cmd.Context()),
 68			tea.WithMouseCellMotion(),            // Use cell motion instead of all motion to reduce event flooding
 69			tea.WithFilter(tui.MouseEventFilter), // Filter mouse events based on focus state
 70		)
 71
 72		go app.Subscribe(program)
 73
 74		if _, err := program.Run(); err != nil {
 75			slog.Error("TUI run error", "error", err)
 76			return fmt.Errorf("TUI error: %v", err)
 77		}
 78		return nil
 79	},
 80}
 81
 82func Execute() {
 83	if err := fang.Execute(
 84		context.Background(),
 85		rootCmd,
 86		fang.WithVersion(version.Version),
 87		fang.WithNotifySignal(os.Interrupt),
 88	); err != nil {
 89		os.Exit(1)
 90	}
 91}
 92
 93// setupApp handles the common setup logic for both interactive and non-interactive modes.
 94// It returns the app instance, config, cleanup function, and any error.
 95func setupApp(cmd *cobra.Command) (*app.App, error) {
 96	debug, _ := cmd.Flags().GetBool("debug")
 97	yolo, _ := cmd.Flags().GetBool("yolo")
 98	ctx := cmd.Context()
 99
100	cwd, err := ResolveCwd(cmd)
101	if err != nil {
102		return nil, err
103	}
104
105	cfg, err := config.Init(cwd, debug)
106	if err != nil {
107		return nil, err
108	}
109
110	if cfg.Permissions == nil {
111		cfg.Permissions = &config.Permissions{}
112	}
113	cfg.Permissions.SkipRequests = yolo
114
115	// Connect to DB; this will also run migrations.
116	conn, err := db.Connect(ctx, cfg.Options.DataDirectory)
117	if err != nil {
118		return nil, err
119	}
120
121	appInstance, err := app.New(ctx, conn, cfg)
122	if err != nil {
123		slog.Error("Failed to create app instance", "error", err)
124		return nil, err
125	}
126
127	return appInstance, nil
128}
129
130func MaybePrependStdin(prompt string) (string, error) {
131	if term.IsTerminal(os.Stdin.Fd()) {
132		return prompt, nil
133	}
134	fi, err := os.Stdin.Stat()
135	if err != nil {
136		return prompt, err
137	}
138	if fi.Mode()&os.ModeNamedPipe == 0 {
139		return prompt, nil
140	}
141	bts, err := io.ReadAll(os.Stdin)
142	if err != nil {
143		return prompt, err
144	}
145	return string(bts) + "\n\n" + prompt, nil
146}
147
148func ResolveCwd(cmd *cobra.Command) (string, error) {
149	cwd, _ := cmd.Flags().GetString("cwd")
150	if cwd != "" {
151		err := os.Chdir(cwd)
152		if err != nil {
153			return "", fmt.Errorf("failed to change directory: %v", err)
154		}
155		return cwd, nil
156	}
157	cwd, err := os.Getwd()
158	if err != nil {
159		return "", fmt.Errorf("failed to get current working directory: %v", err)
160	}
161	return cwd, nil
162}