root.go

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