on main help sign in
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

commit53ae5beaf5991933d5b2e07f708fe5fc58dbd815 authorCarlos Alexandro Becker <caarlos0@users.noreply.github.com> dateMon 28 Jul 2025, 17:13 (1 year ago) parentd93ecf5
1 file changed+116−77
internal/cmd/root.go+116−77
internal/cmd/root.go+116−77
@@ -6,6 +6,7 @@ import (
66 "io"
77 "log/slog"
88 "os"
9+ "strings"
910
1011 tea "github.com/charmbracelet/bubbletea/v2"
1112 "github.com/charmbracelet/crush/internal/app"
@@ -18,6 +19,17 @@ import (
1819 "github.com/spf13/cobra"
1920)
2021
22+func init() {
23+ rootCmd.PersistentFlags().StringP("cwd", "c", "", "Current working directory")
24+ rootCmd.PersistentFlags().BoolP("debug", "d", false, "Debug")
25+
26+ rootCmd.Flags().BoolP("help", "h", false, "Help")
27+ rootCmd.Flags().BoolP("yolo", "y", false, "Automatically accept all permissions (dangerous mode)")
28+
29+ runCmd.Flags().BoolP("quiet", "q", false, "Hide spinner")
30+ rootCmd.AddCommand(runCmd)
31+}
32+
2133var rootCmd = &cobra.Command{
2234 Use: "crush",
2335 Short: "Terminal-based AI assistant for software development",
@@ -25,91 +37,36 @@ var rootCmd = &cobra.Command{
2537It provides an interactive chat interface with AI capabilities, code analysis, and LSP integration
2638to assist developers in writing, debugging, and understanding code directly from the terminal.`,
2739 Example: `
28− # Run in interactive mode
29− crush
40+# Run in interactive mode
41+crush
3042
31− # Run with debug logging
32− crush -d
43+# Run with debug logging
44+crush -d
3345
34− # Run with debug slog.in a specific directory
35− crush -d -c /path/to/project
46+# Run with debug logging in a specific directory
47+crush -d -c /path/to/project
3648
37− # Print version
38− crush -v
49+# Print version
50+crush -v
3951
40− # Run a single non-interactive prompt
41− crush -p "Explain the use of context in Go"
52+# Run a single non-interactive prompt
53+crush run "Explain the use of context in Go"
4254
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
55+# Run in dangerous mode (auto-accept all permissions)
56+crush -y
4857 `,
4958 RunE: func(cmd *cobra.Command, args []string) error {
50− // Load the config
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")
57−
58− if cwd != "" {
59− err := os.Chdir(cwd)
60− if err != nil {
61− return fmt.Errorf("failed to change directory: %v", err)
62− }
63− }
64− if cwd == "" {
65− c, err := os.Getwd()
66− if err != nil {
67− return fmt.Errorf("failed to get current working directory: %v", err)
68− }
69− cwd = c
70− }
71−
72− cfg, err := config.Init(cwd, debug)
73− if err != nil {
74− return err
75− }
76− if cfg.Permissions == nil {
77− cfg.Permissions = &config.Permissions{}
78− }
79− cfg.Permissions.SkipRequests = yolo
80−
81− ctx := cmd.Context()
82−
83− // Connect to DB; this will also run migrations.
84− conn, err := db.Connect(ctx, cfg.Options.DataDirectory)
59+ app, err := setupApp(cmd)
8560 if err != nil {
8661 return err
8762 }
88−
89− app, err := app.New(ctx, conn, cfg)
90− if err != nil {
91− slog.Error("Failed to create app instance", "error", err)
92− return err
93− }
9463 defer app.Shutdown()
9564
96− prompt, err = maybePrependStdin(prompt)
97− if err != nil {
98− slog.Error("Failed to read from stdin", "error", err)
99− return err
100− }
101−
102− // Non-interactive mode.
103− if prompt != "" {
104− // Run non-interactive flow using the App method
105− return app.RunNonInteractive(ctx, prompt, quiet)
106− }
107−
10865 // Set up the TUI.
10966 program := tea.NewProgram(
11067 tui.New(app),
11168 tea.WithAltScreen(),
112− tea.WithContext(ctx),
69+ tea.WithContext(cmd.Context()),
11370 tea.WithMouseCellMotion(), // Use cell motion instead of all motion to reduce event flooding
11471 tea.WithFilter(tui.MouseEventFilter), // Filter mouse events based on focus state
11572 )
@@ -124,6 +81,47 @@ to assist developers in writing, debugging, and understanding code directly from
12481 },
12582}
12683
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.`,
89+ Example: `
90+# Run a simple prompt
91+crush run Explain the use of context in Go
92+
93+# Pipe input from stdin
94+echo "What is this code doing?" | crush run
95+
96+# Run with quiet mode (no spinner)
97+crush run -q "Generate a README for this project"
98+ `,
99+ RunE: func(cmd *cobra.Command, args []string) error {
100+ quiet, _ := cmd.Flags().GetBool("quiet")
101+
102+ app, err := setupApp(cmd)
103+ if err != nil {
104+ return err
105+ }
106+ defer app.Shutdown()
107+
108+ prompt := strings.Join(args, " ")
109+
110+ prompt, err = maybePrependStdin(prompt)
111+ if err != nil {
112+ slog.Error("Failed to read from stdin", "error", err)
113+ return err
114+ }
115+
116+ if prompt == "" {
117+ return fmt.Errorf("no prompt provided")
118+ }
119+
120+ // Run non-interactive flow using the App method
121+ return app.RunNonInteractive(cmd.Context(), prompt, quiet)
122+ },
123+}
124+
127125func Execute() {
128126 if err := fang.Execute(
129127 context.Background(),
@@ -135,16 +133,41 @@ func Execute() {
135133 }
136134}
137135
138−func init() {
139− rootCmd.PersistentFlags().StringP("cwd", "c", "", "Current working directory")
136+// setupApp handles the common setup logic for both interactive and non-interactive modes.
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()
140142
141− rootCmd.Flags().BoolP("help", "h", false, "Help")
142− rootCmd.Flags().BoolP("debug", "d", false, "Debug")
143− rootCmd.Flags().StringP("prompt", "p", "", "Prompt to run in non-interactive mode")
144− rootCmd.Flags().BoolP("yolo", "y", false, "Automatically accept all permissions (dangerous mode)")
143+ cwd, err := resolveCwd(cmd)
144+ if err != nil {
145+ return nil, err
146+ }
147+
148+ cfg, err := config.Init(cwd, debug)
149+ if err != nil {
150+ return nil, err
151+ }
152+
153+ if cfg.Permissions == nil {
154+ cfg.Permissions = &config.Permissions{}
155+ }
156+ cfg.Permissions.SkipRequests = yolo
157+
158+ // Connect to DB; this will also run migrations.
159+ conn, err := db.Connect(ctx, cfg.Options.DataDirectory)
160+ if err != nil {
161+ return nil, err
162+ }
145163
146− // Add quiet flag to hide spinner in non-interactive mode
147− rootCmd.Flags().BoolP("quiet", "q", false, "Hide spinner in non-interactive mode")
164+ appInstance, err := app.New(ctx, conn, cfg)
165+ if err != nil {
166+ slog.Error("Failed to create app instance", "error", err)
167+ return nil, err
168+ }
169+
170+ return appInstance, nil
148171}
149172
150173func maybePrependStdin(prompt string) (string, error) {
@@ -164,3 +187,19 @@ func maybePrependStdin(prompt string) (string, error) {
164187 }
165188 return string(bts) + "\n\n" + prompt, nil
166189}
190+
191+func resolveCwd(cmd *cobra.Command) (string, error) {
192+ cwd, _ := cmd.Flags().GetString("cwd")
193+ if cwd != "" {
194+ err := os.Chdir(cwd)
195+ if err != nil {
196+ return "", fmt.Errorf("failed to change directory: %v", err)
197+ }
198+ return cwd, nil
199+ }
200+ cwd, err := os.Getwd()
201+ if err != nil {
202+ return "", fmt.Errorf("failed to get current working directory: %v", err)
203+ }
204+ return cwd, nil
205+}