@@ -6,6 +6,7 @@ import (
10 tea "github.com/charmbracelet/bubbletea/v2"
11 tea "github.com/charmbracelet/bubbletea/v2"
11 "github.com/charmbracelet/crush/internal/app"
12 "github.com/charmbracelet/crush/internal/app"
@@ -18,6 +19,17 @@ import (
18 "github.com/spf13/cobra"
19 "github.com/spf13/cobra"
23+ rootCmd.PersistentFlags().StringP("cwd", "c", "", "Current working directory")
24+ rootCmd.PersistentFlags().BoolP("debug", "d", false, "Debug")
26+ rootCmd.Flags().BoolP("help", "h", false, "Help")
27+ rootCmd.Flags().BoolP("yolo", "y", false, "Automatically accept all permissions (dangerous mode)")
29+ runCmd.Flags().BoolP("quiet", "q", false, "Hide spinner")
30+ rootCmd.AddCommand(runCmd)
21var rootCmd = &cobra.Command{
33var rootCmd = &cobra.Command{
22 Use: "crush",
34 Use: "crush",
23 Short: "Terminal-based AI assistant for software development",
35 Short: "Terminal-based AI assistant for software development",
@@ -25,91 +37,36 @@ var rootCmd = &cobra.Command{
25It provides an interactive chat interface with AI capabilities, code analysis, and LSP integration
37It 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.`,
38to assist developers in writing, debugging, and understanding code directly from the terminal.`,
27 Example: `
39 Example: `
28− # Run in interactive mode
40+# Run in interactive mode
31− # Run with debug logging
43+# Run with debug logging
34− # Run with debug slog.in a specific directory
46+# Run with debug logging in a specific directory
35− crush -d -c /path/to/project
47+crush -d -c /path/to/project
37− # Print version
49+# Print version
40− # Run a single non-interactive prompt
52+# Run a single non-interactive prompt
41− crush -p "Explain the use of context in Go"
53+crush run "Explain the use of context in Go"
43− # Run a single non-interactive prompt with JSON output format
44− crush -p "Explain the use of context in Go" -f json
46− # Run in dangerous mode (auto-accept all permissions)
55+# Run in dangerous mode (auto-accept all permissions)
49 RunE: func(cmd *cobra.Command, args []string) error {
58 RunE: func(cmd *cobra.Command, args []string) error {
50− // Load the config
59+ app, err := setupApp(cmd)
51− // XXX: Handle errors.
52− debug, _ := cmd.Flags().GetBool("debug")
53− cwd, _ := cmd.Flags().GetString("cwd")
54− prompt, _ := cmd.Flags().GetString("prompt")
55− quiet, _ := cmd.Flags().GetBool("quiet")
56− yolo, _ := cmd.Flags().GetBool("yolo")
61− return fmt.Errorf("failed to change directory: %v", err)
67− return fmt.Errorf("failed to get current working directory: %v", err)
72− cfg, err := config.Init(cwd, debug)
76− if cfg.Permissions == nil {
77− cfg.Permissions = &config.Permissions{}
79− cfg.Permissions.SkipRequests = yolo
83− // Connect to DB; this will also run migrations.
84− conn, err := db.Connect(ctx, cfg.Options.DataDirectory)
85 if err != nil {
60 if err != nil {
86 return err
61 return err
89− app, err := app.New(ctx, conn, cfg)
91− slog.Error("Failed to create app instance", "error", err)
94 defer app.Shutdown()
63 defer app.Shutdown()
96− prompt, err = maybePrependStdin(prompt)
98− slog.Error("Failed to read from stdin", "error", err)
102− // Non-interactive mode.
104− // Run non-interactive flow using the App method
105− return app.RunNonInteractive(ctx, prompt, quiet)
108 // Set up the TUI.
65 // Set up the TUI.
109 program := tea.NewProgram(
66 program := tea.NewProgram(
110 tui.New(app),
67 tui.New(app),
111 tea.WithAltScreen(),
68 tea.WithAltScreen(),
112− tea.WithContext(ctx),
69+ tea.WithContext(cmd.Context()),
113 tea.WithMouseCellMotion(), // Use cell motion instead of all motion to reduce event flooding
70 tea.WithMouseCellMotion(), // Use cell motion instead of all motion to reduce event flooding
114 tea.WithFilter(tui.MouseEventFilter), // Filter mouse events based on focus state
71 tea.WithFilter(tui.MouseEventFilter), // Filter mouse events based on focus state
@@ -124,6 +81,47 @@ to assist developers in writing, debugging, and understanding code directly from
84+var runCmd = &cobra.Command{
85+ Use: "run [prompt...]",
86+ Short: "Run a single non-interactive prompt",
87+ Long: `Run a single prompt in non-interactive mode and exit.
88+The prompt can be provided as arguments or piped from stdin.`,
91+crush run Explain the use of context in Go
93+# Pipe input from stdin
94+echo "What is this code doing?" | crush run
96+# Run with quiet mode (no spinner)
97+crush run -q "Generate a README for this project"
99+ RunE: func(cmd *cobra.Command, args []string) error {
100+ quiet, _ := cmd.Flags().GetBool("quiet")
102+ app, err := setupApp(cmd)
106+ defer app.Shutdown()
108+ prompt := strings.Join(args, " ")
110+ prompt, err = maybePrependStdin(prompt)
112+ slog.Error("Failed to read from stdin", "error", err)
117+ return fmt.Errorf("no prompt provided")
120+ // Run non-interactive flow using the App method
121+ return app.RunNonInteractive(cmd.Context(), prompt, quiet)
127func Execute() {
125func Execute() {
128 if err := fang.Execute(
126 if err := fang.Execute(
129 context.Background(),
127 context.Background(),
@@ -135,16 +133,41 @@ func Execute() {
138−func init() {
136+// setupApp handles the common setup logic for both interactive and non-interactive modes.
139− rootCmd.PersistentFlags().StringP("cwd", "c", "", "Current working directory")
137+// It returns the app instance, config, cleanup function, and any error.
138+func setupApp(cmd *cobra.Command) (*app.App, error) {
139+ debug, _ := cmd.Flags().GetBool("debug")
140+ yolo, _ := cmd.Flags().GetBool("yolo")
141+ ctx := cmd.Context()
141− rootCmd.Flags().BoolP("help", "h", false, "Help")
143+ cwd, err := resolveCwd(cmd)
142− rootCmd.Flags().BoolP("debug", "d", false, "Debug")
144+ if err != nil {
143− rootCmd.Flags().StringP("prompt", "p", "", "Prompt to run in non-interactive mode")
145+ return nil, err
144− rootCmd.Flags().BoolP("yolo", "y", false, "Automatically accept all permissions (dangerous mode)")
146+ }
148+ cfg, err := config.Init(cwd, debug)
153+ if cfg.Permissions == nil {
154+ cfg.Permissions = &config.Permissions{}
156+ cfg.Permissions.SkipRequests = yolo
158+ // Connect to DB; this will also run migrations.
159+ conn, err := db.Connect(ctx, cfg.Options.DataDirectory)
146− // Add quiet flag to hide spinner in non-interactive mode
164+ appInstance, err := app.New(ctx, conn, cfg)
147− rootCmd.Flags().BoolP("quiet", "q", false, "Hide spinner in non-interactive mode")
165+ if err != nil {
166+ slog.Error("Failed to create app instance", "error", err)
170+ return appInstance, nil
150func maybePrependStdin(prompt string) (string, error) {
173func maybePrependStdin(prompt string) (string, error) {
@@ -164,3 +187,19 @@ func maybePrependStdin(prompt string) (string, error) {
165 return string(bts) + "\n\n" + prompt, nil
188 return string(bts) + "\n\n" + prompt, nil
191+func resolveCwd(cmd *cobra.Command) (string, error) {
192+ cwd, _ := cmd.Flags().GetString("cwd")
194+ err := os.Chdir(cwd)
196+ return "", fmt.Errorf("failed to change directory: %v", err)
200+ cwd, err := os.Getwd()
202+ return "", fmt.Errorf("failed to get current working directory: %v", err)