Files
2026-09-10 13:29:26 +08:00

794 lines
29 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"io"
"io/fs"
"sort"
"strings"
"github.com/larksuite/cli/cmd/api"
"github.com/larksuite/cli/cmd/auth"
"github.com/larksuite/cli/cmd/completion"
cmdconfig "github.com/larksuite/cli/cmd/config"
"github.com/larksuite/cli/cmd/doctor"
cmdevent "github.com/larksuite/cli/cmd/event"
"github.com/larksuite/cli/cmd/profile"
"github.com/larksuite/cli/cmd/schema"
"github.com/larksuite/cli/cmd/service"
"github.com/larksuite/cli/cmd/skill"
cmdupdate "github.com/larksuite/cli/cmd/update"
"github.com/larksuite/cli/cmd/whoami"
"github.com/larksuite/cli/extension/command"
"github.com/larksuite/cli/extension/platform"
"github.com/larksuite/cli/internal/affordance"
"github.com/larksuite/cli/internal/apicatalog"
"github.com/larksuite/cli/internal/build"
"github.com/larksuite/cli/internal/cmdmeta"
"github.com/larksuite/cli/internal/cmdpolicy"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/commandhost"
"github.com/larksuite/cli/internal/core"
"github.com/larksuite/cli/internal/hook"
"github.com/larksuite/cli/internal/keychain"
internalplatform "github.com/larksuite/cli/internal/platform"
"github.com/larksuite/cli/internal/recovery"
"github.com/larksuite/cli/internal/registry"
"github.com/larksuite/cli/internal/skillpolicy"
"github.com/larksuite/cli/internal/skillref"
"github.com/larksuite/cli/internal/surface"
"github.com/larksuite/cli/shortcuts"
"github.com/larksuite/cli/shortcuts/common"
"github.com/spf13/cobra"
)
// BuildOption configures optional aspects of the command tree construction.
type BuildOption func(*buildConfig)
type buildConfig struct {
streams *cmdutil.IOStreams
keychain keychain.KeychainAccess
globals GlobalOptions
invocationArgs []string
presentation restrictionPresentationConfig
skipPlugins bool
skipStrictMode bool
skipService bool
deferStartup bool
apiCatalog *apicatalog.Catalog
// catalogOpener yields this build's lazy Catalog handle. Opening reads only
// the manifest; service bodies are parsed on first navigation.
catalogOpener func() (apicatalog.Catalog, error)
pluginProvider func() []platform.Plugin
afterCatalogOpen func()
hideProfileSet bool
commandSets []command.Set
}
// buildRuntime owns presentation state for exactly one command tree. Factory
// remains the business dependency container; distribution policy never enters
// it. The embedded pointer preserves convenient access to Factory fields in
// cmd-internal tests without exposing the surface plan to business packages.
type buildRuntime struct {
*cmdutil.Factory
surface *surface.Plan
recovery *recovery.Projector
skillReferences *skillref.Resolver
help *service.HelpRenderer
}
// WithStartupBrand is retained for source compatibility with wrapper mains.
// Deprecated: the committed API catalog is brand-independent, so this option
// no longer changes command construction.
func WithStartupBrand(_ core.LarkBrand) BuildOption {
return func(*buildConfig) {}
}
// WithIO sets the IO streams for the CLI by wrapping raw reader/writers.
// Terminal detection is delegated to cmdutil.NewIOStreams.
func WithIO(in io.Reader, out, errOut io.Writer) BuildOption {
return func(c *buildConfig) {
c.streams = cmdutil.NewIOStreams(in, out, errOut)
}
}
// WithKeychain sets the secret storage backend. If not provided, the platform keychain is used.
func WithKeychain(kc keychain.KeychainAccess) BuildOption {
return func(c *buildConfig) {
c.keychain = kc
}
}
// embeddedSkillContent is the skill tree wired into cmdutil.Factory.SkillContent
// at build time. It is registered by the repo-root package main's init via
// SetEmbeddedSkillContent — it cannot be threaded through main.go without
// breaking the single-file preview build (see skills_embed.go). nil in builds
// that embed no skills; the `skills` commands then return a typed internal error.
var embeddedSkillContent fs.FS
// SetEmbeddedSkillContent registers the embedded skill tree. Called from the
// repo-root package main's init; a wrapper main can call it before Execute to
// supply its own skill content.
func SetEmbeddedSkillContent(fsys fs.FS) { embeddedSkillContent = fsys }
// SetEmbeddedAffordanceContent registers the per-domain command guidance tree.
// Wrapper mains should wire the repository's affordance directory alongside
// embedded skills so generic --help presentation remains complete and skill
// references follow the composed distribution.
func SetEmbeddedAffordanceContent(fsys fs.FS) { affordance.SetSource(fsys) }
// HideProfile sets the visibility policy for the root-level --profile flag.
// When hide is true the flag stays registered (so existing invocations still
// parse) but is omitted from help and shell completion. Typically called as
// HideProfile(isSingleAppMode()).
func HideProfile(hide bool) BuildOption {
return func(c *buildConfig) {
c.globals.HideProfile = hide
c.hideProfileSet = true
}
}
// WithInvocationArgs enables target-aware assembly for Build. The provided
// arguments are used both to select the Catalog and Shortcut domains and as
// Cobra's execution arguments, keeping assembly and dispatch on one source of
// truth. Build remains a full-tree constructor when this option is omitted.
//
// An explicitly empty slice represents a bare invocation and therefore still
// assembles the complete tree for root help. The slice is copied so later
// caller mutation cannot change the planned or executed command.
func WithInvocationArgs(args []string) BuildOption {
return func(c *buildConfig) {
c.invocationArgs = append(make([]string, 0, len(args)), args...)
}
}
// WithoutPlugins builds only repository-owned commands. It is intended for
// inspection tools that need a deterministic command tree.
func WithoutPlugins() BuildOption {
return func(c *buildConfig) {
c.skipPlugins = true
}
}
// WithoutStrictMode builds the complete repository-owned command tree without
// applying user/profile strict-mode pruning. It is intended for offline
// inspection tools, not production execution.
func WithoutStrictMode() BuildOption {
return func(c *buildConfig) {
c.skipStrictMode = true
}
}
// WithoutServiceCommands builds only hand-authored commands. It is intended for
// repository quality gates that should not depend on the generated API command
// surface of the embedded Catalog.
func WithoutServiceCommands() BuildOption {
return func(c *buildConfig) {
c.skipService = true
}
}
// WithServiceCatalog uses catalog as the authoritative metadata for the entire
// command build instead of opening the embedded snapshot. It is primarily
// intended for deterministic inspection tools and tests.
func WithServiceCatalog(catalog apicatalog.Catalog) BuildOption {
return func(c *buildConfig) {
c.apiCatalog = &catalog
}
}
// WithCommandSets adds build-time business commands to an independently built CLI.
// The supplied declarations are copied when this option is created and compiled
// as one atomic contribution during command-tree construction.
func WithCommandSets(sets ...command.Set) BuildOption {
captured := command.CloneSets(sets)
return func(c *buildConfig) {
c.commandSets = append(c.commandSets, command.CloneSets(captured)...)
}
}
// Build constructs the full command tree by default. When
// WithInvocationArgs is provided, it constructs only the command domains that
// those arguments can reach and configures Cobra to execute the same arguments.
//
// Build also installs registered plugins and emits the Startup lifecycle event
// during assembly -- so Plugin.On(Startup) handlers run even if the returned
// command is never dispatched. The matching Shutdown event is only emitted by
// Execute; callers that bypass Execute will not see Shutdown fire.
//
// Returns only the cobra.Command; Factory and hook Registry are internal.
// Use Execute for the standard production entry point.
func Build(ctx context.Context, inv cmdutil.InvocationContext, opts ...BuildOption) *cobra.Command {
cfg := resolveBuildConfig(opts)
if cfg.invocationArgs != nil {
result, err := buildForArgsWithConfig(ctx, inv, cfg.invocationArgs, cfg)
if err != nil {
result = failedCatalogBuild(ctx, inv, cfg, err)
}
// The copy must stay non-nil: Cobra treats nil args as "read os.Args",
// which would hand an explicitly empty invocation the host's arguments.
result.root.SetArgs(append([]string{}, cfg.invocationArgs...))
return result.root
}
_, rootCmd, _ := buildInternalWithConfig(ctx, inv, cfg)
return rootCmd
}
// buildResult is what one assembly produces. The registry is nil when plugin
// install failed (a FailClosed guard is installed) or no plugin produced hooks;
// callers that wire Shutdown emit must nil-check before calling hook.Emit.
type buildResult struct {
runtime *buildRuntime
root *cobra.Command
registry *hook.Registry
}
// buildForArgsWithConfig constructs only the command domains that args can
// reach. Cobra remains responsible for parsing and executing args after
// assembly. A Catalog failure is returned typed so the caller can substitute
// failedCatalogBuild.
func buildForArgsWithConfig(
ctx context.Context,
inv cmdutil.InvocationContext,
args []string,
cfg *buildConfig,
) (*buildResult, error) {
cfg = normalizeBuildConfig(cfg)
runtime, root, reg, err := assembleInternal(ctx, inv, assemblyRequest{routed: true, args: args}, cfg)
if err != nil {
return nil, err
}
return &buildResult{runtime: runtime, root: root, registry: reg}, nil
}
// failedCatalogBuild is the fail-closed assembly used when the Catalog cannot
// be opened: a root whose every dispatch reports err, over a runtime with an
// empty surface so recovery hints reference nothing that was not built. Every
// build entry point that can hit a Catalog failure substitutes this result so
// the recovery wiring cannot drift between them.
func failedCatalogBuild(ctx context.Context, inv cmdutil.InvocationContext, cfg *buildConfig, err error) *buildResult {
f := cmdutil.NewDefault(cfg.streams, inv)
runtime := &buildRuntime{Factory: f, surface: surface.NewPlan(nil)}
runtime.recovery = recovery.NewProjector(func() *surface.Plan { return runtime.surface })
f.Recovery = runtime.recovery
return &buildResult{runtime: runtime, root: newCatalogFailureRoot(ctx, cfg, err)}
}
func resolveBuildConfig(opts []BuildOption) *buildConfig {
cfg := &buildConfig{}
for _, o := range opts {
if o != nil {
o(cfg)
}
}
return normalizeBuildConfig(cfg)
}
func normalizeBuildConfig(cfg *buildConfig) *buildConfig {
if cfg == nil {
cfg = &buildConfig{}
}
if cfg.streams == nil {
cfg.streams = cmdutil.SystemIO()
}
if cfg.catalogOpener == nil {
cfg.catalogOpener = openEmbeddedCatalog
}
return cfg
}
func openEmbeddedCatalog() (apicatalog.Catalog, error) {
snapshot, err := registry.OpenSnapshot()
if err != nil {
return apicatalog.Catalog{}, err
}
return snapshot.Catalog(), nil
}
// buildInternal is a pure assembly function: it wires the command tree from
// inv and BuildOptions alone. Any state-dependent decision (disk, network,
// env) belongs in the caller and must be threaded in via BuildOption.
//
// Returns (runtime, rootCmd, registry). The registry is nil when plugin
// install failed (FailClosed guard installed) or when no plugin produced
// hooks; callers that wire Shutdown emit must nil-check before calling
// hook.Emit.
func buildInternal(ctx context.Context, inv cmdutil.InvocationContext, opts ...BuildOption) (*buildRuntime, *cobra.Command, *hook.Registry) {
return buildInternalWithConfig(ctx, inv, resolveBuildConfig(opts))
}
func frozenPlugins(cfg *buildConfig) []platform.Plugin {
if cfg.skipPlugins {
return nil
}
if cfg.pluginProvider != nil {
return cfg.pluginProvider()
}
return platform.RegisteredPlugins()
}
// resolveShortcutSnapshot compiles this build's business command sets and returns
// one snapshot carrying built-in and external shortcuts together. On failure the
// built-in snapshot is returned so assembly can install a fail-closed guard.
func resolveShortcutSnapshot(sets []command.Set) ([]common.Shortcut, error) {
external, err := commandhost.CompileSets(sets)
if err != nil {
return shortcuts.AllShortcuts(), err
}
return shortcuts.AllShortcutsWithExternal(external)
}
// buildInternalWithConfig assembles the complete command tree from an
// already-applied option snapshot. Target-aware callers use
// buildForArgsWithConfig instead.
func buildInternalWithConfig(ctx context.Context, inv cmdutil.InvocationContext, cfg *buildConfig) (*buildRuntime, *cobra.Command, *hook.Registry) {
cfg = normalizeBuildConfig(cfg)
runtime, root, reg, err := assembleInternal(ctx, inv, assemblyRequest{}, cfg)
if err != nil {
failed := failedCatalogBuild(ctx, inv, cfg, err)
return failed.runtime, failed.root, failed.registry
}
return runtime, root, reg
}
func openCatalog(cfg *buildConfig) (apicatalog.Catalog, error) {
if cfg.apiCatalog != nil {
return *cfg.apiCatalog, nil
}
return cfg.catalogOpener()
}
func newCatalogFailureRoot(ctx context.Context, cfg *buildConfig, catalogErr error) *cobra.Command {
root := &cobra.Command{
Use: "lark-cli",
Short: "Lark/Feishu CLI — OAuth authorization, UAT management, API calls",
Long: rootLong,
Version: build.Version,
SilenceErrors: true,
SilenceUsage: true,
RunE: func(*cobra.Command, []string) error {
return catalogErr
},
}
root.SetContext(ctx)
root.SetIn(cfg.streams.In)
root.SetOut(cfg.streams.Out)
root.SetErr(cfg.streams.ErrOut)
root.DisableFlagParsing = true
return root
}
// assemblyRequest says how much of the domain surface one build must expand.
// The zero value assembles every domain; a routed request lets Cobra decide
// which single domain args reach (see routeDomains) and expands only that one.
type assemblyRequest struct {
routed bool
args []string
}
// assembleInternal is a pure assembly function over the frozen plugin snapshot
// and this build's shortcut snapshot. It never reloads either of them.
//
// Domain expansion happens in two steps so routing and dispatch share one
// parser: every domain is first mounted as an empty stub, Cobra's own Find
// picks the stub (or hand-authored command) args reach, and only that domain's
// Catalog shard and shortcuts are expanded onto the very same *cobra.Command.
// Unselected stubs are removed before any later stage sees the tree, so the
// target tree is a subtree of the full tree by construction.
//
// Returns (runtime, rootCmd, registry). The registry is nil when plugin
// install failed (FailClosed guard installed) or when no plugin produced
// hooks; callers that wire Shutdown emit must nil-check before calling
// hook.Emit. A non-nil error is a Catalog open or shard failure; the caller
// substitutes a fail-closed root.
func assembleInternal(
ctx context.Context,
inv cmdutil.InvocationContext,
request assemblyRequest,
cfg *buildConfig,
) (*buildRuntime, *cobra.Command, *hook.Registry, error) {
// cfg.globals.Profile is left zero here; it's bound to the --profile
// flag in RegisterGlobalFlags and filled by cobra's parse step.
// Reset the legacy process-global diagnostic snapshots before paths that
// may return early. Distribution presentation state is deliberately not
// stored here; it belongs to this build's immutable surface plan.
cmdpolicy.SetActive(nil)
internalplatform.SetActiveInventory(nil)
// Plugins are frozen before the Catalog opens. Any registered plugin
// receives the complete service tree so its policy and hook expectations
// cannot be bypassed by a target-only assembly; the version-only path is
// the one exception, since it never opens the Catalog at all.
plugins := frozenPlugins(cfg)
registeredShortcuts, commandSetErr := resolveShortcutSnapshot(cfg.commandSets)
f := cmdutil.NewDefault(cfg.streams, inv)
if cfg.keychain != nil {
f.Keychain = cfg.keychain
}
f.SkillContent = embeddedSkillContent
runtime := &buildRuntime{Factory: f}
runtime.recovery = recovery.NewProjectorWithContext(func() *surface.Plan {
return runtime.surface
}, recovery.RenderContext{Profile: inv.Profile})
f.Recovery = runtime.recovery
rootCmd := &cobra.Command{
Use: "lark-cli",
Short: "Lark/Feishu CLI — OAuth authorization, UAT management, API calls",
Long: rootLong,
Version: build.Version,
}
rootCmd.SetContext(ctx)
rootCmd.SetIn(cfg.streams.In)
rootCmd.SetOut(cfg.streams.Out)
rootCmd.SetErr(cfg.streams.ErrOut)
// Root-only usage template (curated Usage synopsis + skills footer); see
// rootUsageTemplate.
rootCmd.SetUsageTemplate(rootUsageTemplate)
rootCmd.SilenceErrors = true
// SilenceUsage as a static field (not only in PersistentPreRun) so it also
// covers flag-parse errors, which fail before PreRun runs — otherwise cobra
// dumps usage instead of our structured error. SetFlagErrorFunc on root is
// inherited by every subcommand, turning unknown-flag errors into a
// structured "did you mean" envelope.
rootCmd.SilenceUsage = true
rootCmd.SetFlagErrorFunc(flagDidYouMean)
RegisterGlobalFlags(rootCmd.PersistentFlags(), &cfg.globals)
rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
cmd.SilenceUsage = true
f.CurrentCommand = cmd
}
// Version is the only deterministic no-Catalog invocation: it must succeed
// even when the embedded Catalog is corrupt. Plugins are still installed
// below, and their Startup hooks run against the repository-owned root.
var catalog apicatalog.Catalog
versionOnly := request.routed && isVersionOnlyInvocation(rootCmd, request.args)
if !versionOnly {
var err error
catalog, err = openCatalog(cfg)
if err != nil {
return nil, nil, nil, err
}
if cfg.afterCatalogOpen != nil {
cfg.afterCatalogOpen()
}
}
f.APICatalog = catalog
f.Affordance = affordance.NewResolver(affordance.Source(), catalog)
// Framework-generated help reads this build's final content and exact
// command surface lazily. A second Build therefore cannot rewrite help
// rendered by the first tree.
runtime.help = &service.HelpRenderer{
Guidance: f.Affordance,
SkillContent: func() fs.FS {
if !runtime.surface.CanReference(surface.CommandSkillsRead) {
return nil
}
return runtime.SkillContent
},
SkillReferences: func() *skillref.Resolver { return runtime.skillReferences },
CanReferenceSchema: func() bool {
return runtime.recovery == nil || runtime.recovery.CanReference(recovery.TargetSchema)
},
}
installTipsHelpFunc(rootCmd, runtime.help)
rootCmd.AddCommand(cmdconfig.NewCmdConfigWithRecovery(f, runtime.recovery))
rootCmd.AddCommand(auth.NewCmdAuthWithRecoveryAndShortcuts(f, runtime.recovery, registeredShortcuts))
rootCmd.AddCommand(profile.NewCmdProfile(f))
rootCmd.AddCommand(doctor.NewCmdDoctorWithRecovery(f, runtime.recovery))
rootCmd.AddCommand(whoami.NewCmdWhoamiWithRecovery(f, runtime.recovery))
rootCmd.AddCommand(api.NewCmdApiWithContext(ctx, f, nil))
rootCmd.AddCommand(schema.NewCmdSchemaWithVisibility(f, func(path []string) bool {
return runtime.surface.CanReference(surface.CommandID(strings.Join(path, "/")))
}, nil))
rootCmd.AddCommand(completion.NewCmdCompletion(f))
rootCmd.AddCommand(cmdupdate.NewCmdUpdate(f))
rootCmd.AddCommand(cmdevent.NewCmdEvents(f))
rootCmd.AddCommand(skill.NewCmdSkill(f))
var catalogNames []string
if !cfg.skipService {
catalogNames = catalog.Names()
}
shortcutDomains := shortcuts.ServiceNamesOf(registeredShortcuts)
stubs := mountDomainStubs(rootCmd, catalogNames, shortcutDomains)
selected := selectAllDomains
switch {
case versionOnly:
selected = selectNoDomains
case request.routed:
selected = routeDomains(rootCmd, request.args, catalogNames, shortcutDomains)
if len(plugins) > 0 {
selected = selectAllDomains
}
}
if !cfg.skipService {
names := selected.pick(catalogNames)
if err := catalog.Preload(names...); err != nil {
return nil, nil, nil, err
}
service.RegisterServiceCommandsForNames(ctx, rootCmd, f, catalog, names)
}
shortcuts.RegisterShortcutSnapshotForDomainsWithContext(ctx, rootCmd, f, registeredShortcuts, selected.shortcutSelection())
for name, stub := range stubs {
if !selected.includes(name) {
rootCmd.RemoveCommand(stub)
}
}
if commandSetErr != nil {
installCommandSetErrorGuard(rootCmd, commandSetErr)
return finalizeFailedBuild(runtime, rootCmd)
}
classifyRootCommands(rootCmd)
installUnknownSubcommandGuard(rootCmd)
// Bare `lark-cli` in an interactive terminal offers an interactive upgrade
// before printing help; non-bare invocations and non-TTY are unaffected.
installRootUpgradePrompt(f, rootCmd, runtime.recovery)
if mode := f.ResolveStrictMode(ctx); mode.IsActive() && !cfg.skipStrictMode {
pruneForStrictMode(rootCmd, mode)
}
var (
installResult *internalplatform.InstallResult
pluginRules []cmdpolicy.PluginRule
pluginSkills []skillpolicy.PluginSkill
hookRegistry *hook.Registry
denied map[string]cmdpolicy.Denial
)
if !cfg.skipPlugins {
var installErr error
installResult, installErr = installPluginsAndHooks(plugins, cfg.streams.ErrOut)
if installErr != nil {
installPluginInstallErrorGuard(rootCmd, installErr)
return finalizeFailedBuild(runtime, rootCmd)
}
if installResult != nil {
pluginRules = installResult.PluginRules
pluginSkills = installResult.PluginSkills
hookRegistry = installResult.Registry
}
// Policy errors fail-CLOSED when a plugin contributed (security
// intent must not be silently dropped); yaml-only errors fail-OPEN
// with a warning so a typo can't lock the user out.
var policyErr error
denied, policyErr = applyUserPolicyPruning(rootCmd, pluginRules)
if policyErr != nil {
if len(pluginRules) > 0 {
installPluginConflictGuard(rootCmd, policyErr)
return finalizeFailedBuild(runtime, rootCmd)
}
warnPolicyError(cfg.streams.ErrOut, policyErr)
}
}
// Presentation is an explicit host projection over the exact enforcement
// decisions. With no opt-in, legacy Restrict and YAML policy behavior is
// mechanically unchanged.
var hasConcealedCommands bool
runtime.surface, hasConcealedCommands = applyDistributionPresentation(rootCmd, cfg.presentation, denied)
// Resolve skill assets and canonical references before installing hooks.
// A declared customization is a build-integrity boundary: failure must
// happen before Startup so no lifecycle side effect is stranded.
skillResolution, skillErr := skillpolicy.ResolveWithReferences(embeddedSkillContent, pluginSkills)
if skillErr != nil {
installPluginSkillErrorGuard(rootCmd, skillErr)
return finalizeFailedBuild(runtime, rootCmd)
}
f.SkillContent = skillResolution.Content
runtime.skillReferences = skillResolution.References
f.SkillReferences = skillResolution.References
// Global flags and their environment equivalents belong to the same
// distribution capability. Flag tokens are rejected by applyPluginFlagGate;
// install the equivalent guard for an environment-origin profile before
// hooks, Startup, or business commands can observe the invocation.
if installEnvironmentProfileGate(rootCmd, inv, runtime.surface) {
recordInventory(installResult)
return finalizeFailedBuild(runtime, rootCmd)
}
// Install hooks only on business commands. The concealment-specific help
// command is attached afterwards, preserving Cobra's historical contract
// that help is not observed or wrapped by plugins.
if hookRegistry != nil {
installHooks(rootCmd, hookRegistry)
}
if hasConcealedCommands {
installHelpCommand(rootCmd)
}
finalizeRootCommandGroups(rootCmd, runtime.surface)
if hookRegistry != nil && !cfg.deferStartup {
if err := emitStartup(ctx, hookRegistry); err != nil {
installPluginLifecycleErrorGuard(rootCmd, err)
recordInventory(installResult)
return runtime, rootCmd, nil, nil
}
}
recordInventory(installResult)
return runtime, rootCmd, hookRegistry, nil
}
func finalizeFailedBuild(runtime *buildRuntime, root *cobra.Command) (*buildRuntime, *cobra.Command, *hook.Registry, error) {
finalizeRootCommandGroups(root, runtime.surface)
return runtime, root, nil, nil
}
// isVersionOnlyInvocation reports whether args ask Cobra for nothing but the
// root version: the root flag set parses them cleanly, --version is set, --help
// is not, and no positional token remains that could name a command. Cobra's
// own execute path makes the same decision, so this never diverges from what
// Execute would print.
//
// The probe parses on a throwaway root carrying the same RegisterGlobalFlags
// definitions plus Cobra's default help/version flags. Parsing the real root
// would leave its flag values set, and a fail-closed guard later disables flag
// parsing on that root, so a stale value there could let --version bypass the
// guard.
func isVersionOnlyInvocation(root *cobra.Command, args []string) bool {
// Two Cobra decisions must both land on the root before the Catalog can be
// skipped. Find runs first, before --help/--version exist, on a root that
// has subcommands: `--version --profile x` swallows --profile there and
// leaves x as an unknown command whose error lists the full tree, so it is
// not a version-only invocation even though the flag parse below says so.
dispatch := &cobra.Command{Use: root.Use}
RegisterGlobalFlags(dispatch.PersistentFlags(), &GlobalOptions{})
dispatch.AddCommand(&cobra.Command{Use: "placeholder"})
if target, _, err := dispatch.Find(args); err != nil || target != dispatch {
return false
}
probe := &cobra.Command{Use: root.Use, Version: root.Version}
RegisterGlobalFlags(probe.PersistentFlags(), &GlobalOptions{})
probe.InitDefaultHelpFlag()
probe.InitDefaultVersionFlag()
if err := probe.ParseFlags(args); err != nil {
return false
}
flags := probe.Flags()
if len(flags.Args()) != 0 {
return false
}
// Cobra prints the version only when the flag's value is true; a spelled-out
// `--version=false` falls through to the root help, which needs the tree.
version, err := flags.GetBool("version")
if err != nil {
return false
}
return version && !flags.Changed("help")
}
// domainSelection is the outcome of routing: which mounted domains to expand.
// A nil set with all=false expands nothing (hand-authored or version targets).
type domainSelection struct {
all bool
names map[string]struct{}
}
var (
selectAllDomains = domainSelection{all: true}
selectNoDomains = domainSelection{}
)
func selectDomain(name string) domainSelection {
return domainSelection{names: map[string]struct{}{name: {}}}
}
func (s domainSelection) includes(name string) bool {
if s.all {
return true
}
_, ok := s.names[name]
return ok
}
// pick returns the subset of candidates this selection expands, preserving
// candidate order. It never returns nil for an empty selection so callers can
// distinguish "none" from the shortcut registrar's nil-means-all convention.
func (s domainSelection) pick(candidates []string) []string {
out := make([]string, 0, len(candidates))
for _, name := range candidates {
if s.includes(name) {
out = append(out, name)
}
}
return out
}
// shortcutSelection translates to RegisterShortcutSnapshotForDomainsWithContext's
// contract: nil mounts every domain, a non-nil empty slice mounts none.
func (s domainSelection) shortcutSelection() []string {
if s.all {
return nil
}
out := make([]string, 0, len(s.names))
for name := range s.names {
out = append(out, name)
}
sort.Strings(out)
return out
}
// mountDomainStubs adds one empty command per targetable domain so Cobra can
// route to it. A domain whose name is already a hand-authored root command
// (event) is a shared root: the shortcuts expand onto the existing command, so
// no stub is needed. Stubs carry only routing identity (name and aliases);
// expansion fills description and children, and unselected stubs are removed.
func mountDomainStubs(root *cobra.Command, catalogNames, shortcutDomains []string) map[string]*cobra.Command {
existing := make(map[string]struct{})
for _, cmd := range root.Commands() {
existing[cmd.Name()] = struct{}{}
}
stubs := make(map[string]*cobra.Command)
mount := func(name string) {
if _, taken := existing[name]; taken {
return
}
existing[name] = struct{}{}
stub := &cobra.Command{Use: name, Aliases: shortcuts.ServiceAliases(name)}
root.AddCommand(stub)
stubs[name] = stub
}
for _, name := range catalogNames {
mount(name)
}
for _, name := range shortcutDomains {
mount(name)
}
return stubs
}
// routeDomains lets Cobra's Find decide which root child args reach and maps
// it to a domain selection. Find is called on the root exactly as ExecuteC
// will call it: no flag is registered here that ExecuteC would not have
// registered at that point (in particular --help/--version are added only
// after Find), so routing cannot disagree with execution. Anything Find cannot
// resolve — the bare root, a leading --help/--version, an unknown first token —
// conservatively expands every domain so Cobra's real error, help, and version
// paths see the complete tree.
func routeDomains(root *cobra.Command, args []string, catalogNames, shortcutDomains []string) domainSelection {
target, _, err := root.Find(args)
if err != nil || target == nil || target == root {
return selectAllDomains
}
if cmdmeta.RequiresFullTree(target) {
return selectAllDomains
}
for target.Parent() != root {
target = target.Parent()
}
name := target.Name()
for _, domain := range catalogNames {
if domain == name {
return selectDomain(name)
}
}
for _, domain := range shortcutDomains {
if domain == name {
return selectDomain(name)
}
}
return selectNoDomains
}