Adds three small but high-leverage presentation tweaks for v0.1: 1. internal/cli/style is a new ~70 LOC package with Pass/Fail/Warn/ Dim/Bold helpers. Each is TTY-gated and obeys NO_COLOR. No external dep. Wired into the doctor PASS/FAIL/WARN status, the "banger:" error prefix on stderr, and the dim 'ready in <elapsed>' line. 2. internal/cli/errors translates rpc.ErrorResponse into user-facing text. operation_failed becomes invisible (the message wins); not_found, already_exists, bad_request, bad_version, unauthorized, unknown_method get short labels; unknown codes pass through. The daemon-attached op_id lands in dim parens — paste into journalctl --grep to find the daemon log line that produced the failure. 3. Tabwriter config converges on (0, 8, 2, ' ', 0) across every list/table command. The vm prune confirmation table picked up the right config; system install + system status switched from bare "key: value\n" lines to tabular form. printVMSpecLine drops its Unicode middle dot for an ASCII '|' so terminals without UTF-8 render cleanly. Tests cover translateRPCError for every code, style helpers no-op on non-TTY and under NO_COLOR. Smoke status greps switch from "key: value" to "key value" to match the new format. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
90 lines
2.9 KiB
Go
90 lines
2.9 KiB
Go
package cli
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
|
|
"banger/internal/cli/style"
|
|
"banger/internal/rpc"
|
|
"io"
|
|
)
|
|
|
|
// TranslateError is the public entry point used by cmd/banger/main.go
|
|
// to render any error reaching the top of the cobra tree. Forwards
|
|
// to the package-internal helper so tests can reach it directly.
|
|
func TranslateError(w io.Writer, err error) string {
|
|
return translateRPCError(w, err)
|
|
}
|
|
|
|
// translateRPCError turns an error returned by rpc.Call into a
|
|
// user-facing string. Known codes get short, friendly prefixes;
|
|
// unknown codes pass through verbatim so debuggability is preserved.
|
|
// When the daemon attached an op_id the helper appends it in parens
|
|
// so an operator can paste it into journalctl --grep.
|
|
//
|
|
// Color is applied only when w is a TTY (and NO_COLOR is unset).
|
|
// The returned string never includes a trailing newline — caller
|
|
// chooses where it goes.
|
|
func translateRPCError(w io.Writer, err error) string {
|
|
if err == nil {
|
|
return ""
|
|
}
|
|
var rpcErr *rpc.ErrorResponse
|
|
if !errors.As(err, &rpcErr) || rpcErr == nil {
|
|
// Non-RPC failures (dialing the socket, decode errors,
|
|
// context cancellation, ...) come through as plain Go
|
|
// errors. Surface them verbatim — they already mention
|
|
// the underlying cause clearly enough.
|
|
return err.Error()
|
|
}
|
|
prefix := errorCodePrefix(rpcErr.Code)
|
|
body := rpcErr.Message
|
|
if prefix != "" {
|
|
body = prefix + ": " + rpcErr.Message
|
|
} else if rpcErr.Message == "" {
|
|
// Defensive: a server that returned a code with no
|
|
// message still has SOMETHING to report; default to the
|
|
// raw code so we never print an empty error.
|
|
body = rpcErr.Code
|
|
}
|
|
if rpcErr.OpID != "" {
|
|
body = body + " (" + style.Dim(w, rpcErr.OpID) + ")"
|
|
}
|
|
return body
|
|
}
|
|
|
|
// errorCodePrefix maps the small set of codes the daemon emits to
|
|
// short user-facing labels. Unknown codes return "" so the message
|
|
// alone is shown — keeps the door open for future codes the CLI
|
|
// hasn't been updated to recognise.
|
|
//
|
|
// "operation_failed" is the catch-all the generic dispatcher uses
|
|
// when a service returned an error; the message is already self-
|
|
// explanatory, so we strip the code entirely. Specialised codes
|
|
// (not_found, already_exists, ...) keep a label because the
|
|
// message body alone may not say what kind of failure it is.
|
|
func errorCodePrefix(code string) string {
|
|
switch strings.TrimSpace(code) {
|
|
case "", "operation_failed":
|
|
return ""
|
|
case "not_found":
|
|
return "not found"
|
|
case "not_running":
|
|
return "not running"
|
|
case "already_exists":
|
|
return "already exists"
|
|
case "bad_request", "bad_params":
|
|
return "bad request"
|
|
case "bad_version":
|
|
return "version mismatch"
|
|
case "unauthorized":
|
|
return "unauthorized"
|
|
case "unknown_method":
|
|
return "unknown method"
|
|
default:
|
|
// Surface the raw code so an operator filing a bug has
|
|
// something concrete to grep for. Strips the boilerplate
|
|
// "operation_failed" but keeps anything novel.
|
|
return code
|
|
}
|
|
}
|