15 tea "charm.land/bubbletea/v2"
16 "charm.land/lipgloss/v2"
17 "github.com/charmbracelet/colorprofile"
18 "github.com/charmbracelet/crush/internal/app"
19 "github.com/charmbracelet/crush/internal/config"
20 "github.com/charmbracelet/crush/internal/db"
21 "github.com/charmbracelet/crush/internal/event"
22 "github.com/charmbracelet/crush/internal/stringext"
23 termutil "github.com/charmbracelet/crush/internal/term"
24 "github.com/charmbracelet/crush/internal/tui"
25 "github.com/charmbracelet/crush/internal/ui/common"
26 ui "github.com/charmbracelet/crush/internal/ui/model"
27 "github.com/charmbracelet/crush/internal/version"
28 "github.com/charmbracelet/fang"
29 uv "github.com/charmbracelet/ultraviolet"
30 "github.com/charmbracelet/x/ansi"
31 "github.com/charmbracelet/x/exp/charmtone"
32 "github.com/charmbracelet/x/term"
33 "github.com/spf13/cobra"
37 rootCmd.PersistentFlags().StringP("cwd", "c", "", "Current working directory")
38 rootCmd.PersistentFlags().StringP("data-dir", "D", "", "Custom crush data directory")
39 rootCmd.PersistentFlags().BoolP("debug", "d", false, "Debug")
40 rootCmd.Flags().BoolP("help", "h", false, "Help")
41 rootCmd.Flags().BoolP("yolo", "y", false, "Automatically accept all permissions (dangerous mode)")
53var rootCmd = &cobra.Command{
55 Short: "Terminal-based AI assistant for software development",
56 Long: `Crush is a powerful terminal-based AI assistant that helps with software development tasks.
57It provides an interactive chat interface with AI capabilities, code analysis, and LSP integration
58to assist developers in writing, debugging, and understanding code directly from the terminal.`,
60# Run in interactive mode
63# Run with debug logging
66# Run with debug logging in a specific directory
67crush -d -c /path/to/project
69# Run with custom data directory
70crush -D /path/to/custom/.crush
75# Run a single non-interactive prompt
76crush run "Explain the use of context in Go"
78# Run in dangerous mode (auto-accept all permissions)
81 RunE: func(cmd *cobra.Command, args []string) error {
82 app, err := setupAppWithProgressBar(cmd)
88 event.AppInitialized()
91 var env uv.Environ = os.Environ()
92 com := common.DefaultCommon(app)
94 ui.QueryVersion = shouldQueryTerminalVersion(env)
95 program := tea.NewProgram(
97 tea.WithEnvironment(env),
98 tea.WithContext(cmd.Context()),
99 tea.WithFilter(tui.MouseEventFilter)) // Filter mouse events based on focus state
100 go app.Subscribe(program)
102 if _, err := program.Run(); err != nil {
104 slog.Error("TUI run error", "error", err)
105 return errors.New("Crush crashed. If metrics are enabled, we were notified about it. If you'd like to report it, please copy the stacktrace above and open an issue at https://github.com/charmbracelet/crush/issues/new?template=bug.yml") //nolint:staticcheck
109 PostRun: func(cmd *cobra.Command, args []string) {
114var heartbit = lipgloss.NewStyle().Foreground(charmtone.Dolly).SetString(`
116 ███████████ ███████████
117████████████████████████████
118████████████████████████████
119██████████▀██████▀██████████
120██████████ ██████ ██████████
121▀▀██████▄████▄▄████▄██████▀▀
122 ████████████████████████
129const defaultVersionTemplate = `{{with .DisplayName}}{{printf "%s " .}}{{end}}{{printf "version %s" .Version}}
133 // NOTE: very hacky: we create a colorprofile writer with STDOUT, then make
134 // it forward to a bytes.Buffer, write the colored heartbit to it, and then
135 // finally prepend it in the version template.
136 // Unfortunately cobra doesn't give us a way to set a function to handle
137 // printing the version, and PreRunE runs after the version is already
138 // handled, so that doesn't work either.
139 // This is the only way I could find that works relatively well.
140 if term.IsTerminal(os.Stdout.Fd()) {
142 w := colorprofile.NewWriter(os.Stdout, os.Environ())
144 _, _ = w.WriteString(heartbit.String())
145 rootCmd.SetVersionTemplate(b.String() + "\n" + defaultVersionTemplate)
147 if err := fang.Execute(
148 context.Background(),
150 fang.WithVersion(version.Version),
151 fang.WithNotifySignal(os.Interrupt),
157func setupAppWithProgressBar(cmd *cobra.Command) (*app.App, error) {
158 if termutil.SupportsProgressBar() {
159 _, _ = fmt.Fprintf(os.Stderr, ansi.SetIndeterminateProgressBar)
160 defer func() { _, _ = fmt.Fprintf(os.Stderr, ansi.ResetProgressBar) }()
166// setupApp handles the common setup logic for both interactive and non-interactive modes.
167// It returns the app instance, config, cleanup function, and any error.
168func setupApp(cmd *cobra.Command) (*app.App, error) {
169 debug, _ := cmd.Flags().GetBool("debug")
170 yolo, _ := cmd.Flags().GetBool("yolo")
171 dataDir, _ := cmd.Flags().GetString("data-dir")
174 cwd, err := ResolveCwd(cmd)
179 cfg, err := config.Init(cwd, dataDir, debug)
184 if cfg.Permissions == nil {
185 cfg.Permissions = &config.Permissions{}
187 cfg.Permissions.SkipRequests = yolo
189 if err := createDotCrushDir(cfg.Options.DataDirectory); err != nil {
193 // Connect to DB; this will also run migrations.
194 conn, err := db.Connect(ctx, cfg.Options.DataDirectory)
199 appInstance, err := app.New(ctx, conn, cfg)
201 slog.Error("Failed to create app instance", "error", err)
205 if shouldEnableMetrics() {
209 return appInstance, nil
212func shouldEnableMetrics() bool {
213 if v, _ := strconv.ParseBool(os.Getenv("CRUSH_DISABLE_METRICS")); v {
216 if v, _ := strconv.ParseBool(os.Getenv("DO_NOT_TRACK")); v {
219 if config.Get().Options.DisableMetrics {
225func MaybePrependStdin(prompt string) (string, error) {
226 if term.IsTerminal(os.Stdin.Fd()) {
229 fi, err := os.Stdin.Stat()
233 if fi.Mode()&os.ModeNamedPipe == 0 {
236 bts, err := io.ReadAll(os.Stdin)
240 return string(bts) + "\n\n" + prompt, nil
243func ResolveCwd(cmd *cobra.Command) (string, error) {
244 cwd, _ := cmd.Flags().GetString("cwd")
248 return "", fmt.Errorf("failed to change directory: %v", err)
252 cwd, err := os.Getwd()
254 return "", fmt.Errorf("failed to get current working directory: %v", err)
259func createDotCrushDir(dir string) error {
260 if err := os.MkdirAll(dir, 0o700); err != nil {
261 return fmt.Errorf("failed to create data directory: %q %w", dir, err)
264 gitIgnorePath := filepath.Join(dir, ".gitignore")
265 if _, err := os.Stat(gitIgnorePath); os.IsNotExist(err) {
266 if err := os.WriteFile(gitIgnorePath, []byte("*\n"), 0o644); err != nil {
267 return fmt.Errorf("failed to create .gitignore file: %q %w", gitIgnorePath, err)
274func shouldQueryTerminalVersion(env uv.Environ) bool {
275 termType := env.Getenv("TERM")
276 termProg, okTermProg := env.LookupEnv("TERM_PROGRAM")
277 _, okSSHTTY := env.LookupEnv("SSH_TTY")
278 return (!okTermProg && !okSSHTTY) ||
279 (!strings.Contains(termProg, "Apple") && !okSSHTTY) ||
280 // Terminals that do support XTVERSION.
281 stringext.ContainsAny(termType, "alacritty", "ghostty", "kitty", "rio", "wezterm")
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list