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