mirror of
https://github.com/larksuite/cli.git
synced 2026-07-04 06:29:52 +08:00
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
122 lines
2.7 KiB
Go
122 lines
2.7 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package output
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
|
|
"github.com/larksuite/cli/internal/validate"
|
|
)
|
|
|
|
// PrintJson prints data as formatted JSON to w.
|
|
func PrintJson(w io.Writer, data interface{}) {
|
|
injectNotice(data)
|
|
b, err := json.MarshalIndent(data, "", " ")
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "json marshal error: %v\n", err)
|
|
return
|
|
}
|
|
fmt.Fprintln(w, string(b))
|
|
}
|
|
|
|
// injectNotice adds a "_notice" field into CLI envelope maps.
|
|
// Only modifies map[string]interface{} values that have an "ok" key
|
|
// (e.g. doctor, auth, config commands that build map envelopes directly).
|
|
//
|
|
// Struct-based envelopes (Envelope, ErrorEnvelope) are NOT handled here —
|
|
// callers must set the Notice field explicitly via GetNotice().
|
|
// See: shortcuts/common/runner.go Out(), output/errors.go WriteErrorEnvelope().
|
|
func injectNotice(data interface{}) {
|
|
if PendingNotice == nil {
|
|
return
|
|
}
|
|
m, ok := data.(map[string]interface{})
|
|
if !ok {
|
|
return
|
|
}
|
|
if _, isEnvelope := m["ok"]; !isEnvelope {
|
|
return
|
|
}
|
|
notice := PendingNotice()
|
|
if notice == nil {
|
|
return
|
|
}
|
|
m["_notice"] = notice
|
|
}
|
|
|
|
// PrintNdjson prints data as NDJSON (Newline Delimited JSON) to w.
|
|
func PrintNdjson(w io.Writer, data interface{}) {
|
|
emit := func(item interface{}) {
|
|
b, err := json.Marshal(item)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "ndjson marshal error: %v\n", err)
|
|
return
|
|
}
|
|
fmt.Fprintln(w, string(b))
|
|
}
|
|
if arr, ok := data.([]interface{}); ok {
|
|
for _, item := range arr {
|
|
emit(item)
|
|
}
|
|
} else {
|
|
emit(data)
|
|
}
|
|
}
|
|
|
|
func cellStr(val interface{}) string {
|
|
if val == nil {
|
|
return ""
|
|
}
|
|
var s string
|
|
switch v := val.(type) {
|
|
case string:
|
|
s = v
|
|
case json.Number:
|
|
s = v.String()
|
|
case float64:
|
|
if v == float64(int(v)) {
|
|
s = fmt.Sprintf("%d", int(v))
|
|
} else {
|
|
s = fmt.Sprintf("%g", v)
|
|
}
|
|
case bool:
|
|
s = fmt.Sprintf("%v", v)
|
|
default:
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
return fmt.Sprintf("%v", v)
|
|
}
|
|
s = string(b)
|
|
}
|
|
// Sanitize for terminal display: strip ANSI escapes, control chars, dangerous Unicode.
|
|
return validate.SanitizeForTerminal(s)
|
|
}
|
|
|
|
// PrintTable prints rows as a table to w.
|
|
// Delegates to FormatAsTable for flattening, column union, and width handling.
|
|
func PrintTable(w io.Writer, rows []map[string]interface{}) {
|
|
if len(rows) == 0 {
|
|
fmt.Fprintln(w, "(no data)")
|
|
return
|
|
}
|
|
items := make([]interface{}, len(rows))
|
|
for i, r := range rows {
|
|
items[i] = r
|
|
}
|
|
FormatAsTable(w, items)
|
|
}
|
|
|
|
// PrintSuccess prints a success message to w.
|
|
func PrintSuccess(w io.Writer, msg string) {
|
|
fmt.Fprintf(w, "OK: %s\n", msg)
|
|
}
|
|
|
|
// PrintError prints an error message to w.
|
|
func PrintError(w io.Writer, msg string) {
|
|
fmt.Fprintf(w, "ERROR: %s\n", msg)
|
|
}
|