Files
dingtalk-workspace-cli/internal/output/pretty.go

294 lines
8.4 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package output
import (
"encoding/json"
"fmt"
"io"
"sort"
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/tui"
)
// writePretty renders the payload as ANSI-colored, human-readable text.
// If the payload looks like a `dws schema` response (has `kind: "schema"`),
// a schema-specific hierarchical renderer runs. Anything else falls back
// to the table renderer so `--format pretty` is safe on any command.
func writePretty(w io.Writer, payload any) error {
normalized, err := normalizePayload(payload)
if err != nil {
return err
}
if m, ok := normalized.(map[string]any); ok {
if kind, _ := m["kind"].(string); kind == "schema" {
return writeSchemaPretty(w, m)
}
}
return writeTableish(w, normalized)
}
// writeSchemaPretty handles both the list-all shape (has `products`) and
// the single-tool shape (has `tool`). Colours are on by default via fatih/color,
// auto-disabled when stdout is not a TTY or when NO_COLOR is set.
func writeSchemaPretty(w io.Writer, payload map[string]any) error {
if tool, ok := payload["tool"].(map[string]any); ok {
return writeSchemaToolPretty(w, payload, tool)
}
if products, ok := payload["products"].([]any); ok {
return writeSchemaListPretty(w, payload, products)
}
if degraded, _ := payload["degraded"].(bool); degraded {
reason, _ := payload["reason"].(string)
hint, _ := payload["hint"].(string)
fmt.Fprintf(w, "%s discovery degraded: %s\n", tui.StateMark("warning"), tui.Warning(reason))
if hint != "" {
fmt.Fprintf(w, " %s %s\n", tui.Key("hint"), hint)
}
return nil
}
return writeTableish(w, payload)
}
func writeSchemaListPretty(
w io.Writer,
payload map[string]any,
products []any,
) error {
count, _ := payload["count"].(float64)
if count == 0 {
count = float64(len(products))
}
fmt.Fprintf(w, "%s\n", tui.Header("Catalog", fmt.Sprintf("%d products discovered", int(count))))
fmt.Fprintf(w, "%s\n", tui.Rule(64))
for _, raw := range products {
p, ok := raw.(map[string]any)
if !ok {
continue
}
id, _ := p["id"].(string)
name, _ := p["name"].(string)
desc, _ := p["description"].(string)
tools, _ := p["tools"].([]any)
fmt.Fprintf(w, "\n%s %s %s\n", tui.StateMark("ok"), tui.Bold(id), tui.Dim(name))
if desc != "" && desc != name {
fmt.Fprintf(w, " %s\n", tui.Dim(desc))
}
fmt.Fprintf(w, " %s %s\n", tui.Key("tools"), tui.Cyan(fmt.Sprintf("%d", len(tools))))
shown := 0
for _, t := range tools {
tm, ok := t.(map[string]any)
if !ok {
continue
}
rpc, _ := tm["name"].(string)
cli, _ := tm["cli_name"].(string)
if cli == "" || cli == rpc {
fmt.Fprintf(w, " %s %s\n", tui.Bullet(), rpc)
} else {
fmt.Fprintf(w, " %s %s %s %s\n", tui.Bullet(), rpc, tui.Arrow(), tui.Dim(cli))
}
shown++
if shown >= 6 && len(tools) > 8 {
fmt.Fprintf(w, " %s\n", tui.Dim(fmt.Sprintf("… %d more", len(tools)-shown)))
break
}
}
}
return nil
}
func writeSchemaToolPretty(
w io.Writer,
payload map[string]any,
tool map[string]any,
) error {
rpc, _ := tool["name"].(string)
cli, _ := tool["cli_name"].(string)
title, _ := tool["title"].(string)
desc, _ := tool["description"].(string)
group, _ := tool["group"].(string)
canonical, _ := tool["canonical_path"].(string)
header := rpc
if title != "" && title != rpc {
header = fmt.Sprintf("%s %s", rpc, tui.Dim(title))
}
fmt.Fprintf(w, "%s\n", tui.Header("Tool "+header, "schema"))
fmt.Fprintf(w, "%s\n", tui.Rule(72))
var productID string
if product, ok := payload["product"].(map[string]any); ok {
pid, _ := product["id"].(string)
pname, _ := product["name"].(string)
productID = pid
fmt.Fprintf(w, " %s %s %s\n", tui.Key("product"), tui.Cyan(pid), tui.Dim(pname))
}
if canonical != "" {
fmt.Fprintf(w, " %s %s\n", tui.Key("canonical"), canonical)
}
if cli != "" {
parts := []string{}
if productID != "" {
parts = append(parts, productID)
}
if group != "" {
parts = append(parts, strings.Split(group, ".")...)
}
parts = append(parts, cli)
fmt.Fprintf(w, " %s %s\n", tui.Key("cli path"), tui.Cyan(strings.Join(parts, " ")))
}
// Sensitivity + annotations in one line.
sensitive, _ := tool["sensitive"].(bool)
if sensitive {
fmt.Fprintf(w, " %s %s\n", tui.Key("sensitive"), tui.Danger("yes (needs --yes)"))
}
if ann, ok := tool["annotations"].(map[string]any); ok && len(ann) > 0 {
var parts []string
for _, key := range sortedMapKeys(ann) {
parts = append(parts, fmt.Sprintf("%s=%v", key, ann[key]))
}
fmt.Fprintf(w, " %s %s\n", tui.Key("annotations"), tui.Warning(strings.Join(parts, " ")))
}
if desc != "" && desc != title {
fmt.Fprintln(w)
for _, line := range strings.Split(strings.TrimSpace(desc), "\n") {
fmt.Fprintf(w, " %s\n", tui.Dim(line))
}
}
// Parameters section.
if params, ok := tool["parameters"].(map[string]any); ok && len(params) > 0 {
required := map[string]bool{}
if req, ok := tool["required"].([]any); ok {
for _, r := range req {
if s, ok := r.(string); ok {
required[s] = true
}
}
}
overlay := map[string]map[string]any{}
if ov, ok := tool["flag_overlay"].(map[string]any); ok {
for name, v := range ov {
if m, ok := v.(map[string]any); ok {
overlay[name] = m
}
}
}
fmt.Fprintln(w)
fmt.Fprintf(w, "%s\n", tui.Section("Parameters"))
for _, name := range sortedMapKeys(params) {
prop, _ := params[name].(map[string]any)
writeParamPretty(w, name, prop, required[name], overlay[name])
}
}
// Output schema, if any.
if out, ok := tool["output_schema"].(map[string]any); ok && len(out) > 0 {
fmt.Fprintln(w)
fmt.Fprintf(w, "%s\n", tui.Section("Output schema"))
b, _ := json.MarshalIndent(out, " ", " ")
fmt.Fprintf(w, " %s\n", string(b))
}
return nil
}
func writeParamPretty(
w io.Writer,
name string,
prop map[string]any,
required bool,
overlay map[string]any,
) {
typeStr := describeType(prop)
marker := " "
if required {
marker = tui.Danger("*")
}
alias, _ := overlay["alias"].(string)
line := fmt.Sprintf(" %s %s", marker, tui.Bold(name))
if alias != "" && alias != name {
line += fmt.Sprintf(" %s", tui.Cyan("--"+alias))
}
line += fmt.Sprintf(" %s", tui.Dim(typeStr))
fmt.Fprintln(w, line)
if d, _ := prop["description"].(string); d != "" {
for _, ln := range strings.Split(strings.TrimSpace(d), "\n") {
fmt.Fprintf(w, " %s\n", tui.Dim(ln))
}
}
// enum values — render inline.
if enum, ok := prop["enum"].([]any); ok && len(enum) > 0 {
vals := make([]string, 0, len(enum))
for _, v := range enum {
vals = append(vals, fmt.Sprintf("%v", v))
}
fmt.Fprintf(w, " %s %s\n", tui.Key("enum"), tui.Success(strings.Join(vals, ", ")))
}
// overlay — transform / env default / default.
if transform, _ := overlay["transform"].(string); transform != "" {
extras := transform
if args, ok := overlay["transform_args"].(map[string]any); ok && len(args) > 0 {
kvs := make([]string, 0, len(args))
for _, k := range sortedMapKeys(args) {
kvs = append(kvs, fmt.Sprintf("%s=%v", k, args[k]))
}
extras += "(" + strings.Join(kvs, ", ") + ")"
}
fmt.Fprintf(w, " %s %s\n", tui.Key("transform"), tui.Warning(extras))
}
if env, _ := overlay["env_default"].(string); env != "" {
fmt.Fprintf(w, " %s %s\n", tui.Key("env default"), tui.Success("$"+env))
}
if def, _ := overlay["default"].(string); def != "" {
fmt.Fprintf(w, " %s %s\n", tui.Key("default"), tui.Success(def))
}
}
func describeType(prop map[string]any) string {
t, _ := prop["type"].(string)
if t == "array" {
if items, ok := prop["items"].(map[string]any); ok {
if inner, _ := items["type"].(string); inner != "" {
return inner + "[]"
}
}
return "array"
}
if t == "" {
return "any"
}
return t
}
func sortedMapKeys(m map[string]any) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}