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