root.go 4.7 KB

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