// 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 interfacesnapshot captures and compares the public Cobra command // surface without executing commands or contacting runtime services. package interfacesnapshot import ( "encoding/json" "fmt" "io" "sort" "strings" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/runtimeannotate" "github.com/spf13/cobra" "github.com/spf13/pflag" ) const SchemaVersion = 3 // FlagAliasOfAnnotation records a framework-originated reviewed relationship // between a retained compatibility flag and an exact canonical Cobra flag. const FlagAliasOfAnnotation = runtimeannotate.AnnotationFlagAliasOf var ( // Framework-owned command subtrees are deliberately excluded. They are // generated by Cobra or shell-completion plumbing rather than DWS product // code, so changes to them are not command-surface compatibility changes. excludedCommandSubtrees = []string{ "dws __complete", "dws __completeNoDesc", "dws completion", "dws help", } // Cobra installs --help lazily on every command. Keeping it would make the // snapshot depend on whether help happened to be rendered before capture. excludedFlags = map[string]bool{"help": true} ) // Rules records the noise filtering contract in every snapshot. A future rule // change is therefore visible instead of silently changing comparison scope. type Rules struct { ExcludedCommandSubtrees []string `json:"excluded_command_subtrees"` ExcludedFlags []string `json:"excluded_flags"` } // Snapshot is a deterministic representation of a Cobra command tree. type Snapshot struct { SchemaVersion int `json:"schema_version"` Rules Rules `json:"rules"` Commands []Command `json:"commands"` } // Command contains compatibility-relevant command metadata. Path always // includes the root command name (for example, "dws chat message send"). type Command struct { Path string `json:"path"` Runnable bool `json:"runnable"` Hidden bool `json:"hidden"` Deprecated string `json:"deprecated,omitempty"` Aliases []string `json:"aliases"` LocalFlags []Flag `json:"local_flags"` InheritedFlags []Flag `json:"inherited_flags"` BoolConstParams map[string]bool `json:"bool_const_params,omitempty"` } // Flag contains the stable pflag contract visible at a command node. type Flag struct { Name string `json:"name"` Shorthand string `json:"shorthand,omitempty"` Type string `json:"type"` Default string `json:"default"` NoOpt string `json:"no_opt,omitempty"` Required bool `json:"required"` Hidden bool `json:"hidden"` Deprecated string `json:"deprecated,omitempty"` AliasOf string `json:"alias_of,omitempty"` // Capture cannot return an error, so it preserves malformed or non-framework // relation evidence here for Validate/Write to reject. Valid snapshots never // serialize this implementation detail. malformedAliasEvidence bool } // Capture walks root without rendering help or executing any command. Both // hidden compatibility commands and hidden flags are retained unless they are // covered by the explicit framework-noise rules above. func Capture(root *cobra.Command) Snapshot { snapshot := Snapshot{ SchemaVersion: SchemaVersion, Rules: Rules{ ExcludedCommandSubtrees: append([]string(nil), excludedCommandSubtrees...), ExcludedFlags: []string{"help"}, }, Commands: []Command{}, } if root == nil { return snapshot } // --version is also installed lazily by Cobra, but unlike --help it is a // real root CLI contract and should be captured. root.InitDefaultVersionFlag() var walk func(*cobra.Command) walk = func(cmd *cobra.Command) { path := normalizePath(cmd.CommandPath()) if commandExcluded(path) { return } aliases := append([]string{}, cmd.Aliases...) for i := range aliases { aliases[i] = strings.TrimSpace(aliases[i]) } aliases = compactSorted(aliases) boolConstParams := corecmd.InterfaceBoolConstParams(cmd) snapshot.Commands = append(snapshot.Commands, Command{ Path: path, Runnable: cmd.Runnable(), Hidden: cmd.Hidden, Deprecated: strings.TrimSpace(cmd.Deprecated), Aliases: aliases, LocalFlags: captureFlags(cmd.LocalFlags()), InheritedFlags: captureFlags(cmd.InheritedFlags()), BoolConstParams: boolConstParams, }) children := append([]*cobra.Command(nil), cmd.Commands()...) sort.Slice(children, func(i, j int) bool { return children[i].Name() < children[j].Name() }) for _, child := range children { walk(child) } } walk(root) sort.Slice(snapshot.Commands, func(i, j int) bool { return snapshot.Commands[i].Path < snapshot.Commands[j].Path }) return snapshot } // Write emits canonical, indented JSON with a trailing newline. func Write(w io.Writer, snapshot Snapshot) error { if err := snapshot.Validate(); err != nil { return err } encoder := json.NewEncoder(w) encoder.SetEscapeHTML(false) encoder.SetIndent("", " ") return encoder.Encode(snapshot) } // Read decodes and validates a snapshot. Unknown fields are rejected so a // comparison never silently ignores a newer contract it does not understand. func Read(r io.Reader) (Snapshot, error) { var snapshot Snapshot decoder := json.NewDecoder(r) decoder.DisallowUnknownFields() if err := decoder.Decode(&snapshot); err != nil { return Snapshot{}, err } if err := decoder.Decode(&struct{}{}); err != io.EOF { if err == nil { return Snapshot{}, fmt.Errorf("interface snapshot contains multiple JSON values") } return Snapshot{}, fmt.Errorf("read trailing interface snapshot data: %w", err) } if err := snapshot.Validate(); err != nil { return Snapshot{}, err } return snapshot, nil } // Validate checks the invariants needed by the comparison algorithm. func (s Snapshot) Validate() error { if s.SchemaVersion != SchemaVersion { return fmt.Errorf("unsupported interface snapshot schema_version %d (want %d)", s.SchemaVersion, SchemaVersion) } seenCommands := make(map[string]bool, len(s.Commands)) for _, command := range s.Commands { if command.Path == "" { return fmt.Errorf("interface snapshot contains an empty command path") } if seenCommands[command.Path] { return fmt.Errorf("interface snapshot contains duplicate command path %q", command.Path) } seenCommands[command.Path] = true if command.BoolConstParams != nil && len(command.BoolConstParams) == 0 { return fmt.Errorf("command %q must omit empty bool_const_params", command.Path) } if err := validateFlags(command.Path, "local", command.LocalFlags); err != nil { return err } if err := validateFlags(command.Path, "inherited", command.InheritedFlags); err != nil { return err } if err := validateFlagAliases(command); err != nil { return err } } return nil } func captureFlags(set *pflag.FlagSet) []Flag { flags := []Flag{} if set == nil { return flags } set.VisitAll(func(flag *pflag.Flag) { if flag == nil || excludedFlags[flag.Name] { return } aliasOf, malformedAliasEvidence := captureFlagAliasOf(flag) flags = append(flags, Flag{ Name: flag.Name, Shorthand: flag.Shorthand, Type: flag.Value.Type(), Default: flag.DefValue, NoOpt: flag.NoOptDefVal, Required: isRequired(flag), Hidden: flag.Hidden, Deprecated: strings.TrimSpace(flag.Deprecated), AliasOf: aliasOf, malformedAliasEvidence: malformedAliasEvidence, }) }) sort.Slice(flags, func(i, j int) bool { return flags[i].Name < flags[j].Name }) return flags } func captureFlagAliasOf(flag *pflag.Flag) (string, bool) { aliasOf, aliasMalformed := exactFlagAnnotation(flag, FlagAliasOfAnnotation) origin, originMalformed := exactFlagAnnotation(flag, runtimeannotate.AnnotationFlagAliasOrigin) if aliasOf == "" && origin == "" && !aliasMalformed && !originMalformed { return "", false } malformed := aliasMalformed || originMalformed || aliasOf == "" || origin != runtimeannotate.FlagAliasOriginCorecmdV1 return aliasOf, malformed } func exactFlagAnnotation(flag *pflag.Flag, key string) (string, bool) { values, exists := flag.Annotations[key] if !exists { return "", false } if len(values) != 1 || values[0] == "" || values[0] != strings.TrimSpace(values[0]) { return "", true } return values[0], false } func isRequired(flag *pflag.Flag) bool { for _, value := range flag.Annotations[cobra.BashCompOneRequiredFlag] { if value == "true" { return true } } return false } func commandExcluded(path string) bool { for _, prefix := range excludedCommandSubtrees { if path == prefix || strings.HasPrefix(path, prefix+" ") { return true } } return false } func normalizePath(path string) string { return strings.Join(strings.Fields(path), " ") } func compactSorted(values []string) []string { sort.Strings(values) out := values[:0] for _, value := range values { if value == "" || (len(out) > 0 && out[len(out)-1] == value) { continue } out = append(out, value) } return out } func validateFlags(path, scope string, flags []Flag) error { seen := make(map[string]bool, len(flags)) for _, flag := range flags { if flag.Name == "" { return fmt.Errorf("command %q contains an empty %s flag name", path, scope) } if seen[flag.Name] { return fmt.Errorf("command %q contains duplicate %s flag %q", path, scope, flag.Name) } seen[flag.Name] = true } return nil } func validateFlagAliases(command Command) error { flags := make(map[string]Flag, len(command.LocalFlags)+len(command.InheritedFlags)) ordered := make([]Flag, 0, len(command.LocalFlags)+len(command.InheritedFlags)) for _, flag := range command.LocalFlags { flags[flag.Name] = flag ordered = append(ordered, flag) } for _, flag := range command.InheritedFlags { if _, exists := flags[flag.Name]; exists { return fmt.Errorf("command %q contains flag %q in both local and inherited scopes", command.Path, flag.Name) } flags[flag.Name] = flag ordered = append(ordered, flag) } for _, flag := range ordered { if flag.malformedAliasEvidence { return fmt.Errorf( "command %q flag --%s must declare one exact %q value and framework origin %q", command.Path, flag.Name, FlagAliasOfAnnotation, runtimeannotate.FlagAliasOriginCorecmdV1, ) } if flag.AliasOf == "" { continue } if !isExactFlagName(flag.AliasOf) { return fmt.Errorf( "command %q flag --%s has non-exact alias_of target %q", command.Path, flag.Name, flag.AliasOf, ) } if flag.AliasOf == flag.Name { return fmt.Errorf("command %q flag --%s cannot alias itself", command.Path, flag.Name) } target, exists := flags[flag.AliasOf] if !exists { return fmt.Errorf( "command %q flag --%s aliases missing flag --%s", command.Path, flag.Name, flag.AliasOf, ) } if target.AliasOf != "" { return fmt.Errorf( "command %q flag --%s aliases --%s, which is itself an alias", command.Path, flag.Name, flag.AliasOf, ) } if flag.Type != target.Type { return fmt.Errorf( "command %q alias flags --%s and --%s have different types %q and %q", command.Path, flag.Name, flag.AliasOf, flag.Type, target.Type, ) } } return nil }