check / check (push) Successful in 10m44s
A binary from go install, the README's first command, reported itself as version dev, a development build, although Go had recorded the tag it was built from. When the linker flags leave the version, commit or commit date unset, they are now filled in from the Go build info, so such a binary reports its tag and counts as a release. Builds through make, script/docker and goreleaser are unchanged. Model: opus-5-5
134 lines
4.3 KiB
Go
134 lines
4.3 KiB
Go
package cli
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"sneak.berlin/go/vaultik/internal/globals"
|
|
"sneak.berlin/go/vaultik/internal/ui"
|
|
)
|
|
|
|
// shortCommitLen is the number of git commit hash characters shown in
|
|
// the startup banner.
|
|
const shortCommitLen = 12
|
|
|
|
// exitCodeInterrupted is the exit status of a command that SIGINT or
|
|
// SIGTERM stopped. It is 128 plus SIGINT's number, 2, which is what a
|
|
// shell reports for a command stopped by Ctrl-C.
|
|
const exitCodeInterrupted = 130
|
|
|
|
// Entry is the main entry point for the CLI application.
|
|
// It first calls globals.FillUnsetFromBuildInfo, which fills in the
|
|
// version, commit and commit date that the linker flags did not set.
|
|
// It prints the startup banner to stderr (unless a banner-suppressing
|
|
// flag is present in os.Args — see bannerSuppressedInArgs), executes the
|
|
// root cobra command, and routes any returned error through the
|
|
// ui.Writer so the user sees a properly formatted "🛑 ERROR:" line.
|
|
// The banner goes to stderr because stdout carries only the output the
|
|
// user asked for, such as a completion script or a `config get` value.
|
|
//
|
|
// It returns the process exit code (0 on success, 130 when interrupted,
|
|
// 1 on any other error) rather than calling os.Exit, so that main's
|
|
// deferred profile writers run before the process ends. See run in
|
|
// cmd/vaultik/main.go.
|
|
func Entry() int {
|
|
globals.FillUnsetFromBuildInfo()
|
|
emitStartupBanner(os.Args[1:], os.Stderr)
|
|
|
|
rootCmd := NewRootCommand()
|
|
rootCmd.SilenceErrors = true
|
|
|
|
err := rootCmd.Execute()
|
|
if err != nil {
|
|
// An operation that ran inside the fx app has already reported
|
|
// its own failure (`snapshot verify --json` puts it in the
|
|
// document instead); errReported says so. Printing it again
|
|
// here would double the error line.
|
|
// Every other error — bad arguments, a config that would not
|
|
// load, an interrupt — reaches Entry unreported, so it is shown
|
|
// here.
|
|
if !errors.Is(err, errReported) {
|
|
ReportErrorf("%s", err.Error())
|
|
}
|
|
|
|
if errors.Is(err, errInterrupted) {
|
|
return exitCodeInterrupted
|
|
}
|
|
|
|
return 1
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
// emitStartupBanner writes the startup banner to w unless args (the
|
|
// argument vector with the program name already stripped) contains a
|
|
// flag that suppresses it. Split out of Entry so that the decision is
|
|
// reachable from a test without running the whole CLI.
|
|
func emitStartupBanner(args []string, w io.Writer) {
|
|
if bannerSuppressedInArgs(args) {
|
|
return
|
|
}
|
|
|
|
short := globals.Commit
|
|
if len(short) > shortCommitLen {
|
|
short = short[:shortCommitLen]
|
|
}
|
|
|
|
writeStartupBanner(ui.New(w), time.Now().UTC(), short)
|
|
}
|
|
|
|
// ReportErrorf emits a user-facing error to stderr in the standard
|
|
// 🛑 ERROR: format. Use it from goroutine error paths (where returning
|
|
// an error to cobra isn't an option) and anywhere else a CLI command
|
|
// must surface a failure outside the normal RunE return path.
|
|
func ReportErrorf(format string, args ...any) {
|
|
ui.New(os.Stderr).Errorf(format, args...)
|
|
}
|
|
|
|
// bannerSuppressedInArgs reports whether any of args is a flag that
|
|
// should suppress the startup banner (--quiet/-q/--cron/--json). Stops
|
|
// at the "--" argument terminator. Recognizes both long forms and short
|
|
// -q, including combined short flags like "-qv".
|
|
//
|
|
// This scans the raw argument vector because the banner is printed
|
|
// before cobra parses anything — deliberately, so that it still appears
|
|
// when cobra rejects the arguments and on --help. The consequence is
|
|
// that a flag is matched wherever it occurs in the vector, including
|
|
// positions where the command it belongs to would not accept it.
|
|
// --json is a subcommand flag rather than a persistent one, but so is
|
|
// --cron (it exists only on `snapshot create`), so this adds no new
|
|
// class of imprecision. The only cost of a false positive is a missing
|
|
// decorative banner.
|
|
func bannerSuppressedInArgs(args []string) bool {
|
|
for _, a := range args {
|
|
if a == "--" {
|
|
return false
|
|
}
|
|
|
|
switch a {
|
|
case "--quiet", "-q", "--cron", "--json":
|
|
return true
|
|
}
|
|
|
|
if strings.HasPrefix(a, "--quiet=") ||
|
|
strings.HasPrefix(a, "--cron=") ||
|
|
strings.HasPrefix(a, "--json=") {
|
|
return true
|
|
}
|
|
// Combined short flags like -qv or -vq.
|
|
if len(a) > 1 && a[0] == '-' && a[1] != '-' {
|
|
for _, c := range a[1:] {
|
|
if c == 'q' {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|