-
Notifications
You must be signed in to change notification settings - Fork 62
Add a global JSON error envelope with stable codes and next_steps #1282
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+503
−51
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
0300b6e
Add structured JSON next_steps to workflow commands and a global JSON…
no-itsbackpack 86989ca
Defer non-workflow errors to the global JSON classifier in workflow c…
no-itsbackpack 2cac4d5
Only classify transport-level failures as NETWORK_ERROR
no-itsbackpack ab8a035
Restore non-human format rejection for confirmation-gated workflow co…
no-itsbackpack 4b14f75
Merge branch 'main' into cli/workflow-json-next-steps
no-itsbackpack 0651e90
Revert workflow-specific JSON error handling to focus on the global e…
no-itsbackpack b7ce77a
Unify JSON error envelope on the issues schema, strip ANSI codes, doc…
no-itsbackpack 008f9fe
Match both interactive-shell and interactive-terminal messages as TTY…
no-itsbackpack 8220ab3
Error on unknown root subcommands so JSON mode reports UNKNOWN_COMMAND.
no-itsbackpack 838f2a7
Revert "Error on unknown root subcommands so JSON mode reports UNKNOW…
no-itsbackpack File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,204 @@ | ||
| package cmdutil | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "errors" | ||
| "fmt" | ||
| "io" | ||
| "regexp" | ||
| "strings" | ||
| ) | ||
|
|
||
| // JSONErrorIssue mirrors the issue shape used by auth check, sql, and import | ||
| // d1 responses: a stable machine-readable code plus the human message. | ||
| type JSONErrorIssue struct { | ||
| Code string `json:"code"` | ||
| Message string `json:"message"` | ||
| } | ||
|
|
||
| // JSONErrorResponse is the fallback JSON envelope for unhandled command | ||
| // errors. It follows the same schema as command-specific error responses: | ||
| // status, error, issues (code + message), next_steps. | ||
| type JSONErrorResponse struct { | ||
| Status string `json:"status"` | ||
| Error string `json:"error"` | ||
| Issues []JSONErrorIssue `json:"issues"` | ||
| NextSteps []string `json:"next_steps"` | ||
| } | ||
|
|
||
| // Code returns the classification code of the first issue. | ||
| func (r JSONErrorResponse) Code() string { | ||
| if len(r.Issues) == 0 { | ||
| return "" | ||
| } | ||
| return r.Issues[0].Code | ||
| } | ||
|
|
||
| // ansiEscape matches CSI color/style sequences. Error messages built for | ||
| // human output may embed them (e.g. via printer.BoldBlue); they must never | ||
| // reach a JSON payload. | ||
| var ansiEscape = regexp.MustCompile(`\x1b\[[0-9;]*m`) | ||
|
|
||
| // GlobalJSONError classifies an error for the global JSON envelope. | ||
| // | ||
| // Classification is by message text because this is the top-level fallback: | ||
| // command-specific handlers with typed errors have already had their chance. | ||
| // next_steps must earn their place: each case lists only the commands that | ||
| // actually address the failure, not generic bootstrap commands. | ||
| func GlobalJSONError(err error) JSONErrorResponse { | ||
| msg := err.Error() | ||
| status := "error" | ||
| code := "COMMAND_FAILED" | ||
|
|
||
| if cmdErr, ok := errors.AsType[*Error](err); ok { | ||
| if cmdErr.Msg != "" { | ||
| msg = cmdErr.Msg | ||
| } | ||
| if cmdErr.ExitCode == ActionRequestedExitCode { | ||
| status = "action_required" | ||
| } | ||
| } | ||
|
|
||
| msg = ansiEscape.ReplaceAllString(msg, "") | ||
|
|
||
| var nextSteps []string | ||
| lower := strings.ToLower(msg) | ||
|
|
||
| switch { | ||
| // Auth problems: the fix is logging in (or fixing credentials), then verifying. | ||
| case strings.Contains(msg, WarnAuthMessage) || | ||
| strings.Contains(lower, "not authenticated") || | ||
| strings.Contains(msg, "access token has expired"): | ||
| status = "action_required" | ||
| code = "NO_AUTH" | ||
| nextSteps = []string{AgentAuthLoginCmd(), AgentAuthCheckCmd()} | ||
|
|
||
| case strings.Contains(msg, "Authentication failed") && strings.Contains(msg, "service token"): | ||
| status = "action_required" | ||
| code = "SERVICE_TOKEN_INVALID" | ||
| nextSteps = []string{ | ||
| "Verify --service-token-id and --service-token values (or PLANETSCALE_SERVICE_TOKEN_ID / PLANETSCALE_SERVICE_TOKEN)", | ||
| AgentAuthCheckCmd(), | ||
| } | ||
|
|
||
| // --org on pscale root: show the corrected shape, not a re-auth loop. | ||
| case strings.Contains(msg, "unknown flag: --org"): | ||
| code = "INVALID_FLAG_PLACEMENT" | ||
| nextSteps = []string{ | ||
| "Move --org onto the resource subcommand", | ||
| AgentDatabaseListCmd(""), | ||
| } | ||
|
|
||
| // Wrong invocation: cobra's message already names the missing piece and | ||
| // includes usage. Point at the guide for command shapes; auth is not the issue. | ||
| case strings.Contains(msg, "missing argument") || | ||
| strings.Contains(msg, "missing arguments") || | ||
| strings.Contains(msg, "missing required flags") || | ||
| strings.Contains(msg, "required flag"): | ||
| status = "action_required" | ||
| code = "INVALID_USAGE" | ||
| nextSteps = []string{ | ||
| "Re-run with the missing arguments or flags named in the error message", | ||
| AgentGuideCmd(), | ||
| } | ||
|
|
||
| case strings.Contains(msg, "unknown command"): | ||
| code = "UNKNOWN_COMMAND" | ||
| nextSteps = []string{ | ||
| "Run `pscale --help` to list valid commands", | ||
| AgentGuideCmd(), | ||
| } | ||
|
|
||
| case strings.Contains(msg, "unknown flag") || strings.Contains(msg, "unknown shorthand flag"): | ||
| code = "UNKNOWN_FLAG" | ||
| nextSteps = []string{ | ||
| "Run the same command with --help to list valid flags", | ||
| AgentGuideCmd(), | ||
| } | ||
|
|
||
| // TTY-gated commands: messages vary ("interactive shell", "interactive | ||
| // terminal"), so match the shared prefix. Only login has a JSON-mode | ||
| // alternative worth suggesting. | ||
| case strings.Contains(msg, "requires an interactive"): | ||
| status = "action_required" | ||
| code = "TTY_REQUIRED" | ||
| if strings.Contains(lower, "login") { | ||
| nextSteps = []string{AgentAuthLoginCmd()} | ||
| } else { | ||
| nextSteps = []string{"Re-run this command in an interactive terminal"} | ||
| } | ||
|
|
||
| // Confirmation-gated commands: the fix is user approval, then --force. | ||
| case strings.Contains(msg, "run with --force"): | ||
| status = "action_required" | ||
| code = "CONFIRMATION_REQUIRED" | ||
| nextSteps = []string{ | ||
| "Ask the user to approve this action", | ||
| "Re-run the same command with --force after approval", | ||
| } | ||
|
|
||
| // Resource lookups that failed: discovery commands are the way forward. | ||
| case strings.Contains(msg, "does not exist"): | ||
| code = "NOT_FOUND" | ||
| nextSteps = []string{ | ||
| AgentOrgListCmd(), | ||
| AgentDatabaseListCmd(""), | ||
| } | ||
|
|
||
| // Connectivity: nothing an agent can self-serve beyond retry and status. | ||
| // Match transport-level failures only — a bare "timeout" substring would | ||
| // swallow operation or deadline timeouts that deserve command-specific | ||
| // handling. | ||
| case strings.Contains(lower, "connection refused") || | ||
| strings.Contains(lower, "connection reset by peer") || | ||
| strings.Contains(lower, "no such host") || | ||
| strings.Contains(lower, "i/o timeout") || | ||
| strings.Contains(lower, "tls handshake timeout") || | ||
| strings.Contains(lower, "temporary failure in name resolution"): | ||
| code = "NETWORK_ERROR" | ||
| nextSteps = []string{ | ||
| "Check network connectivity and --api-url, then retry", | ||
| AgentAuthCheckCmd(), | ||
| } | ||
|
|
||
| // Unclassified: auth state is the one thing worth ruling out first. | ||
| default: | ||
| nextSteps = []string{ | ||
| AgentAuthCheckCmd(), | ||
| AgentGuideCmd(), | ||
| } | ||
| } | ||
|
|
||
| return JSONErrorResponse{ | ||
| Status: status, | ||
| Error: msg, | ||
| Issues: []JSONErrorIssue{{Code: code, Message: msg}}, | ||
| NextSteps: nextSteps, | ||
| } | ||
| } | ||
|
|
||
| // ReportGlobalJSONError writes the classified envelope for err to w and | ||
| // returns the process exit code. On encoding failure it falls back to a | ||
| // plain error line on errW. | ||
| func ReportGlobalJSONError(w, errW io.Writer, err error) int { | ||
| resp := GlobalJSONError(err) | ||
| if writeErr := WriteJSONError(w, resp); writeErr != nil { | ||
| fmt.Fprintf(errW, "Error: %s\n", err) | ||
| } | ||
|
|
||
| if resp.Status == "action_required" { | ||
| return ActionRequestedExitCode | ||
| } | ||
| if cmdErr, ok := errors.AsType[*Error](err); ok && cmdErr.ExitCode != 0 { | ||
| return cmdErr.ExitCode | ||
| } | ||
| return FatalErrExitCode | ||
| } | ||
|
|
||
| // WriteJSONError writes a JSON error envelope to w. | ||
| func WriteJSONError(w io.Writer, resp JSONErrorResponse) error { | ||
| enc := json.NewEncoder(w) | ||
| enc.SetEscapeHTML(false) | ||
| enc.SetIndent("", " ") | ||
| return enc.Encode(resp) | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.