Files
dingtalk-workspace-cli/pkg/cmdutil/flagfix.go
T
wxianfeng 93d6fdb17e feat: edition layer for Wukong overlay; promote shared code to pkg/
Introduce a build-time edition hook so downstream overlays (e.g. Wukong)
can customize auth UX, config dir, static server list, visible products,
and extra root commands while keeping the default open-source behavior.

- Add pkg/edition (defaults) and pkg/editiontest contract tests; Makefile
  target edition-test; CI job edition-tests.
- Root: optional RegisterExtraCommands with tool caller adapter; inject
  static servers to skip market discovery when configured; deduplicate
  top-level commands so overlay wins; extend hideNonDirectRuntimeCommands
  with edition VisibleProducts and static commands (recovery/schema/mcp).
- Auth/config: gate login subcommand and auth login hints for embedded
  editions; optional token auto-purge; edition ConfigDir override.
- version: human-readable multi-line output plus JSON with edition,
  architecture, build, commit.
- Relocate internal/{config,convert,validate} to pkg/; add pkg/cli and
  pkg/cmdutil (flags/time helpers).
- CI: optional notify-downstream job (GitLab trigger via secrets) on main
  push; open_source_policy_test adjusted for new layout.

Secrets (repo settings): WUKONG_TRIGGER_TOKEN, WUKONG_TRIGGER_URL — optional;
if unset, downstream step is skipped.

Made-with: Cursor
2026-03-31 15:29:21 +08:00

167 lines
4.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 cmdutil
import (
"fmt"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// FlagFixResult holds the result of SuggestFlagFix analysis.
type FlagFixResult struct {
Suggestion string
AutoFixFlag string
AutoFixValue string
}
// CommonFlagAliases maps commonly misused flag names to their correct equivalents.
var CommonFlagAliases = map[string]string{
"json": "format json",
"output": "format",
"out": "format",
"o": "format",
"silent": "quiet",
"dry": "dry-run",
"force": "yes",
"f": "yes",
"timeout-seconds": "timeout",
"device-flow": "device",
"deviceflow": "device",
}
// DetectNumericTypeError checks if err is a Cobra/pflag numeric type
// validation error. Returns the flag name and the bad value if detected.
func DetectNumericTypeError(err error) (flagName, badValue string, ok bool) {
msg := err.Error()
if !strings.Contains(msg, "strconv.Parse") {
return "", "", false
}
const argPrefix = "invalid argument \""
argIdx := strings.Index(msg, argPrefix)
if argIdx < 0 {
return "", "", false
}
afterArg := msg[argIdx+len(argPrefix):]
argEnd := strings.Index(afterArg, "\"")
if argEnd < 0 {
return "", "", false
}
badVal := afterArg[:argEnd]
const marker = "\" for \"--"
idx := strings.Index(msg, marker)
if idx < 0 {
return "", "", false
}
rest := msg[idx+len(marker):]
endIdx := strings.Index(rest, "\" flag")
if endIdx < 0 {
return "", "", false
}
return rest[:endIdx], badVal, true
}
// SuggestFlagFix detects flag-value concatenation errors, common flag aliases,
// and Levenshtein-close typos.
func SuggestFlagFix(cmd *cobra.Command, flagErr error) FlagFixResult {
msg := flagErr.Error()
const prefix = "unknown flag: --"
idx := strings.Index(msg, prefix)
if idx < 0 {
return FlagFixResult{}
}
body := strings.TrimSpace(msg[idx+len(prefix):])
if alias, ok := CommonFlagAliases[body]; ok {
return FlagFixResult{Suggestion: fmt.Sprintf("Did you mean --%s? Run '%s --help' for options", alias, cmd.CommandPath())}
}
var bestFlag, bestValue string
cmd.Flags().VisitAll(func(f *pflag.Flag) {
name := f.Name
if strings.HasPrefix(body, name) && len(body) > len(name) {
if len(name) > len(bestFlag) {
bestFlag = name
bestValue = body[len(name):]
}
}
})
if bestFlag != "" {
canAutoFix := len(bestValue) > 0
suggestion := fmt.Sprintf("Space required between flag and value: --%s %s", bestFlag, bestValue)
if canAutoFix {
return FlagFixResult{Suggestion: suggestion, AutoFixFlag: bestFlag, AutoFixValue: bestValue}
}
return FlagFixResult{Suggestion: suggestion}
}
bestName, bestDist := "", 999
cmd.Flags().VisitAll(func(f *pflag.Flag) {
d := LevenshteinDist(body, f.Name)
if d < bestDist {
bestDist = d
bestName = f.Name
}
})
threshold := LevenshteinThreshold(len(body))
if bestDist > 0 && bestDist <= threshold && bestName != "" {
return FlagFixResult{Suggestion: fmt.Sprintf("Did you mean --%s?", bestName)}
}
return FlagFixResult{Suggestion: fmt.Sprintf("Run '%s --help' to see available options", cmd.CommandPath())}
}
// LevenshteinThreshold returns the max edit distance allowed based on string length.
func LevenshteinThreshold(nameLen int) int {
if nameLen <= 3 {
return 1
}
if nameLen <= 8 {
return 2
}
return 3
}
// LevenshteinDist returns the edit distance between two strings.
func LevenshteinDist(a, b string) int {
la, lb := len(a), len(b)
if la == 0 {
return lb
}
if lb == 0 {
return la
}
dp := make([]int, lb+1)
for j := 0; j <= lb; j++ {
dp[j] = j
}
for i := 1; i <= la; i++ {
prev := dp[0]
dp[0] = i
for j := 1; j <= lb; j++ {
temp := dp[j]
cost := 1
if a[i-1] == b[j-1] {
cost = 0
}
dp[j] = min(dp[j]+1, min(dp[j-1]+1, prev+cost))
prev = temp
}
}
return dp[lb]
}