root.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. package cmd
  2. import (
  3. "context"
  4. "fmt"
  5. "io"
  6. "log/slog"
  7. "os"
  8. "strings"
  9. tea "github.com/charmbracelet/bubbletea/v2"
  10. "github.com/charmbracelet/crush/internal/app"
  11. "github.com/charmbracelet/crush/internal/config"
  12. "github.com/charmbracelet/crush/internal/db"
  13. "github.com/charmbracelet/crush/internal/tui"
  14. "github.com/charmbracelet/crush/internal/version"
  15. "github.com/charmbracelet/fang"
  16. "github.com/charmbracelet/x/term"
  17. "github.com/spf13/cobra"
  18. )
  19. func init() {
  20. rootCmd.PersistentFlags().StringP("cwd", "c", "", "Current working directory")
  21. rootCmd.PersistentFlags().BoolP("debug", "d", false, "Debug")
  22. rootCmd.Flags().BoolP("help", "h", false, "Help")
  23. rootCmd.Flags().BoolP("yolo", "y", false, "Automatically accept all permissions (dangerous mode)")
  24. runCmd.Flags().BoolP("quiet", "q", false, "Hide spinner")
  25. rootCmd.AddCommand(runCmd)
  26. }
  27. var rootCmd = &cobra.Command{
  28. Use: "crush",
  29. Short: "Terminal-based AI assistant for software development",
  30. Long: `Crush is a powerful terminal-based AI assistant that helps with software development tasks.
  31. It provides an interactive chat interface with AI capabilities, code analysis, and LSP integration
  32. to assist developers in writing, debugging, and understanding code directly from the terminal.`,
  33. Example: `
  34. # Run in interactive mode
  35. crush
  36. # Run with debug logging
  37. crush -d
  38. # Run with debug logging in a specific directory
  39. crush -d -c /path/to/project
  40. # Print version
  41. crush -v
  42. # Run a single non-interactive prompt
  43. crush run "Explain the use of context in Go"
  44. # Run in dangerous mode (auto-accept all permissions)
  45. crush -y
  46. `,
  47. RunE: func(cmd *cobra.Command, args []string) error {
  48. app, err := setupApp(cmd)
  49. if err != nil {
  50. return err
  51. }
  52. defer app.Shutdown()
  53. // Set up the TUI.
  54. program := tea.NewProgram(
  55. tui.New(app),
  56. tea.WithAltScreen(),
  57. tea.WithContext(cmd.Context()),
  58. tea.WithMouseCellMotion(), // Use cell motion instead of all motion to reduce event flooding
  59. tea.WithFilter(tui.MouseEventFilter), // Filter mouse events based on focus state
  60. )
  61. go app.Subscribe(program)
  62. if _, err := program.Run(); err != nil {
  63. slog.Error("TUI run error", "error", err)
  64. return fmt.Errorf("TUI error: %v", err)
  65. }
  66. return nil
  67. },
  68. }
  69. var runCmd = &cobra.Command{
  70. Use: "run [prompt...]",
  71. Short: "Run a single non-interactive prompt",
  72. Long: `Run a single prompt in non-interactive mode and exit.
  73. The prompt can be provided as arguments or piped from stdin.`,
  74. Example: `
  75. # Run a simple prompt
  76. crush run Explain the use of context in Go
  77. # Pipe input from stdin
  78. echo "What is this code doing?" | crush run
  79. # Run with quiet mode (no spinner)
  80. crush run -q "Generate a README for this project"
  81. `,
  82. RunE: func(cmd *cobra.Command, args []string) error {
  83. quiet, _ := cmd.Flags().GetBool("quiet")
  84. app, err := setupApp(cmd)
  85. if err != nil {
  86. return err
  87. }
  88. defer app.Shutdown()
  89. prompt := strings.Join(args, " ")
  90. prompt, err = maybePrependStdin(prompt)
  91. if err != nil {
  92. slog.Error("Failed to read from stdin", "error", err)
  93. return err
  94. }
  95. if prompt == "" {
  96. return fmt.Errorf("no prompt provided")
  97. }
  98. // Run non-interactive flow using the App method
  99. return app.RunNonInteractive(cmd.Context(), prompt, quiet)
  100. },
  101. }
  102. func Execute() {
  103. if err := fang.Execute(
  104. context.Background(),
  105. rootCmd,
  106. fang.WithVersion(version.Version),
  107. fang.WithNotifySignal(os.Interrupt),
  108. ); err != nil {
  109. os.Exit(1)
  110. }
  111. }
  112. // setupApp handles the common setup logic for both interactive and non-interactive modes.
  113. // It returns the app instance, config, cleanup function, and any error.
  114. func setupApp(cmd *cobra.Command) (*app.App, error) {
  115. debug, _ := cmd.Flags().GetBool("debug")
  116. yolo, _ := cmd.Flags().GetBool("yolo")
  117. ctx := cmd.Context()
  118. cwd, err := resolveCwd(cmd)
  119. if err != nil {
  120. return nil, err
  121. }
  122. cfg, err := config.Init(cwd, debug)
  123. if err != nil {
  124. return nil, err
  125. }
  126. if cfg.Permissions == nil {
  127. cfg.Permissions = &config.Permissions{}
  128. }
  129. cfg.Permissions.SkipRequests = yolo
  130. // Connect to DB; this will also run migrations.
  131. conn, err := db.Connect(ctx, cfg.Options.DataDirectory)
  132. if err != nil {
  133. return nil, err
  134. }
  135. appInstance, err := app.New(ctx, conn, cfg)
  136. if err != nil {
  137. slog.Error("Failed to create app instance", "error", err)
  138. return nil, err
  139. }
  140. return appInstance, nil
  141. }
  142. func maybePrependStdin(prompt string) (string, error) {
  143. if term.IsTerminal(os.Stdin.Fd()) {
  144. return prompt, nil
  145. }
  146. fi, err := os.Stdin.Stat()
  147. if err != nil {
  148. return prompt, err
  149. }
  150. if fi.Mode()&os.ModeNamedPipe == 0 {
  151. return prompt, nil
  152. }
  153. bts, err := io.ReadAll(os.Stdin)
  154. if err != nil {
  155. return prompt, err
  156. }
  157. return string(bts) + "\n\n" + prompt, nil
  158. }
  159. func resolveCwd(cmd *cobra.Command) (string, error) {
  160. cwd, _ := cmd.Flags().GetString("cwd")
  161. if cwd != "" {
  162. err := os.Chdir(cwd)
  163. if err != nil {
  164. return "", fmt.Errorf("failed to change directory: %v", err)
  165. }
  166. return cwd, nil
  167. }
  168. cwd, err := os.Getwd()
  169. if err != nil {
  170. return "", fmt.Errorf("failed to get current working directory: %v", err)
  171. }
  172. return cwd, nil
  173. }