alias-vs-alias collisions in the command meta lookup now resolve to the owner with the lexicographically smallest primary path instead of map iteration order. Move catalogStringVal next to its sibling helpers in command_meta.go and drop the redundant captureBaseHelpFunc alias in the calendar help wrapper. Unify the Safety help annotation to English "(requires --yes)".
159 lines
5.3 KiB
Go
159 lines
5.3 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.
|
|
|
|
// command_meta.go provides the unified metadata consumption API. All runtime
|
|
// consumers (help, schema, agent selection, skill generation) call ResolveMeta
|
|
// to get a CommandMeta struct — one function, one struct, no need to know which
|
|
// of the 6 generation layers a field comes from.
|
|
//
|
|
// This is the "simple consumption" half of the generation/consumption split:
|
|
// - Generation (gen.go + internal/generator/): 6 inputs → catalog snapshot.
|
|
// - Consumption (this file): catalog snapshot → ResolveMeta → CommandMeta.
|
|
|
|
package cli
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// CommandMeta is the complete runtime metadata view for a single command.
|
|
// Consumers read this struct; they never touch the raw catalog maps.
|
|
type CommandMeta struct {
|
|
Identity CommandIdentity
|
|
Safety CommandSafety
|
|
Selection CommandSelection
|
|
}
|
|
|
|
// CommandIdentity is the stable identity of a command.
|
|
type CommandIdentity struct {
|
|
CLIPath string // "dev app delete"
|
|
Canonical string // "dev.delete_dev_app"
|
|
Aliases []string // ["search", ...]
|
|
ProductID string // "devapp"
|
|
Title string // one-line description
|
|
}
|
|
|
|
// CommandSelection is the agent-facing selection metadata.
|
|
type CommandSelection struct {
|
|
AgentSummary string
|
|
UseWhen []string
|
|
AvoidWhen []string
|
|
Examples []string
|
|
}
|
|
|
|
var (
|
|
metaByCLIPathOnce sync.Once
|
|
metaByCLIPath map[string]CommandMeta
|
|
)
|
|
|
|
// initMetaByCLIPath builds the cli_path → CommandMeta lookup from the embedded
|
|
// catalog. Runs once (sync.Once); the catalog is already decoded at package init.
|
|
func initMetaByCLIPath() {
|
|
metaByCLIPath = buildMetaByCLIPath(embeddedSchemaCatalog())
|
|
}
|
|
|
|
// buildMetaByCLIPath constructs the lookup from a loaded catalog snapshot.
|
|
// Split from initMetaByCLIPath so malformed-snapshot guards stay testable.
|
|
func buildMetaByCLIPath(loaded loadedSchemaCatalog) map[string]CommandMeta {
|
|
lookup := make(map[string]CommandMeta)
|
|
if loaded.Snapshot.Tools == nil {
|
|
return lookup
|
|
}
|
|
metas := make([]CommandMeta, 0, len(loaded.Snapshot.Tools))
|
|
for _, tool := range loaded.Snapshot.Tools {
|
|
cliPath := catalogStringVal(tool, "cli_path")
|
|
if cliPath == "" {
|
|
continue
|
|
}
|
|
meta := CommandMeta{
|
|
Identity: CommandIdentity{
|
|
CLIPath: cliPath,
|
|
Canonical: catalogStringVal(tool, "canonical_path"),
|
|
Aliases: catalogStringSliceVal(tool, "aliases"),
|
|
ProductID: catalogStringVal(tool, "product_id"),
|
|
Title: catalogStringVal(tool, "title"),
|
|
},
|
|
Safety: CommandSafety{
|
|
Effect: catalogStringVal(tool, "effect"),
|
|
Risk: catalogStringVal(tool, "risk"),
|
|
Confirmation: catalogStringVal(tool, "confirmation"),
|
|
Idempotency: catalogStringVal(tool, "idempotency"),
|
|
},
|
|
Selection: CommandSelection{
|
|
AgentSummary: catalogStringVal(tool, "agent_summary"),
|
|
UseWhen: catalogStringSliceVal(tool, "use_when"),
|
|
AvoidWhen: catalogStringSliceVal(tool, "avoid_when"),
|
|
Examples: catalogStringSliceVal(tool, "examples"),
|
|
},
|
|
}
|
|
lookup[cliPath] = meta
|
|
metas = append(metas, meta)
|
|
}
|
|
// Register compat alias paths (e.g. "report list") against the same
|
|
// metadata in a second pass, sorted by primary cli_path: primary paths
|
|
// always win (registered above, aliases only fill vacancies), and an
|
|
// alias-vs-alias collision resolves deterministically to the owner with
|
|
// the lexicographically smallest primary path — Snapshot.Tools is a map,
|
|
// so relying on iteration order would make the winner vary per process.
|
|
sort.Slice(metas, func(i, j int) bool {
|
|
return metas[i].Identity.CLIPath < metas[j].Identity.CLIPath
|
|
})
|
|
for _, meta := range metas {
|
|
for _, alias := range meta.Identity.Aliases {
|
|
alias = strings.TrimSpace(alias)
|
|
if alias == "" || alias == meta.Identity.CLIPath {
|
|
continue
|
|
}
|
|
if _, exists := lookup[alias]; !exists {
|
|
lookup[alias] = meta
|
|
}
|
|
}
|
|
}
|
|
return lookup
|
|
}
|
|
|
|
// catalogStringVal reads a string field from a catalog tool map[string]any.
|
|
func catalogStringVal(tool map[string]any, key string) string {
|
|
if v, ok := tool[key].(string); ok {
|
|
return v
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// catalogStringSliceVal reads a []string field from a catalog tool map.
|
|
func catalogStringSliceVal(tool map[string]any, key string) []string {
|
|
raw, ok := tool[key].([]any)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
out := make([]string, 0, len(raw))
|
|
for _, v := range raw {
|
|
if s, ok := v.(string); ok {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// ResolveMeta returns the complete metadata for a command identified by its CLI
|
|
// path (e.g. "dev app delete") or one of its compat aliases (e.g. "report list"
|
|
// for "report inbox list"). Returns ok=false for commands not in the embedded
|
|
// catalog (utility commands, hidden commands, shortcuts).
|
|
func ResolveMeta(cliPath string) (CommandMeta, bool) {
|
|
metaByCLIPathOnce.Do(initMetaByCLIPath)
|
|
m, ok := metaByCLIPath[strings.TrimSpace(cliPath)]
|
|
return m, ok
|
|
}
|