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
167 lines
4.4 KiB
Go
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]
|
|
}
|