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