26f897c1ec
release / release-please (push) Failing after 1m49s
docs / build (push) Failing after 6m34s
release / build-and-upload (arm64, linux) (push) Has been cancelled
release / build-and-upload (arm64, windows) (push) Has been cancelled
release / build-darwin (amd64, darwin) (push) Has been cancelled
release / checksums (push) Has been cancelled
release / finalize (push) Has been cancelled
release / build-darwin (arm64, darwin) (push) Has been cancelled
release / build-and-upload (amd64, linux) (push) Has been cancelled
release / build-and-upload (amd64, windows) (push) Has been cancelled
docs / deploy (push) Has been cancelled
151 lines
4.4 KiB
Go
151 lines
4.4 KiB
Go
package cli
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/charmbracelet/lipgloss"
|
|
"github.com/kunchenguid/no-mistakes/internal/buildinfo"
|
|
"github.com/kunchenguid/no-mistakes/internal/db"
|
|
"github.com/kunchenguid/no-mistakes/internal/git"
|
|
"github.com/kunchenguid/no-mistakes/internal/paths"
|
|
"github.com/muesli/termenv"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// exitError carries an explicit process exit code. Commands that render their
|
|
// own structured output (the axi surface) return one of these so they can map
|
|
// outcomes onto AXI exit-code conventions (0 success/no-op, 1 error, 2 usage)
|
|
// without cobra printing the Go error to the user. A nil inner err prints
|
|
// nothing to stderr; a non-nil err is surfaced as a diagnostic.
|
|
type exitError struct {
|
|
code int
|
|
err error
|
|
}
|
|
|
|
func (e *exitError) Error() string {
|
|
if e.err != nil {
|
|
return e.err.Error()
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (e *exitError) Unwrap() error { return e.err }
|
|
|
|
// Execute runs the root CLI command.
|
|
func Execute() int {
|
|
root := newRootCmd()
|
|
if err := root.Execute(); err != nil {
|
|
var ee *exitError
|
|
if errors.As(err, &ee) {
|
|
if ee.err != nil {
|
|
fmt.Fprintln(root.ErrOrStderr(), ee.err)
|
|
}
|
|
return ee.code
|
|
}
|
|
fmt.Fprintln(root.ErrOrStderr(), err)
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func newRootCmd() *cobra.Command {
|
|
var autoYes bool
|
|
var skipValue string
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "no-mistakes",
|
|
Short: "Local Git proxy that validates code before pushing to the configured target",
|
|
Version: buildinfo.String(),
|
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
|
setColorProfileForOutput(cmd.OutOrStdout())
|
|
},
|
|
// Silence cobra's default error/usage printing — we handle it ourselves.
|
|
SilenceErrors: true,
|
|
SilenceUsage: true,
|
|
// When run without a subcommand, attach to the current branch run or
|
|
// route users into the setup wizard when no run is active. The default
|
|
// wizard flow is interactive, while --yes auto-accepts defaults and can
|
|
// still fall back to headless mode when no TTY is available.
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
return trackCommand("root", func() error {
|
|
skipSteps, err := parseSkipSteps(skipValue)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return attachRun(cmd.Context(), cmd.OutOrStdout(), "", true, autoYes, skipSteps)
|
|
})
|
|
},
|
|
}
|
|
|
|
cmd.Flags().BoolVarP(&autoYes, "yes", "y", false, "run setup wizard and accept defaults automatically")
|
|
cmd.Flags().StringVar(&skipValue, "skip", "", "comma-separated pipeline steps to skip for a new run")
|
|
|
|
cmd.AddCommand(newInitCmd())
|
|
cmd.AddCommand(newEjectCmd())
|
|
cmd.AddCommand(newUpdateCmd())
|
|
cmd.AddCommand(newDaemonCmd())
|
|
cmd.AddCommand(newAttachCmd())
|
|
cmd.AddCommand(newRerunCmd())
|
|
cmd.AddCommand(newStatusCmd())
|
|
cmd.AddCommand(newRunsCmd())
|
|
cmd.AddCommand(newStatsCmd())
|
|
cmd.AddCommand(newDoctorCmd())
|
|
cmd.AddCommand(newAxiCmd())
|
|
|
|
return cmd
|
|
}
|
|
|
|
func setColorProfileForOutput(w io.Writer) {
|
|
lipgloss.SetColorProfile(termenv.NewOutput(w).EnvColorProfile())
|
|
}
|
|
|
|
// findRepo looks up the repo for the current directory. If the working
|
|
// directory is inside a git worktree, it falls back to the main repository
|
|
// root so that worktrees work out of the box when the main repo is
|
|
// already initialized.
|
|
func findRepo(d *db.DB) (*db.Repo, error) {
|
|
gitRoot, err := git.FindGitRoot(".")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("not in a git repository")
|
|
}
|
|
repo, err := d.GetRepoByPath(gitRoot)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get repo: %w", err)
|
|
}
|
|
if repo != nil {
|
|
return repo, nil
|
|
}
|
|
// Try the main worktree root (handles git worktrees).
|
|
mainRoot, err := git.FindMainRepoRoot(".")
|
|
if err != nil || mainRoot == gitRoot {
|
|
return nil, fmt.Errorf("repo not initialized (run 'no-mistakes init' first)")
|
|
}
|
|
repo, err = d.GetRepoByPath(mainRoot)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get repo: %w", err)
|
|
}
|
|
if repo == nil {
|
|
return nil, fmt.Errorf("repo not initialized (run 'no-mistakes init' first)")
|
|
}
|
|
return repo, nil
|
|
}
|
|
|
|
// openResources initializes paths, ensures directories exist, and opens the DB.
|
|
// Caller must close the returned DB.
|
|
func openResources() (*paths.Paths, *db.DB, error) {
|
|
p, err := paths.New()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("resolve paths: %w", err)
|
|
}
|
|
if err := p.EnsureDirs(); err != nil {
|
|
return nil, nil, fmt.Errorf("create directories: %w", err)
|
|
}
|
|
d, err := db.Open(p.DB())
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("open database: %w", err)
|
|
}
|
|
return p, d, nil
|
|
}
|