// 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/spf13/cobra" "github.com/spf13/pflag" ) const SchemaVersion = 1 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"` } // 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"` } // 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) 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()), }) 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 err := validateFlags(command.Path, "local", command.LocalFlags); err != nil { return err } if err := validateFlags(command.Path, "inherited", command.InheritedFlags); 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 } 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), }) }) sort.Slice(flags, func(i, j int) bool { return flags[i].Name < flags[j].Name }) return flags } 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 }