cli: maturity polish — color, error translation, tabwriter consistency
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>
This commit is contained in:
parent
e47b8146dc
commit
71a332a6a1
11 changed files with 358 additions and 28 deletions
90
internal/cli/errors.go
Normal file
90
internal/cli/errors.go
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue