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 } }