From 80613412ecbd6198d067c45a1ea827281e3d87d1 Mon Sep 17 00:00:00 2001 From: xuzhuocong Date: Fri, 15 May 2026 08:30:28 +0000 Subject: [PATCH 1/2] feat(common): add FlagHints + did-you-mean unknown-flag error enhancement Add framework-level unknown-flag error interception in shortcuts/common/. When a user or AI agent passes an unrecognized flag, cobra's FlagErrorFunc now returns structured JSON (via output.ExitError) instead of plain-text, with an optional "did you mean: --X?" hint. Changes: - types.go: add FlagHints map[string]string field to Shortcut struct - flag_suggest.go: implement extractUnknownFlagName, editDistance, min3, flagSuggestThreshold, collectKnownFlagNamesFromCmd, didYouMeanFlag, wrapFlagError with four-priority logic (FlagHints > edit-distance > no-match > non-unknown-flag) - runner.go: add SilenceUsage: true to cobra.Command literal and register SetFlagErrorFunc before parent.AddCommand in mountDeclarative - mail shortcuts: populate FlagHints for +draft-edit, +message, +thread, +triage, +send with domain-specific misuse mappings sprint: S2 --- shortcuts/common/flag_suggest.go | 165 +++++++++++++++ shortcuts/common/flag_suggest_test.go | 282 ++++++++++++++++++++++++++ shortcuts/common/runner.go | 13 +- shortcuts/common/types.go | 7 + shortcuts/mail/mail_draft_edit.go | 11 + shortcuts/mail/mail_message.go | 4 + shortcuts/mail/mail_send.go | 6 + shortcuts/mail/mail_thread.go | 4 + shortcuts/mail/mail_triage.go | 8 + 9 files changed, 496 insertions(+), 4 deletions(-) create mode 100644 shortcuts/common/flag_suggest.go create mode 100644 shortcuts/common/flag_suggest_test.go diff --git a/shortcuts/common/flag_suggest.go b/shortcuts/common/flag_suggest.go new file mode 100644 index 000000000..4a0cf6fad --- /dev/null +++ b/shortcuts/common/flag_suggest.go @@ -0,0 +1,165 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package common + +import ( + "strings" + + "github.com/larksuite/cli/internal/output" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// unknownFlagPrefixes lists the pflag error message prefixes that indicate an +// unknown flag. The list is ordered longest-first so we match the most specific +// prefix first and extract the correct flag name. +var unknownFlagPrefixes = []string{ + "unknown flag: --", + "flag provided but not defined: --", +} + +// extractUnknownFlagName returns the bare flag name (without "--") from a pflag +// error message, or "" if the message does not match a known unknown-flag +// pattern (e.g. "bad flag syntax: -", "unknown shorthand flag: 'x' in -xfoo"). +func extractUnknownFlagName(msg string) string { + for _, prefix := range unknownFlagPrefixes { + if strings.HasPrefix(msg, prefix) { + return strings.TrimPrefix(msg, prefix) + } + } + return "" +} + +// editDistance computes the Levenshtein edit distance between two rune slices. +// It is rune-aware so non-ASCII flag names (theoretically possible) are handled +// correctly. +func editDistance(a, b string) int { + ra, rb := []rune(a), []rune(b) + la, lb := len(ra), len(rb) + if la == 0 { + return lb + } + if lb == 0 { + return la + } + + // Single-row DP: prev[j] = edit distance between ra[:i] and rb[:j]. + prev := make([]int, lb+1) + for j := range prev { + prev[j] = j + } + curr := make([]int, lb+1) + + for i := 1; i <= la; i++ { + curr[0] = i + for j := 1; j <= lb; j++ { + cost := 1 + if ra[i-1] == rb[j-1] { + cost = 0 + } + curr[j] = min3( + curr[j-1]+1, // insert + prev[j]+1, // delete + prev[j-1]+cost, // replace + ) + } + prev, curr = curr, prev + } + return prev[lb] +} + +func min3(a, b, c int) int { + if a < b { + if a < c { + return a + } + return c + } + if b < c { + return b + } + return c +} + +// flagSuggestThreshold returns the maximum edit distance we accept when +// suggesting a flag candidate. Tighter for short names (higher false-positive +// risk), looser for long names. +// +// rune len ≤ 3 → 1 +// rune len 4–7 → 2 +// rune len ≥ 8 → 3 +func flagSuggestThreshold(name string) int { + n := len([]rune(name)) + switch { + case n <= 3: + return 1 + case n <= 7: + return 2 + default: + return 3 + } +} + +// collectKnownFlagNamesFromCmd collects names via the standard pflag +// VisitAll interface used by cobra. +func collectKnownFlagNamesFromCmd(cmd *cobra.Command) []string { + var names []string + cmd.Flags().VisitAll(func(f *pflag.Flag) { + names = append(names, f.Name) + }) + return names +} + +// didYouMeanFlag searches knownFlags for the closest match to unknown using +// Levenshtein edit distance. Returns (bestCandidate, distance); returns +// ("", -1) when no candidate falls within the dynamic threshold. +func didYouMeanFlag(unknown string, knownFlags []string) (string, int) { + threshold := flagSuggestThreshold(unknown) + best, bestDist := "", threshold+1 + for _, known := range knownFlags { + d := editDistance(unknown, known) + if d < bestDist { + best, bestDist = known, d + } + } + if bestDist > threshold { + return "", -1 + } + return best, bestDist +} + +// wrapFlagError is the cobra FlagErrorFunc injected by mountDeclarative. +// It implements the four-priority logic: +// +// 1. FlagHints exact match → unknown_flag + "did you mean: --?" +// 2. edit-distance match → unknown_flag + "did you mean: --?" +// 3. unknown flag, no match → validation_error, no hint +// 4. non-unknown-flag error → validation_error, no hint +func wrapFlagError(s *Shortcut, cmd *cobra.Command, err error) error { + msg := err.Error() + name := extractUnknownFlagName(msg) + + if name == "" { + // Priority 4: not an unknown-flag error (e.g. bad syntax, shorthand). + return output.ErrValidation(msg) + } + + // Priority 1: per-shortcut FlagHints exact match. + if s.FlagHints != nil { + if correct, ok := s.FlagHints[name]; ok { + hint := "did you mean: --" + correct + "?" + return output.ErrWithHint(output.ExitValidation, "unknown_flag", msg, hint) + } + } + + // Priority 2: edit-distance fallback over registered flags. + knownFlags := collectKnownFlagNamesFromCmd(cmd) + if candidate, _ := didYouMeanFlag(name, knownFlags); candidate != "" { + hint := "did you mean: --" + candidate + "?" + return output.ErrWithHint(output.ExitValidation, "unknown_flag", msg, hint) + } + + // Priority 3: no suggestion available. + return output.ErrValidation(msg) +} diff --git a/shortcuts/common/flag_suggest_test.go b/shortcuts/common/flag_suggest_test.go new file mode 100644 index 000000000..d180eb6f4 --- /dev/null +++ b/shortcuts/common/flag_suggest_test.go @@ -0,0 +1,282 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package common + +import ( + "strings" + "testing" + + "github.com/larksuite/cli/internal/output" + "github.com/spf13/cobra" +) + +// ─── extractUnknownFlagName ─────────────────────────────────────────────────── + +func TestExtractUnknownFlagName(t *testing.T) { + tests := []struct { + msg string + want string + }{ + {"unknown flag: --foo", "foo"}, + {"flag provided but not defined: --bar", "bar"}, + {"bad flag syntax: -", ""}, + {"unknown shorthand flag: 'x' in -xfoo", ""}, + } + for _, tt := range tests { + got := extractUnknownFlagName(tt.msg) + if got != tt.want { + t.Errorf("extractUnknownFlagName(%q) = %q, want %q", tt.msg, got, tt.want) + } + } +} + +// ─── editDistance ───────────────────────────────────────────────────────────── + +func TestEditDistance(t *testing.T) { + tests := []struct { + a, b string + want int + }{ + {"", "abc", 3}, + {"abc", "abc", 0}, + {"kitten", "sitting", 3}, + {"你好", "你们好", 1}, + {"a", "b", 1}, + {"abc", "", 3}, + } + for _, tt := range tests { + got := editDistance(tt.a, tt.b) + if got != tt.want { + t.Errorf("editDistance(%q, %q) = %d, want %d", tt.a, tt.b, got, tt.want) + } + } +} + +// ─── flagSuggestThreshold ──────────────────────────────────────────────────── + +func TestFlagSuggestThreshold(t *testing.T) { + tests := []struct { + name string + want int + }{ + {"ab", 1}, // length 2 + {"five5", 2}, // length 5 + {"tencharstr0", 3}, // length 11 + {"abc", 1}, // length 3 (boundary) + {"four", 2}, // length 4 (boundary) + {"eightchr", 3}, // length 8 (boundary) + } + for _, tt := range tests { + got := flagSuggestThreshold(tt.name) + if got != tt.want { + t.Errorf("flagSuggestThreshold(%q) = %d, want %d", tt.name, got, tt.want) + } + } +} + +// ─── didYouMeanFlag ────────────────────────────────────────────────────────── + +func TestDidYouMeanFlag_Hit(t *testing.T) { + best, dist := didYouMeanFlag("patch-fil", []string{"patch-file", "dry-run"}) + if best != "patch-file" { + t.Errorf("expected patch-file, got %q", best) + } + if dist != 1 { + t.Errorf("expected dist=1, got %d", dist) + } +} + +func TestDidYouMeanFlag_Miss(t *testing.T) { + _, dist := didYouMeanFlag("xyz", []string{"patch-file", "subject"}) + if dist != -1 { + t.Errorf("expected dist=-1 (no match), got %d", dist) + } +} + +func TestDidYouMeanFlag_Empty(t *testing.T) { + best, dist := didYouMeanFlag("foo", []string{}) + if best != "" || dist != -1 { + t.Errorf("expected (\"\", -1), got (%q, %d)", best, dist) + } +} + +func TestDidYouMeanFlag_Tie(t *testing.T) { + // Two candidates both at distance 1; should return the first in declaration order. + best, _ := didYouMeanFlag("fooa", []string{"foob", "fooc"}) + if best != "foob" { + t.Errorf("tie-break: expected foob (first in order), got %q", best) + } +} + +// ─── wrapFlagError ────────────────────────────────────────────────────────── + +// newTestCmd creates a minimal cobra command with the given flag names registered. +func newTestCmd(flagNames ...string) *cobra.Command { + cmd := &cobra.Command{Use: "test"} + for _, name := range flagNames { + cmd.Flags().String(name, "", "") + } + return cmd +} + +// asExitError asserts err is *output.ExitError and returns it; fails the test if not. +func asExitError(t *testing.T, err error) *output.ExitError { + t.Helper() + if err == nil { + t.Fatal("expected non-nil error") + } + exitErr, ok := err.(*output.ExitError) + if !ok { + t.Fatalf("expected *output.ExitError, got %T: %v", err, err) + } + return exitErr +} + +func TestWrapFlagError_FlagHints(t *testing.T) { + s := &Shortcut{ + FlagHints: map[string]string{"set-body": "patch-file"}, + } + cmd := newTestCmd("patch-file") + err := wrapFlagError(s, cmd, errMsg("unknown flag: --set-body")) + e := asExitError(t, err) + if e.Detail.Type != "unknown_flag" { + t.Errorf("Type = %q, want unknown_flag", e.Detail.Type) + } + if e.Detail.Hint != "did you mean: --patch-file?" { + t.Errorf("Hint = %q, want 'did you mean: --patch-file?'", e.Detail.Hint) + } +} + +func TestWrapFlagError_FlagHints_Format(t *testing.T) { + s := &Shortcut{ + FlagHints: map[string]string{"set-body": "patch-file"}, + } + cmd := newTestCmd("patch-file") + err := wrapFlagError(s, cmd, errMsg("unknown flag: --set-body")) + e := asExitError(t, err) + hint := e.Detail.Hint + if !strings.HasPrefix(hint, "did you mean: --") { + t.Errorf("hint %q does not start with 'did you mean: --'", hint) + } + if !strings.HasSuffix(hint, "?") { + t.Errorf("hint %q does not end with '?'", hint) + } +} + +func TestWrapFlagError_DidYouMean(t *testing.T) { + s := &Shortcut{} + cmd := newTestCmd("patch-file") + err := wrapFlagError(s, cmd, errMsg("unknown flag: --patch-fil")) + e := asExitError(t, err) + if e.Detail.Type != "unknown_flag" { + t.Errorf("Type = %q, want unknown_flag", e.Detail.Type) + } + if e.Detail.Hint != "did you mean: --patch-file?" { + t.Errorf("Hint = %q, want 'did you mean: --patch-file?'", e.Detail.Hint) + } +} + +func TestWrapFlagError_HintFormatUnified(t *testing.T) { + // FlagHints path + s1 := &Shortcut{FlagHints: map[string]string{"set-body": "patch-file"}} + cmd1 := newTestCmd("patch-file") + err1 := wrapFlagError(s1, cmd1, errMsg("unknown flag: --set-body")) + e1 := asExitError(t, err1) + hint1 := e1.Detail.Hint + + // Edit-distance path + s2 := &Shortcut{} + cmd2 := newTestCmd("patch-file") + err2 := wrapFlagError(s2, cmd2, errMsg("unknown flag: --patch-fil")) + e2 := asExitError(t, err2) + hint2 := e2.Detail.Hint + + // Both should follow the same format pattern + for _, h := range []string{hint1, hint2} { + if !strings.HasPrefix(h, "did you mean: --") || !strings.HasSuffix(h, "?") { + t.Errorf("hint %q has unexpected format", h) + } + } +} + +func TestWrapFlagError_NoHint(t *testing.T) { + s := &Shortcut{} + cmd := newTestCmd("patch-file", "subject") + err := wrapFlagError(s, cmd, errMsg("unknown flag: --zzz")) + e := asExitError(t, err) + if e.Detail.Type != "validation" { + t.Errorf("Type = %q, want validation", e.Detail.Type) + } + if e.Detail.Hint != "" { + t.Errorf("Hint = %q, want empty hint for no-match", e.Detail.Hint) + } +} + +func TestWrapFlagError_NonUnknownFlag(t *testing.T) { + s := &Shortcut{} + cmd := newTestCmd() + err := wrapFlagError(s, cmd, errMsg("bad flag syntax: -")) + e := asExitError(t, err) + if e.Detail.Type != "validation" { + t.Errorf("Type = %q, want validation", e.Detail.Type) + } +} + +func TestWrapFlagError_UsageSilenced(t *testing.T) { + // When SilenceUsage is true, cobra should not emit usage text. + // We verify that wrapFlagError itself returns errors without "Usage:" text. + s := &Shortcut{} + cmd := newTestCmd("patch-file") + cmd.SilenceUsage = true + err := wrapFlagError(s, cmd, errMsg("unknown flag: --patch-fil")) + if err == nil { + t.Fatal("expected non-nil error") + } + // Verify the error message does not contain "Usage:" + if strings.Contains(err.Error(), "Usage:") { + t.Errorf("error message unexpectedly contains 'Usage:': %s", err.Error()) + } +} + +func TestWrapFlagError_EmptyKnownFlags(t *testing.T) { + // Shortcut with no registered flags, unknown flag passed — should not panic. + s := &Shortcut{} + cmd := newTestCmd() // no flags + err := wrapFlagError(s, cmd, errMsg("unknown flag: --anything")) + e := asExitError(t, err) + // Should fall through to validation, no panic. + if e.Detail.Type != "validation" { + t.Errorf("Type = %q, want validation", e.Detail.Type) + } +} + +func TestWrapFlagError_NilFlagHints(t *testing.T) { + // FlagHints is nil — should not panic, should use edit-distance fallback. + s := &Shortcut{FlagHints: nil} + cmd := newTestCmd("patch-file") + err := wrapFlagError(s, cmd, errMsg("unknown flag: --patch-fil")) + e := asExitError(t, err) + // Should find patch-file via edit distance, not panic. + if e.Detail.Type != "unknown_flag" { + t.Errorf("Type = %q, want unknown_flag", e.Detail.Type) + } +} + +// ─── BenchmarkEditDistance ─────────────────────────────────────────────────── + +func BenchmarkEditDistance(b *testing.B) { + a := "message-id" + c := "mail-address" + b.ResetTimer() + for i := 0; i < b.N; i++ { + editDistance(a, c) + } +} + +// ─── helpers ───────────────────────────────────────────────────────────────── + +type simpleErr struct{ msg string } + +func errMsg(msg string) error { return &simpleErr{msg} } +func (e *simpleErr) Error() string { return e.msg } diff --git a/shortcuts/common/runner.go b/shortcuts/common/runner.go index 7e4c8ecef..e8a3c7307 100644 --- a/shortcuts/common/runner.go +++ b/shortcuts/common/runner.go @@ -722,10 +722,11 @@ func (s Shortcut) mountDeclarative(ctx context.Context, parent *cobra.Command, f botOnly := len(shortcut.AuthTypes) == 1 && shortcut.AuthTypes[0] == "bot" cmd := &cobra.Command{ - Use: shortcut.Command, - Short: shortcut.Description, - Hidden: shortcut.Hidden, - Args: rejectPositionalArgs(), + Use: shortcut.Command, + Short: shortcut.Description, + Hidden: shortcut.Hidden, + SilenceUsage: true, // prevent cobra from printing usage text on flag-parse errors + Args: rejectPositionalArgs(), RunE: func(cmd *cobra.Command, _ []string) error { return runShortcut(cmd, f, &shortcut, botOnly) }, @@ -734,6 +735,10 @@ func (s Shortcut) mountDeclarative(ctx context.Context, parent *cobra.Command, f registerShortcutFlagsWithContext(ctx, cmd, f, &shortcut) cmdutil.SetTips(cmd, shortcut.Tips) cmdutil.SetRisk(cmd, shortcut.Risk) + sc := shortcut // capture loop variable (required for go < 1.22; safe for go >= 1.22) + cmd.SetFlagErrorFunc(func(c *cobra.Command, err error) error { + return wrapFlagError(&sc, c, err) + }) parent.AddCommand(cmd) if shortcut.PostMount != nil { shortcut.PostMount(cmd) diff --git a/shortcuts/common/types.go b/shortcuts/common/types.go index 76626c06c..fc2b0b91c 100644 --- a/shortcuts/common/types.go +++ b/shortcuts/common/types.go @@ -51,6 +51,13 @@ type Shortcut struct { Flags []Flag // flag definitions; --dry-run is auto-injected HasFormat bool // auto-inject --format flag (json|pretty|table|ndjson|csv) Tips []string // optional tips shown in --help output + // FlagHints maps a misused flag name (without "--") to the correct flag name + // (without "--"). The framework generates "did you mean: --?" for exact + // matches, keeping the same format as edit-distance suggestions. + // Key contract: no "--" prefix; value must be a valid registered flag name. + // Leave nil when no domain-specific mapping is needed — edit-distance fallback + // still applies. + FlagHints map[string]string Hidden bool // hide from --help / tab completion (still executable); use when deprecating a command in favor of a replacement // Business logic hooks. diff --git a/shortcuts/mail/mail_draft_edit.go b/shortcuts/mail/mail_draft_edit.go index 3ae2a411b..ab2a09136 100644 --- a/shortcuts/mail/mail_draft_edit.go +++ b/shortcuts/mail/mail_draft_edit.go @@ -27,6 +27,17 @@ var MailDraftEdit = common.Shortcut{ Scopes: []string{"mail:user_mailbox.message:modify", "mail:user_mailbox.message:readonly", "mail:user_mailbox:readonly"}, AuthTypes: []string{"user"}, HasFormat: true, + FlagHints: map[string]string{ + "set-body": "patch-file", + "body": "patch-file", + "content": "patch-file", + "message-body": "patch-file", + "to": "set-to", + "recipient": "set-to", + "recipients": "set-to", + "id": "draft-id", + "mail-id": "draft-id", + }, Flags: []common.Flag{ {Name: "from", Default: "me", Desc: "Mailbox email address containing the draft (default: me). Prefer --mailbox for clarity; --from is kept for backward compatibility."}, {Name: "mailbox", Desc: "Mailbox email address that owns the draft (default: falls back to --from, then me). Takes priority over --from when both are set."}, diff --git a/shortcuts/mail/mail_message.go b/shortcuts/mail/mail_message.go index 86fabb51e..08b7b0236 100644 --- a/shortcuts/mail/mail_message.go +++ b/shortcuts/mail/mail_message.go @@ -20,6 +20,10 @@ var MailMessage = common.Shortcut{ Scopes: []string{"mail:user_mailbox.message:readonly", "mail:user_mailbox.message.address:read", "mail:user_mailbox.message.subject:read", "mail:user_mailbox.message.body:read"}, AuthTypes: []string{"user", "bot"}, HasFormat: true, + FlagHints: map[string]string{ + "id": "message-id", + "mail-id": "message-id", + }, Flags: []common.Flag{ {Name: "mailbox", Default: "me", Desc: "email address (default: me)"}, {Name: "message-id", Desc: "Required. Email message ID", Required: true}, diff --git a/shortcuts/mail/mail_send.go b/shortcuts/mail/mail_send.go index 9ea3b422a..807fb6a71 100644 --- a/shortcuts/mail/mail_send.go +++ b/shortcuts/mail/mail_send.go @@ -23,6 +23,12 @@ var MailSend = common.Shortcut{ Risk: "write", Scopes: []string{"mail:user_mailbox.message:send", "mail:user_mailbox.message:modify", "mail:user_mailbox:readonly"}, AuthTypes: []string{"user"}, + FlagHints: map[string]string{ + "recipient": "to", + "recipients": "to", + "message-body": "body", + "content": "body", + }, Flags: []common.Flag{ {Name: "to", Desc: "Recipient email address(es), comma-separated"}, {Name: "subject", Desc: "Email subject. Required unless --template-id supplies a non-empty subject."}, diff --git a/shortcuts/mail/mail_thread.go b/shortcuts/mail/mail_thread.go index 67d3fbebd..d3c69709b 100644 --- a/shortcuts/mail/mail_thread.go +++ b/shortcuts/mail/mail_thread.go @@ -51,6 +51,10 @@ var MailThread = common.Shortcut{ Scopes: []string{"mail:user_mailbox.message:readonly", "mail:user_mailbox.message.address:read", "mail:user_mailbox.message.subject:read", "mail:user_mailbox.message.body:read"}, AuthTypes: []string{"user", "bot"}, HasFormat: true, + FlagHints: map[string]string{ + "thread": "thread-id", + "conversation": "thread-id", + }, Flags: []common.Flag{ {Name: "mailbox", Default: "me", Desc: "email address (default: me)"}, {Name: "thread-id", Desc: "Required. Email thread ID", Required: true}, diff --git a/shortcuts/mail/mail_triage.go b/shortcuts/mail/mail_triage.go index 1274c5f11..a964a21e9 100644 --- a/shortcuts/mail/mail_triage.go +++ b/shortcuts/mail/mail_triage.go @@ -53,6 +53,14 @@ var MailTriage = common.Shortcut{ Risk: "read", Scopes: []string{"mail:user_mailbox.message:readonly", "mail:user_mailbox.message.address:read", "mail:user_mailbox.message.subject:read", "mail:user_mailbox.message.body:read"}, AuthTypes: []string{"user", "bot"}, + FlagHints: map[string]string{ + "folder": "filter", + "mailbox": "mailbox", + "search": "query", + "keyword": "query", + "limit": "max", + "count": "max", + }, Flags: []common.Flag{ {Name: "format", Default: "table", Desc: "output format: table | json | data (json/data output object with pagination fields)"}, {Name: "max", Type: "int", Default: "20", Desc: "maximum number of messages to fetch (1-400; auto-paginates internally)"}, From b06ba1647a95b8c92132a9812c6329f7b58abb32 Mon Sep 17 00:00:00 2001 From: xuzhuocong Date: Fri, 15 May 2026 10:08:04 +0000 Subject: [PATCH 2/2] fix(common): use validation_error type for priority-3/4 flag error paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wrapFlagError priority-3 (no match) and priority-4 (non-unknown-flag error) were calling output.ErrValidation which hardcodes type="validation"; tech-design §处理优先级 lines 115/116 require type="validation_error". Replace both callsites with output.Errorf(output.ExitValidation, "validation_error", msg) so the type string matches the spec without touching the global ErrValidation helper. --- shortcuts/common/flag_suggest.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shortcuts/common/flag_suggest.go b/shortcuts/common/flag_suggest.go index 4a0cf6fad..fc97714f6 100644 --- a/shortcuts/common/flag_suggest.go +++ b/shortcuts/common/flag_suggest.go @@ -142,7 +142,7 @@ func wrapFlagError(s *Shortcut, cmd *cobra.Command, err error) error { if name == "" { // Priority 4: not an unknown-flag error (e.g. bad syntax, shorthand). - return output.ErrValidation(msg) + return output.Errorf(output.ExitValidation, "validation_error", msg) } // Priority 1: per-shortcut FlagHints exact match. @@ -161,5 +161,5 @@ func wrapFlagError(s *Shortcut, cmd *cobra.Command, err error) error { } // Priority 3: no suggestion available. - return output.ErrValidation(msg) + return output.Errorf(output.ExitValidation, "validation_error", msg) }