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/projects"
23 "github.com/charmbracelet/crush/internal/ui/common"
24 ui "github.com/charmbracelet/crush/internal/ui/model"
25 "github.com/charmbracelet/crush/internal/version"
26 "github.com/charmbracelet/fang"
27 uv "github.com/charmbracelet/ultraviolet"
28 "github.com/charmbracelet/x/ansi"
29 "github.com/charmbracelet/x/exp/charmtone"
30 "github.com/charmbracelet/x/term"
31 "github.com/spf13/cobra"
35 rootCmd.PersistentFlags().StringP("cwd", "c", "", "Current working directory")
36 rootCmd.PersistentFlags().StringP("data-dir", "D", "", "Custom crush data directory")
37 rootCmd.PersistentFlags().BoolP("debug", "d", false, "Debug")
38 rootCmd.Flags().BoolP("help", "h", false, "Help")
39 rootCmd.Flags().BoolP("yolo", "y", false, "Automatically accept all permissions (dangerous mode)")
53var rootCmd = &cobra.Command{
55 Short: "An AI assistant for software development",
56 Long: "An AI assistant for software development and similar tasks with direct access to the terminal",
58# Run in interactive mode
61# Run with debug logging
64# Run with debug logging in a specific directory
65crush -d -c /path/to/project
67# Run with custom data directory
68crush -D /path/to/custom/.crush
73# Run a single non-interactive prompt
74crush run "Explain the use of context in Go"
76# Run in dangerous mode (auto-accept all permissions)
79 RunE: func(cmd *cobra.Command, args []string) error {
80 app, err := setupAppWithProgressBar(cmd)
86 event.AppInitialized()
89 var env uv.Environ = os.Environ()
91 com := common.DefaultCommon(app)
94 program := tea.NewProgram(
96 tea.WithEnvironment(env),
97 tea.WithContext(cmd.Context()),
98 tea.WithFilter(ui.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
111var heartbit = lipgloss.NewStyle().Foreground(charmtone.Dolly).SetString(`
113 ███████████ ███████████
114████████████████████████████
115████████████████████████████
116██████████▀██████▀██████████
117██████████ ██████ ██████████
118▀▀██████▄████▄▄████▄██████▀▀
119 ████████████████████████
126const defaultVersionTemplate = `{{with .DisplayName}}{{printf "%s " .}}{{end}}{{printf "version %s" .Version}}
130 // NOTE: very hacky: we create a colorprofile writer with STDOUT, then make
131 // it forward to a bytes.Buffer, write the colored heartbit to it, and then
132 // finally prepend it in the version template.
133 // Unfortunately cobra doesn't give us a way to set a function to handle
134 // printing the version, and PreRunE runs after the version is already
135 // handled, so that doesn't work either.
136 // This is the only way I could find that works relatively well.
137 if term.IsTerminal(os.Stdout.Fd()) {
139 w := colorprofile.NewWriter(os.Stdout, os.Environ())
141 _, _ = w.WriteString(heartbit.String())
142 rootCmd.SetVersionTemplate(b.String() + "\n" + defaultVersionTemplate)
144 if err := fang.Execute(
145 context.Background(),
147 fang.WithVersion(version.Version),
148 fang.WithNotifySignal(os.Interrupt),
154// supportsProgressBar tries to determine whether the current terminal supports
155// progress bars by looking into environment variables.
156func supportsProgressBar() bool {
157 if !term.IsTerminal(os.Stderr.Fd()) {
160 termProg := os.Getenv("TERM_PROGRAM")
161 _, isWindowsTerminal := os.LookupEnv("WT_SESSION")
163 return isWindowsTerminal || strings.Contains(strings.ToLower(termProg), "ghostty")
166func setupAppWithProgressBar(cmd *cobra.Command) (*app.App, error) {
167 app, err := setupApp(cmd)
172 // Check if progress bar is enabled in config (defaults to true if nil)
173 progressEnabled := app.Config().Options.Progress == nil || *app.Config().Options.Progress
174 if progressEnabled && supportsProgressBar() {
175 _, _ = fmt.Fprintf(os.Stderr, ansi.SetIndeterminateProgressBar)
176 defer func() { _, _ = fmt.Fprintf(os.Stderr, ansi.ResetProgressBar) }()
182// setupApp handles the common setup logic for both interactive and non-interactive modes.
183// It returns the app instance, config, cleanup function, and any error.
184func setupApp(cmd *cobra.Command) (*app.App, error) {
185 debug, _ := cmd.Flags().GetBool("debug")
186 yolo, _ := cmd.Flags().GetBool("yolo")
187 dataDir, _ := cmd.Flags().GetString("data-dir")
190 cwd, err := ResolveCwd(cmd)
195 cfg, err := config.Init(cwd, dataDir, debug)
200 if cfg.Permissions == nil {
201 cfg.Permissions = &config.Permissions{}
203 cfg.Permissions.SkipRequests = yolo
205 if err := createDotCrushDir(cfg.Options.DataDirectory); err != nil {
209 // Register this project in the centralized projects list.
210 if err := projects.Register(cwd, cfg.Options.DataDirectory); err != nil {
211 slog.Warn("Failed to register project", "error", err)
212 // Non-fatal: continue even if registration fails
215 // Connect to DB; this will also run migrations.
216 conn, err := db.Connect(ctx, cfg.Options.DataDirectory)
221 appInstance, err := app.New(ctx, conn, cfg)
223 slog.Error("Failed to create app instance", "error", err)
227 if shouldEnableMetrics(cfg) {
231 return appInstance, nil
234func shouldEnableMetrics(cfg *config.Config) bool {
235 if v, _ := strconv.ParseBool(os.Getenv("CRUSH_DISABLE_METRICS")); v {
238 if v, _ := strconv.ParseBool(os.Getenv("DO_NOT_TRACK")); v {
241 if cfg.Options.DisableMetrics {
247func MaybePrependStdin(prompt string) (string, error) {
248 if term.IsTerminal(os.Stdin.Fd()) {
251 fi, err := os.Stdin.Stat()
255 // Check if stdin is a named pipe ( | ) or regular file ( < ).
256 if fi.Mode()&os.ModeNamedPipe == 0 && !fi.Mode().IsRegular() {
259 bts, err := io.ReadAll(os.Stdin)
263 return string(bts) + "\n\n" + prompt, nil
266func ResolveCwd(cmd *cobra.Command) (string, error) {
267 cwd, _ := cmd.Flags().GetString("cwd")
271 return "", fmt.Errorf("failed to change directory: %v", err)
275 cwd, err := os.Getwd()
277 return "", fmt.Errorf("failed to get current working directory: %v", err)
282func createDotCrushDir(dir string) error {
283 if err := os.MkdirAll(dir, 0o700); err != nil {
284 return fmt.Errorf("failed to create data directory: %q %w", dir, err)
287 gitIgnorePath := filepath.Join(dir, ".gitignore")
288 if _, err := os.Stat(gitIgnorePath); os.IsNotExist(err) {
289 if err := os.WriteFile(gitIgnorePath, []byte("*\n"), 0o644); err != nil {
290 return fmt.Errorf("failed to create .gitignore file: %q %w", gitIgnorePath, err)
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