on main help sign in
feat: crush run (#322)
53ae5beCarlos Alexandro Becker
internal/cmd/root.go+116−77
@@ -6,6 +6,7 @@ import (
6 "io"
6 "io"
7 "log/slog"
7 "log/slog"
8 "os"
8 "os"
9+ "strings"
9
10
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"
19)
20)
20
21
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+
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
29− crush
41+crush
30
42
31− # Run with debug logging
43+# Run with debug logging
32− crush -d
44+crush -d
33
45
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
36
48
37− # Print version
49+# Print version
38− crush -v
50+crush -v
39
51
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"
42
54
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)
55+# Run in dangerous mode (auto-accept all permissions)
47− crush -y
56+crush -y
48 `,
57 `,
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")
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)
85 if err != nil {
60 if err != nil {
86 return err
61 return err
87 }
62 }
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− }
94 defer app.Shutdown()
63 defer app.Shutdown()
95
64
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−
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
115 )
72 )
@@ -124,6 +81,47 @@ to assist developers in writing, debugging, and understanding code directly from
124 },
81 },
125}
82}
126
83
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+
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() {
135 }
133 }
136}
134}
137
135
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()
140
142
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+ }
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+ }
145
163
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)
167+ return nil, err
168+ }
169+
170+ return appInstance, nil
148}
171}
149
172
150func maybePrependStdin(prompt string) (string, error) {
173func maybePrependStdin(prompt string) (string, error) {
@@ -164,3 +187,19 @@ func maybePrependStdin(prompt string) (string, error) {
164 }
187 }
165 return string(bts) + "\n\n" + prompt, nil
188 return string(bts) + "\n\n" + prompt, nil
166}
189}
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+}