mirror of
https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git
synced 2026-09-28 21:25:22 +08:00
290 lines
11 KiB
Go
290 lines
11 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0
|
|
|
|
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
_ "embed"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/cmdutil"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
const commandPathFallbacksSchemaRef = "./command_path_fallbacks.schema.json"
|
|
|
|
// command_path_fallbacks.json is a reviewed recovery policy for invalid paths
|
|
// observed in evaluation. It is deliberately not a command identity source:
|
|
// Help, Skill, Schema, and normal navigation continue to publish canonical
|
|
// paths from the CommandRegistry and the real Cobra tree only.
|
|
|
|
//go:embed command_path_fallbacks.json
|
|
var embeddedCommandPathFallbacksJSON []byte
|
|
|
|
//go:embed command_path_fallbacks.schema.json
|
|
var embeddedCommandPathFallbacksSchemaJSON []byte
|
|
|
|
var commandFallbackPathPattern = regexp.MustCompile(`^[A-Za-z0-9+][A-Za-z0-9._:+-]*(?: [A-Za-z0-9+][A-Za-z0-9._:+-]*)*$`)
|
|
|
|
// CommandPathFallbackMode determines whether an invalid reviewed path can be
|
|
// normalized exactly or must stop and surface reviewed candidates.
|
|
type CommandPathFallbackMode string
|
|
|
|
const (
|
|
CommandPathFallbackRewrite CommandPathFallbackMode = "rewrite"
|
|
CommandPathFallbackAmbiguous CommandPathFallbackMode = "ambiguous"
|
|
)
|
|
|
|
// CommandPathFallback is one validated, generated runtime recovery record.
|
|
// From is never advertised as a stable alias. To and Candidates are canonical
|
|
// real Cobra paths validated at generation time.
|
|
type CommandPathFallback struct {
|
|
From string `json:"from"`
|
|
Mode CommandPathFallbackMode `json:"mode"`
|
|
To string `json:"to,omitempty"`
|
|
Candidates []string `json:"candidates,omitempty"`
|
|
Reviewed bool `json:"reviewed"`
|
|
ReviewReason string `json:"review_reason"`
|
|
}
|
|
|
|
type commandPathFallbackSnapshot struct {
|
|
Schema string `json:"$schema"`
|
|
Version int `json:"version"`
|
|
Entries []commandPathFallbackEntrySpec `json:"entries"`
|
|
}
|
|
|
|
type commandPathFallbackEntrySpec struct {
|
|
From string `json:"from"`
|
|
Mode CommandPathFallbackMode `json:"mode"`
|
|
To string `json:"to,omitempty"`
|
|
Candidates []string `json:"candidates,omitempty"`
|
|
Reviewed bool `json:"reviewed"`
|
|
ReviewReason string `json:"review_reason"`
|
|
}
|
|
|
|
var (
|
|
embeddedCommandPathFallbacksOnce sync.Once
|
|
embeddedCommandPathFallbacksData []CommandPathFallback
|
|
embeddedCommandPathFallbacksErr error
|
|
loadReviewedCommandPathFallbacks = loadEmbeddedCommandPathFallbacks
|
|
)
|
|
|
|
// LoadCommandPathFallbacks decodes and validates the authored recovery table.
|
|
// Callers receive a clone so generation and tests cannot mutate shared data.
|
|
func LoadCommandPathFallbacks() ([]CommandPathFallback, error) {
|
|
return loadReviewedCommandPathFallbacks()
|
|
}
|
|
|
|
func loadEmbeddedCommandPathFallbacks() ([]CommandPathFallback, error) {
|
|
embeddedCommandPathFallbacksOnce.Do(func() {
|
|
embeddedCommandPathFallbacksData, embeddedCommandPathFallbacksErr = decodeCommandPathFallbacks(embeddedCommandPathFallbacksJSON)
|
|
})
|
|
return cloneCommandPathFallbacks(embeddedCommandPathFallbacksData), embeddedCommandPathFallbacksErr
|
|
}
|
|
|
|
func decodeCommandPathFallbacks(data []byte) ([]CommandPathFallback, error) {
|
|
var snapshot commandPathFallbackSnapshot
|
|
decoder := json.NewDecoder(bytes.NewReader(data))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&snapshot); err != nil {
|
|
return nil, fmt.Errorf("decode reviewed command path fallbacks: %w", err)
|
|
}
|
|
if err := decoder.Decode(&struct{}{}); err != io.EOF {
|
|
if err == nil {
|
|
err = fmt.Errorf("multiple JSON values")
|
|
}
|
|
return nil, fmt.Errorf("decode reviewed command path fallbacks: %w", err)
|
|
}
|
|
if snapshot.Version != 1 {
|
|
return nil, fmt.Errorf("unsupported command path fallbacks version %d", snapshot.Version)
|
|
}
|
|
if strings.TrimSpace(snapshot.Schema) != commandPathFallbacksSchemaRef {
|
|
return nil, fmt.Errorf("command path fallbacks must declare $schema=%q", commandPathFallbacksSchemaRef)
|
|
}
|
|
if len(snapshot.Entries) == 0 {
|
|
return nil, fmt.Errorf("command path fallbacks declares no entries")
|
|
}
|
|
|
|
entries := make([]CommandPathFallback, 0, len(snapshot.Entries))
|
|
seenFrom := make(map[string]bool, len(snapshot.Entries))
|
|
for index, spec := range snapshot.Entries {
|
|
from, err := validateAuthoredCommandFallbackPath(spec.From)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("command path fallback entry %d from: %w", index, err)
|
|
}
|
|
if seenFrom[from] {
|
|
return nil, fmt.Errorf("command path fallbacks contains duplicate from path %q", from)
|
|
}
|
|
seenFrom[from] = true
|
|
reason := strings.TrimSpace(spec.ReviewReason)
|
|
if !spec.Reviewed || reason == "" {
|
|
return nil, fmt.Errorf("command path fallback %q requires reviewed=true and non-empty review_reason", from)
|
|
}
|
|
|
|
entry := CommandPathFallback{
|
|
From: from,
|
|
Mode: spec.Mode,
|
|
Reviewed: true,
|
|
ReviewReason: reason,
|
|
}
|
|
switch spec.Mode {
|
|
case CommandPathFallbackRewrite:
|
|
if len(spec.Candidates) > 0 {
|
|
return nil, fmt.Errorf("rewrite command path fallback %q must not declare candidates", from)
|
|
}
|
|
to, pathErr := validateAuthoredCommandFallbackPath(spec.To)
|
|
if pathErr != nil {
|
|
return nil, fmt.Errorf("rewrite command path fallback %q to: %w", from, pathErr)
|
|
}
|
|
entry.To = to
|
|
case CommandPathFallbackAmbiguous:
|
|
if strings.TrimSpace(spec.To) != "" {
|
|
return nil, fmt.Errorf("ambiguous command path fallback %q must not declare to", from)
|
|
}
|
|
if len(spec.Candidates) < 2 {
|
|
return nil, fmt.Errorf("ambiguous command path fallback %q requires at least two candidates", from)
|
|
}
|
|
seenCandidates := make(map[string]bool, len(spec.Candidates))
|
|
for _, rawCandidate := range spec.Candidates {
|
|
candidate, pathErr := validateAuthoredCommandFallbackPath(rawCandidate)
|
|
if pathErr != nil {
|
|
return nil, fmt.Errorf("ambiguous command path fallback %q candidate: %w", from, pathErr)
|
|
}
|
|
if seenCandidates[candidate] {
|
|
return nil, fmt.Errorf("ambiguous command path fallback %q repeats candidate %q", from, candidate)
|
|
}
|
|
seenCandidates[candidate] = true
|
|
entry.Candidates = append(entry.Candidates, candidate)
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("command path fallback %q has invalid mode %q", from, spec.Mode)
|
|
}
|
|
entries = append(entries, entry)
|
|
}
|
|
|
|
sort.Slice(entries, func(i, j int) bool { return entries[i].From < entries[j].From })
|
|
return entries, nil
|
|
}
|
|
|
|
func validateAuthoredCommandFallbackPath(raw string) (string, error) {
|
|
path := strings.TrimSpace(raw)
|
|
if path == "" {
|
|
return "", fmt.Errorf("path is empty")
|
|
}
|
|
if path != raw || normalizeSchemaCLIPath(path) != path || !commandFallbackPathPattern.MatchString(path) {
|
|
return "", fmt.Errorf("path %q is not a normalized command path without leading dws or flags", raw)
|
|
}
|
|
return path, nil
|
|
}
|
|
|
|
// ReduceCommandPathFallbacks validates the reviewed recovery table against the
|
|
// live distribution-owned Cobra tree. It refuses to turn an existing command
|
|
// or stable alias into a hidden rewrite. The sole exception is an explicitly
|
|
// annotated hint-only compatibility node, which contains no business action.
|
|
func ReduceCommandPathFallbacks(root *cobra.Command) ([]CommandPathFallback, error) {
|
|
if root == nil {
|
|
return nil, fmt.Errorf("command path fallback source root is nil")
|
|
}
|
|
entries, err := LoadCommandPathFallbacks()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
byFrom := make(map[string]CommandPathFallback, len(entries))
|
|
for _, entry := range entries {
|
|
byFrom[entry.From] = entry
|
|
}
|
|
var problems []string
|
|
for _, entry := range entries {
|
|
match, resolveErr := resolveExactCobraPath(root, entry.From)
|
|
if resolveErr != nil {
|
|
problems = append(problems, fmt.Sprintf("from %q cannot be resolved safely: %v", entry.From, resolveErr))
|
|
} else if match.Command != nil && !cmdutil.IsHintOnlyCommand(match.Command) {
|
|
problems = append(problems, fmt.Sprintf("from %q collides with a real Cobra command or alias", entry.From))
|
|
}
|
|
|
|
switch entry.Mode {
|
|
case CommandPathFallbackRewrite:
|
|
if _, chained := byFrom[entry.To]; chained {
|
|
problems = append(problems, fmt.Sprintf("rewrite %q targets fallback source %q; chained fallbacks are forbidden", entry.From, entry.To))
|
|
}
|
|
problems = append(problems, validateCommandFallbackTarget(root, entry.From, entry.To, true)...)
|
|
case CommandPathFallbackAmbiguous:
|
|
for _, candidate := range entry.Candidates {
|
|
if _, chained := byFrom[candidate]; chained {
|
|
problems = append(problems, fmt.Sprintf("ambiguous fallback %q candidate %q is another fallback source", entry.From, candidate))
|
|
}
|
|
// Ambiguous recovery never dispatches. It may therefore point the
|
|
// caller at both canonical shortcuts and native leaves, while exact
|
|
// rewrites keep the stricter +shortcut identity boundary.
|
|
problems = append(problems, validateCommandFallbackTarget(root, entry.From, candidate, false)...)
|
|
}
|
|
default:
|
|
problems = append(problems, fmt.Sprintf("fallback %q has unsupported mode %q", entry.From, entry.Mode))
|
|
}
|
|
}
|
|
|
|
if len(problems) > 0 {
|
|
sort.Strings(problems)
|
|
return nil, fmt.Errorf("command path fallback reduction failed:\n - %s", strings.Join(problems, "\n - "))
|
|
}
|
|
return cloneCommandPathFallbacks(entries), nil
|
|
}
|
|
|
|
func validateCommandFallbackTarget(root *cobra.Command, from, target string, requireShortcutParity bool) []string {
|
|
var problems []string
|
|
if commandFallbackService(from) != commandFallbackService(target) {
|
|
problems = append(problems, fmt.Sprintf("fallback %q crosses service boundary to %q", from, target))
|
|
}
|
|
if requireShortcutParity && commandFallbackHasShortcut(from) != commandFallbackHasShortcut(target) {
|
|
problems = append(problems, fmt.Sprintf("fallback %q and target %q disagree on +shortcut identity", from, target))
|
|
}
|
|
match, err := resolveExactCobraPath(root, target)
|
|
if err != nil {
|
|
return append(problems, fmt.Sprintf("fallback %q target %q cannot be resolved safely: %v", from, target, err))
|
|
}
|
|
if match.Command == nil {
|
|
return append(problems, fmt.Sprintf("fallback %q target %q does not exist", from, target))
|
|
}
|
|
if match.UsedAlias {
|
|
problems = append(problems, fmt.Sprintf("fallback %q target %q must use canonical Cobra names, not an alias", from, target))
|
|
}
|
|
if !runnableSchemaLeaf(match.Command) || !match.Command.IsAvailableCommand() {
|
|
problems = append(problems, fmt.Sprintf("fallback %q target %q is not a public runnable Cobra leaf", from, target))
|
|
}
|
|
return problems
|
|
}
|
|
|
|
func commandFallbackService(path string) string {
|
|
parts := strings.Fields(path)
|
|
if len(parts) == 0 {
|
|
return ""
|
|
}
|
|
return parts[0]
|
|
}
|
|
|
|
func commandFallbackHasShortcut(path string) bool {
|
|
for _, part := range strings.Fields(path) {
|
|
if strings.HasPrefix(part, "+") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func cloneCommandPathFallbacks(entries []CommandPathFallback) []CommandPathFallback {
|
|
cloned := make([]CommandPathFallback, len(entries))
|
|
for index, entry := range entries {
|
|
cloned[index] = entry
|
|
cloned[index].Candidates = append([]string(nil), entry.Candidates...)
|
|
}
|
|
return cloned
|
|
}
|