1package cmd
2
3import (
4 "context"
5 "fmt"
6 "log/slog"
7 "os"
8 "os/signal"
9 "strings"
10
11 "charm.land/log/v2"
12 "github.com/charmbracelet/crush/internal/event"
13 "github.com/spf13/cobra"
14)
15
16var runCmd = &cobra.Command{
17 Aliases: []string{"r"},
18 Use: "run [prompt...]",
19 Short: "Run a single non-interactive prompt",
20 Long: `Run a single prompt in non-interactive mode and exit.
21The prompt can be provided as arguments or piped from stdin.`,
22 Example: `
23# Run a simple prompt
24crush run "Guess my 5 favorite Pokรฉmon"
25
26# Pipe input from stdin
27curl https://charm.land | crush run "Summarize this website"
28
29# Read from a file
30crush run "What is this code doing?" <<< prrr.go
31
32# Redirect output to a file
33crush run "Generate a hot README for this project" > MY_HOT_README.md
34
35# Run in quiet mode (hide the spinner)
36crush run --quiet "Generate a README for this project"
37
38# Run in verbose mode (show logs)
39crush run --verbose "Generate a README for this project"
40
41# Continue a previous session
42crush run --session {session-id} "Follow up on your last response"
43
44# Continue the most recent session
45crush run --continue "Follow up on your last response"
46
47 `,
48 RunE: func(cmd *cobra.Command, args []string) error {
49 var (
50 quiet, _ = cmd.Flags().GetBool("quiet")
51 verbose, _ = cmd.Flags().GetBool("verbose")
52 largeModel, _ = cmd.Flags().GetString("model")
53 smallModel, _ = cmd.Flags().GetString("small-model")
54 sessionID, _ = cmd.Flags().GetString("session")
55 useLast, _ = cmd.Flags().GetBool("continue")
56 )
57
58 // Cancel on SIGINT or SIGTERM.
59 ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
60 defer cancel()
61
62 app, err := setupApp(cmd)
63 if err != nil {
64 return err
65 }
66 defer app.Shutdown()
67
68 if sessionID != "" {
69 sess, err := resolveSessionID(ctx, app.Sessions, sessionID)
70 if err != nil {
71 return err
72 }
73 sessionID = sess.ID
74 }
75
76 if !app.Config().IsConfigured() {
77 return fmt.Errorf("no providers configured - please run 'crush' to set up a provider interactively")
78 }
79
80 if verbose {
81 slog.SetDefault(slog.New(log.New(os.Stderr)))
82 }
83
84 prompt := strings.Join(args, " ")
85
86 prompt, err = MaybePrependStdin(prompt)
87 if err != nil {
88 slog.Error("Failed to read from stdin", "error", err)
89 return err
90 }
91
92 if prompt == "" {
93 return fmt.Errorf("no prompt provided")
94 }
95
96 event.SetNonInteractive(true)
97 event.AppInitialized()
98
99 switch {
100 case sessionID != "":
101 event.SetContinueBySessionID(true)
102 case useLast:
103 event.SetContinueLastSession(true)
104 }
105
106 return app.RunNonInteractive(ctx, os.Stdout, prompt, largeModel, smallModel, quiet || verbose, sessionID, useLast)
107 },
108}
109
110func init() {
111 runCmd.Flags().BoolP("quiet", "q", false, "Hide spinner")
112 runCmd.Flags().BoolP("verbose", "v", false, "Show logs")
113 runCmd.Flags().StringP("model", "m", "", "Model to use. Accepts 'model' or 'provider/model' to disambiguate models with the same name across providers")
114 runCmd.Flags().String("small-model", "", "Small model to use. If not provided, uses the default small model for the provider")
115 runCmd.Flags().StringP("session", "s", "", "Continue a previous session by ID")
116 runCmd.Flags().BoolP("continue", "C", false, "Continue the most recent session")
117 runCmd.MarkFlagsMutuallyExclusive("session", "continue")
118}