root.go 3.9 KB

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