Files
larksuite-cli/internal/output/errors.go
MaxHuang22 27139a0919 feat: add automatic CLI update detection and notification (#144)
Add non-blocking update check that queries the npm registry for the
latest @larksuite/cli version. Results are cached locally (24h TTL)
to avoid repeated network requests.

When a newer version is detected, a `_notice.update` field is injected
into all JSON output envelopes (success, error, and shortcut responses),
enabling AI agents and scripts to surface upgrade prompts.

Key changes:
- New `internal/update` package: registry fetch, semver compare, cache
- Async check in root command (cache-first, then background refresh)
- `_notice` field added to Envelope/ErrorEnvelope structs
- `PrintJson` injects notice into map-based envelopes with "ok" key
- `doctor` command gains cli_version and cli_update checks
- Suppressed for CI, DEV builds, shell completion, and git-describe versions
2026-03-31 19:01:39 +08:00

136 lines
3.5 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package output
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
)
// ExitError is a structured error that carries an exit code and optional detail.
// It is propagated up the call chain and handled by main.go to produce
// a JSON error envelope on stderr and the correct exit code.
type ExitError struct {
Code int
Detail *ErrDetail
Err error
Raw bool // when true, skip enrichment (e.g. enrichPermissionError) and preserve original error
}
func (e *ExitError) Error() string {
if e.Detail != nil {
return e.Detail.Message
}
if e.Err != nil {
return e.Err.Error()
}
return fmt.Sprintf("exit %d", e.Code)
}
func (e *ExitError) Unwrap() error {
return e.Err
}
// WriteErrorEnvelope writes a JSON error envelope for the given ExitError to w.
func WriteErrorEnvelope(w io.Writer, err *ExitError, identity string) {
if err.Detail == nil {
return
}
env := &ErrorEnvelope{
OK: false,
Identity: identity,
Error: err.Detail,
Notice: GetNotice(),
}
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
if err := enc.Encode(env); err != nil {
return
}
// Encode appends a trailing newline; write directly.
buf.WriteTo(w)
}
// --- Convenience constructors ---
// Errorf creates an ExitError with the given code, type, and formatted message.
func Errorf(code int, errType, format string, args ...any) *ExitError {
var err error
for _, arg := range args {
if e, ok := arg.(error); ok {
err = e
break
}
}
return &ExitError{
Code: code,
Detail: &ErrDetail{Type: errType, Message: fmt.Sprintf(format, args...)},
Err: err,
}
}
// ErrValidation creates a validation ExitError (exit 2).
func ErrValidation(format string, args ...any) *ExitError {
return Errorf(ExitValidation, "validation", format, args...)
}
// ErrAuth creates an auth ExitError (exit 3).
func ErrAuth(format string, args ...any) *ExitError {
return Errorf(ExitAuth, "auth", format, args...)
}
// ErrNetwork creates a network ExitError (exit 4).
func ErrNetwork(format string, args ...any) *ExitError {
return Errorf(ExitNetwork, "network", format, args...)
}
// ErrAPI creates an API ExitError using ClassifyLarkError.
// For permission errors, uses a concise message; the raw API response is preserved in Detail.
func ErrAPI(larkCode int, msg string, detail any) *ExitError {
exitCode, errType, hint := ClassifyLarkError(larkCode, msg)
if errType == "permission" {
msg = fmt.Sprintf("Permission denied [%d]", larkCode)
}
return &ExitError{
Code: exitCode,
Detail: &ErrDetail{
Type: errType,
Code: larkCode,
Message: msg,
Hint: hint,
Detail: detail,
},
}
}
// ErrWithHint creates an ExitError with a hint string.
func ErrWithHint(code int, errType, msg, hint string) *ExitError {
return &ExitError{
Code: code,
Detail: &ErrDetail{Type: errType, Message: msg, Hint: hint},
}
}
// ErrBare creates an ExitError with only an exit code and no envelope.
// Used for cases like `auth check` where the JSON output is already written to stdout.
func ErrBare(code int) *ExitError {
return &ExitError{Code: code}
}
// MarkRaw sets Raw=true on an ExitError so that enrichment (e.g. enrichPermissionError)
// is skipped and the original API error is preserved. Returns the original error unchanged
// if it is not an ExitError.
func MarkRaw(err error) error {
var exitErr *ExitError
if errors.As(err, &exitErr) {
exitErr.Raw = true
}
return err
}