feat: crush run (#322)

* feat: crush run

Proposal: I think it's a lot nicer to be able to type
`crush run my prompt` vs `crush -p "my prompt"`.

This PR implements that.

Let me know what you think.

* chore: move stuff around a bit
This commit is contained in:
Carlos Alexandro Becker
2025-07-28 17:13:34 -03:00
committed by GitHub
parent d93ecf5402
commit 53ae5beaf5

View File

@@ -6,6 +6,7 @@ import (
"io"
"log/slog"
"os"
"strings"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/crush/internal/app"
@@ -18,6 +19,17 @@ import (
"github.com/spf13/cobra"
)
func init() {
rootCmd.PersistentFlags().StringP("cwd", "c", "", "Current working directory")
rootCmd.PersistentFlags().BoolP("debug", "d", false, "Debug")
rootCmd.Flags().BoolP("help", "h", false, "Help")
rootCmd.Flags().BoolP("yolo", "y", false, "Automatically accept all permissions (dangerous mode)")
runCmd.Flags().BoolP("quiet", "q", false, "Hide spinner")
rootCmd.AddCommand(runCmd)
}
var rootCmd = &cobra.Command{
Use: "crush",
Short: "Terminal-based AI assistant for software development",
@@ -25,91 +37,36 @@ var rootCmd = &cobra.Command{
It provides an interactive chat interface with AI capabilities, code analysis, and LSP integration
to assist developers in writing, debugging, and understanding code directly from the terminal.`,
Example: `
# Run in interactive mode
crush
# Run in interactive mode
crush
# Run with debug logging
crush -d
# Run with debug logging
crush -d
# Run with debug slog.in a specific directory
crush -d -c /path/to/project
# Run with debug logging in a specific directory
crush -d -c /path/to/project
# Print version
crush -v
# Print version
crush -v
# Run a single non-interactive prompt
crush -p "Explain the use of context in Go"
# Run a single non-interactive prompt
crush run "Explain the use of context in Go"
# Run a single non-interactive prompt with JSON output format
crush -p "Explain the use of context in Go" -f json
# Run in dangerous mode (auto-accept all permissions)
crush -y
# Run in dangerous mode (auto-accept all permissions)
crush -y
`,
RunE: func(cmd *cobra.Command, args []string) error {
// Load the config
// XXX: Handle errors.
debug, _ := cmd.Flags().GetBool("debug")
cwd, _ := cmd.Flags().GetString("cwd")
prompt, _ := cmd.Flags().GetString("prompt")
quiet, _ := cmd.Flags().GetBool("quiet")
yolo, _ := cmd.Flags().GetBool("yolo")
if cwd != "" {
err := os.Chdir(cwd)
if err != nil {
return fmt.Errorf("failed to change directory: %v", err)
}
}
if cwd == "" {
c, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get current working directory: %v", err)
}
cwd = c
}
cfg, err := config.Init(cwd, debug)
app, err := setupApp(cmd)
if err != nil {
return err
}
if cfg.Permissions == nil {
cfg.Permissions = &config.Permissions{}
}
cfg.Permissions.SkipRequests = yolo
ctx := cmd.Context()
// Connect to DB; this will also run migrations.
conn, err := db.Connect(ctx, cfg.Options.DataDirectory)
if err != nil {
return err
}
app, err := app.New(ctx, conn, cfg)
if err != nil {
slog.Error("Failed to create app instance", "error", err)
return err
}
defer app.Shutdown()
prompt, err = maybePrependStdin(prompt)
if err != nil {
slog.Error("Failed to read from stdin", "error", err)
return err
}
// Non-interactive mode.
if prompt != "" {
// Run non-interactive flow using the App method
return app.RunNonInteractive(ctx, prompt, quiet)
}
// Set up the TUI.
program := tea.NewProgram(
tui.New(app),
tea.WithAltScreen(),
tea.WithContext(ctx),
tea.WithContext(cmd.Context()),
tea.WithMouseCellMotion(), // Use cell motion instead of all motion to reduce event flooding
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
},
}
var runCmd = &cobra.Command{
Use: "run [prompt...]",
Short: "Run a single non-interactive prompt",
Long: `Run a single prompt in non-interactive mode and exit.
The prompt can be provided as arguments or piped from stdin.`,
Example: `
# Run a simple prompt
crush run Explain the use of context in Go
# Pipe input from stdin
echo "What is this code doing?" | crush run
# Run with quiet mode (no spinner)
crush run -q "Generate a README for this project"
`,
RunE: func(cmd *cobra.Command, args []string) error {
quiet, _ := cmd.Flags().GetBool("quiet")
app, err := setupApp(cmd)
if err != nil {
return err
}
defer app.Shutdown()
prompt := strings.Join(args, " ")
prompt, err = maybePrependStdin(prompt)
if err != nil {
slog.Error("Failed to read from stdin", "error", err)
return err
}
if prompt == "" {
return fmt.Errorf("no prompt provided")
}
// Run non-interactive flow using the App method
return app.RunNonInteractive(cmd.Context(), prompt, quiet)
},
}
func Execute() {
if err := fang.Execute(
context.Background(),
@@ -135,16 +133,41 @@ func Execute() {
}
}
func init() {
rootCmd.PersistentFlags().StringP("cwd", "c", "", "Current working directory")
// setupApp handles the common setup logic for both interactive and non-interactive modes.
// It returns the app instance, config, cleanup function, and any error.
func setupApp(cmd *cobra.Command) (*app.App, error) {
debug, _ := cmd.Flags().GetBool("debug")
yolo, _ := cmd.Flags().GetBool("yolo")
ctx := cmd.Context()
rootCmd.Flags().BoolP("help", "h", false, "Help")
rootCmd.Flags().BoolP("debug", "d", false, "Debug")
rootCmd.Flags().StringP("prompt", "p", "", "Prompt to run in non-interactive mode")
rootCmd.Flags().BoolP("yolo", "y", false, "Automatically accept all permissions (dangerous mode)")
cwd, err := resolveCwd(cmd)
if err != nil {
return nil, err
}
// Add quiet flag to hide spinner in non-interactive mode
rootCmd.Flags().BoolP("quiet", "q", false, "Hide spinner in non-interactive mode")
cfg, err := config.Init(cwd, debug)
if err != nil {
return nil, err
}
if cfg.Permissions == nil {
cfg.Permissions = &config.Permissions{}
}
cfg.Permissions.SkipRequests = yolo
// Connect to DB; this will also run migrations.
conn, err := db.Connect(ctx, cfg.Options.DataDirectory)
if err != nil {
return nil, err
}
appInstance, err := app.New(ctx, conn, cfg)
if err != nil {
slog.Error("Failed to create app instance", "error", err)
return nil, err
}
return appInstance, nil
}
func maybePrependStdin(prompt string) (string, error) {
@@ -164,3 +187,19 @@ func maybePrependStdin(prompt string) (string, error) {
}
return string(bts) + "\n\n" + prompt, nil
}
func resolveCwd(cmd *cobra.Command) (string, error) {
cwd, _ := cmd.Flags().GetString("cwd")
if cwd != "" {
err := os.Chdir(cwd)
if err != nil {
return "", fmt.Errorf("failed to change directory: %v", err)
}
return cwd, nil
}
cwd, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("failed to get current working directory: %v", err)
}
return cwd, nil
}