root.go 4.5 KB

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