diff --git a/cliext/flags.gen.go b/cliext/flags.gen.go index dafd27121..22c6f9268 100644 --- a/cliext/flags.gen.go +++ b/cliext/flags.gen.go @@ -30,16 +30,20 @@ type CommonOptions struct { FlagSet *pflag.FlagSet } +func (v *CommonOptions) Description() string { + return "These options apply to every command. They control output formatting,\nlogging, and which configuration profile and environment to use.\nOptions that accept an environment variable can be set instead of\npassing the flag each time.\n" +} + func (v *CommonOptions) BuildFlags(f *pflag.FlagSet) { v.FlagSet = f - f.StringVar(&v.Env, "env", "default", "Active environment name (`ENV`).") - f.StringVar(&v.EnvFile, "env-file", "", "Path to environment settings file. Defaults to `$HOME/.config/temporalio/temporal.yaml`.") - f.StringVar(&v.ConfigFile, "config-file", "", "File path to read TOML config from, defaults to `$CONFIG_PATH/temporalio/temporal.toml` where `$CONFIG_PATH` is defined as `$HOME/.config` on Unix, `$HOME/Library/Application Support` on macOS, and `%AppData%` on Windows.") - f.StringVar(&v.Profile, "profile", "", "Profile to use for config file.") - f.BoolVar(&v.DisableConfigFile, "disable-config-file", false, "If set, disables loading environment config from config file.") - f.BoolVar(&v.DisableConfigEnv, "disable-config-env", false, "If set, disables loading environment config from environment variables.") + f.StringVar(&v.Env, "env", "default", "Active environment name (`ENV`). Env: TEMPORAL_ENV.") + f.StringVar(&v.EnvFile, "env-file", "", "Path to environment settings file. Env: TEMPORAL_ENV_FILE.") + f.StringVar(&v.ConfigFile, "config-file", "", "TOML config file path. Env: TEMPORAL_CONFIG_FILE.") + f.StringVar(&v.Profile, "profile", "", "Profile to use for config file. Env: TEMPORAL_PROFILE.") + f.BoolVar(&v.DisableConfigFile, "disable-config-file", false, "Disable loading config from file.") + f.BoolVar(&v.DisableConfigEnv, "disable-config-env", false, "Disable loading config from environment variables.") v.LogLevel = NewFlagStringEnum([]string{"debug", "info", "warn", "error", "never"}, "never") - f.Var(&v.LogLevel, "log-level", "Log level. Default is \"never\" for most commands and \"warn\" for \"server start-dev\". Accepted values: debug, info, warn, error, never.") + f.Var(&v.LogLevel, "log-level", "Log level. Accepted values: debug, info, warn, error, never.") v.LogFormat = NewFlagStringEnum([]string{"text", "json", "pretty"}, "text") f.Var(&v.LogFormat, "log-format", "Log format. Accepted values: text, json.") v.Output = NewFlagStringEnum([]string{"text", "json", "jsonl", "none"}, "text") @@ -50,9 +54,9 @@ func (v *CommonOptions) BuildFlags(f *pflag.FlagSet) { f.Var(&v.Color, "color", "Output coloring. Accepted values: always, never, auto.") f.BoolVar(&v.NoJsonShorthandPayloads, "no-json-shorthand-payloads", false, "Raw payload output, even if the JSON option was used.") v.CommandTimeout = 0 - f.Var(&v.CommandTimeout, "command-timeout", "The command execution timeout. 0s means no timeout.") + f.Var(&v.CommandTimeout, "command-timeout", "Command execution timeout.") v.ClientConnectTimeout = 0 - f.Var(&v.ClientConnectTimeout, "client-connect-timeout", "The client connection timeout. 0s means no timeout.") + f.Var(&v.ClientConnectTimeout, "client-connect-timeout", "Client connection timeout.") } type ClientOptions struct { @@ -77,24 +81,52 @@ type ClientOptions struct { FlagSet *pflag.FlagSet } +func (v *ClientOptions) Description() string { + return "These options apply to commands that connect to a Temporal Service\n(workflow, activity, schedule, etc). They specify the server address,\nnamespace, authentication, and TLS settings. Values are resolved in\norder: CLI flag > environment variable > config file.\n\nTo persist these settings, use:\n temporal config set --prop KEY --value VALUE\n" +} + func (v *ClientOptions) BuildFlags(f *pflag.FlagSet) { v.FlagSet = f - f.StringVar(&v.Address, "address", "localhost:7233", "Temporal Service gRPC endpoint.") + f.StringVar(&v.Address, "address", "localhost:7233", "Temporal Service gRPC endpoint. Env: TEMPORAL_ADDRESS. Config: address.") f.StringVar(&v.ClientAuthority, "client-authority", "", "Temporal gRPC client :authority pseudoheader.") - f.StringVarP(&v.Namespace, "namespace", "n", "default", "Temporal Service Namespace.") - f.StringVar(&v.ApiKey, "api-key", "", "API key for request.") - f.StringArrayVar(&v.GrpcMeta, "grpc-meta", nil, "HTTP headers for requests. Format as a `KEY=VALUE` pair. May be passed multiple times to set multiple headers. Can also be made available via environment variable as `TEMPORAL_GRPC_META_[name]`.") - f.BoolVar(&v.Tls, "tls", false, "Enable base TLS encryption. Does not have additional options like mTLS or client certs. This is defaulted to true if api-key or any other TLS options are present. Use --tls=false to explicitly disable.") - f.StringVar(&v.TlsCertPath, "tls-cert-path", "", "Path to x509 certificate. Can't be used with --tls-cert-data.") - f.StringVar(&v.TlsCertData, "tls-cert-data", "", "Data for x509 certificate. Can't be used with --tls-cert-path.") - f.StringVar(&v.TlsKeyPath, "tls-key-path", "", "Path to x509 private key. Can't be used with --tls-key-data.") - f.StringVar(&v.TlsKeyData, "tls-key-data", "", "Private certificate key data. Can't be used with --tls-key-path.") - f.StringVar(&v.TlsCaPath, "tls-ca-path", "", "Path to server CA certificate. Can't be used with --tls-ca-data.") - f.StringVar(&v.TlsCaData, "tls-ca-data", "", "Data for server CA certificate. Can't be used with --tls-ca-path.") - f.BoolVar(&v.TlsDisableHostVerification, "tls-disable-host-verification", false, "Disable TLS host-name verification.") - f.StringVar(&v.TlsServerName, "tls-server-name", "", "Override target TLS server name.") - f.StringVar(&v.CodecEndpoint, "codec-endpoint", "", "Remote Codec Server endpoint.") - f.StringVar(&v.CodecAuth, "codec-auth", "", "Authorization header for Codec Server requests.") - f.StringArrayVar(&v.CodecHeader, "codec-header", nil, "HTTP headers for requests to codec server. Format as a `KEY=VALUE` pair. May be passed multiple times to set multiple headers.") - f.StringVar(&v.Identity, "identity", "", "The identity of the user or client submitting this request. Defaults to \"temporal-cli:$USER@$HOST\".") + f.StringVarP(&v.Namespace, "namespace", "n", "default", "Temporal Service Namespace. Env: TEMPORAL_NAMESPACE. Config: namespace.") + f.StringVar(&v.ApiKey, "api-key", "", "API key for request. Env: TEMPORAL_API_KEY. Config: api_key.") + f.StringArrayVar(&v.GrpcMeta, "grpc-meta", nil, "HTTP headers for requests (KEY=VALUE, repeatable). Config: grpc_meta..") + f.BoolVar(&v.Tls, "tls", false, "Enable base TLS encryption. Auto-enabled when api-key or TLS options are set. Env: TEMPORAL_TLS. Config: tls.") + f.StringVar(&v.TlsCertPath, "tls-cert-path", "", "Path to x509 certificate. Env: TEMPORAL_TLS_CLIENT_CERT_PATH. Config: tls.client_cert_path.") + f.StringVar(&v.TlsCertData, "tls-cert-data", "", "Inline x509 certificate data. Env: TEMPORAL_TLS_CLIENT_CERT_DATA. Config: tls.client_cert_data.") + f.StringVar(&v.TlsKeyPath, "tls-key-path", "", "Path to x509 private key. Env: TEMPORAL_TLS_CLIENT_KEY_PATH. Config: tls.client_key_path.") + f.StringVar(&v.TlsKeyData, "tls-key-data", "", "Inline x509 private key data. Env: TEMPORAL_TLS_CLIENT_KEY_DATA. Config: tls.client_key_data.") + f.StringVar(&v.TlsCaPath, "tls-ca-path", "", "Path to server CA certificate. Env: TEMPORAL_TLS_SERVER_CA_CERT_PATH. Config: tls.server_ca_cert_path.") + f.StringVar(&v.TlsCaData, "tls-ca-data", "", "Inline server CA certificate data. Env: TEMPORAL_TLS_SERVER_CA_CERT_DATA. Config: tls.server_ca_cert_data.") + f.BoolVar(&v.TlsDisableHostVerification, "tls-disable-host-verification", false, "Disable TLS host-name verification. Env: TEMPORAL_TLS_DISABLE_HOST_VERIFICATION. Config: tls.disable_host_verification.") + f.StringVar(&v.TlsServerName, "tls-server-name", "", "Override target TLS server name. Env: TEMPORAL_TLS_SERVER_NAME. Config: tls.server_name.") + f.StringVar(&v.CodecEndpoint, "codec-endpoint", "", "Remote Codec Server endpoint. Env: TEMPORAL_CODEC_ENDPOINT. Config: codec.endpoint.") + f.StringVar(&v.CodecAuth, "codec-auth", "", "Authorization header for Codec Server requests. Env: TEMPORAL_CODEC_AUTH. Config: codec.auth.") + f.StringArrayVar(&v.CodecHeader, "codec-header", nil, "HTTP headers for codec server (KEY=VALUE, repeatable).") + f.StringVar(&v.Identity, "identity", "", "Identity of the client submitting requests.") +} + +func (v *ClientOptions) HideFlags() { + if v.FlagSet == nil { + return + } + v.FlagSet.Lookup("address").Hidden = true + v.FlagSet.Lookup("client-authority").Hidden = true + v.FlagSet.Lookup("namespace").Hidden = true + v.FlagSet.Lookup("api-key").Hidden = true + v.FlagSet.Lookup("grpc-meta").Hidden = true + v.FlagSet.Lookup("tls").Hidden = true + v.FlagSet.Lookup("tls-cert-path").Hidden = true + v.FlagSet.Lookup("tls-cert-data").Hidden = true + v.FlagSet.Lookup("tls-key-path").Hidden = true + v.FlagSet.Lookup("tls-key-data").Hidden = true + v.FlagSet.Lookup("tls-ca-path").Hidden = true + v.FlagSet.Lookup("tls-ca-data").Hidden = true + v.FlagSet.Lookup("tls-disable-host-verification").Hidden = true + v.FlagSet.Lookup("tls-server-name").Hidden = true + v.FlagSet.Lookup("codec-endpoint").Hidden = true + v.FlagSet.Lookup("codec-auth").Hidden = true + v.FlagSet.Lookup("codec-header").Hidden = true + v.FlagSet.Lookup("identity").Hidden = true } diff --git a/cliext/option-sets.yaml b/cliext/option-sets.yaml index 1ebad77af..65adfcfc1 100644 --- a/cliext/option-sets.yaml +++ b/cliext/option-sets.yaml @@ -4,6 +4,11 @@ option-sets: - name: common + description: | + These options apply to every command. They control output formatting, + logging, and which configuration profile and environment to use. + Options that accept an environment variable can be set instead of + passing the flag each time. options: - name: env type: string @@ -12,17 +17,11 @@ option-sets: implied-env: TEMPORAL_ENV - name: env-file type: string - description: | - Path to environment settings file. - Defaults to `$HOME/.config/temporalio/temporal.yaml`. + description: Path to environment settings file. implied-env: TEMPORAL_ENV_FILE - name: config-file type: string - description: | - File path to read TOML config from, defaults to - `$CONFIG_PATH/temporalio/temporal.toml` where `$CONFIG_PATH` is defined - as `$HOME/.config` on Unix, `$HOME/Library/Application Support` on - macOS, and `%AppData%` on Windows. + description: TOML config file path. implied-env: TEMPORAL_CONFIG_FILE - name: profile type: string @@ -30,13 +29,10 @@ option-sets: implied-env: TEMPORAL_PROFILE - name: disable-config-file type: bool - description: | - If set, disables loading environment config from config file. + description: Disable loading config from file. - name: disable-config-env type: bool - description: | - If set, disables loading environment config from environment - variables. + description: Disable loading config from environment variables. - name: log-level type: string-enum enum-values: @@ -45,9 +41,7 @@ option-sets: - warn - error - never - description: | - Log level. - Default is "never" for most commands and "warn" for "server start-dev". + description: Log level. default: never - name: log-format type: string-enum @@ -89,20 +83,28 @@ option-sets: description: Raw payload output, even if the JSON option was used. - name: command-timeout type: duration - description: | - The command execution timeout. 0s means no timeout. + description: Command execution timeout. - name: client-connect-timeout type: duration - description: | - The client connection timeout. 0s means no timeout. + description: Client connection timeout. - name: client + hide-from-help: true + description: | + These options apply to commands that connect to a Temporal Service + (workflow, activity, schedule, etc). They specify the server address, + namespace, authentication, and TLS settings. Values are resolved in + order: CLI flag > environment variable > config file. + + To persist these settings, use: + temporal config set --prop KEY --value VALUE options: - name: address type: string description: Temporal Service gRPC endpoint. default: localhost:7233 implied-env: TEMPORAL_ADDRESS + config-key: address - name: client-authority type: string description: Temporal gRPC client :authority pseudoheader. @@ -112,83 +114,74 @@ option-sets: description: Temporal Service Namespace. default: default implied-env: TEMPORAL_NAMESPACE + config-key: namespace - name: api-key type: string description: API key for request. implied-env: TEMPORAL_API_KEY + config-key: api_key - name: grpc-meta type: string[] - description: | - HTTP headers for requests. - Format as a `KEY=VALUE` pair. - May be passed multiple times to set multiple headers. - Can also be made available via environment variable as - `TEMPORAL_GRPC_META_[name]`. + description: HTTP headers for requests (KEY=VALUE, repeatable). + config-key: grpc_meta. - name: tls type: bool - description: | - Enable base TLS encryption. Does not have additional options like mTLS - or client certs. This is defaulted to true if api-key or any other TLS - options are present. Use --tls=false to explicitly disable. + description: Enable base TLS encryption. Auto-enabled when api-key or TLS options are set. implied-env: TEMPORAL_TLS + config-key: tls - name: tls-cert-path type: string - description: | - Path to x509 certificate. - Can't be used with --tls-cert-data. + description: Path to x509 certificate. implied-env: TEMPORAL_TLS_CLIENT_CERT_PATH + config-key: tls.client_cert_path - name: tls-cert-data type: string - description: | - Data for x509 certificate. - Can't be used with --tls-cert-path. + description: Inline x509 certificate data. implied-env: TEMPORAL_TLS_CLIENT_CERT_DATA + config-key: tls.client_cert_data - name: tls-key-path type: string - description: | - Path to x509 private key. - Can't be used with --tls-key-data. + description: Path to x509 private key. implied-env: TEMPORAL_TLS_CLIENT_KEY_PATH + config-key: tls.client_key_path - name: tls-key-data type: string - description: | - Private certificate key data. - Can't be used with --tls-key-path. + description: Inline x509 private key data. implied-env: TEMPORAL_TLS_CLIENT_KEY_DATA + config-key: tls.client_key_data - name: tls-ca-path type: string - description: | - Path to server CA certificate. - Can't be used with --tls-ca-data. + description: Path to server CA certificate. implied-env: TEMPORAL_TLS_SERVER_CA_CERT_PATH + config-key: tls.server_ca_cert_path - name: tls-ca-data type: string - description: | - Data for server CA certificate. - Can't be used with --tls-ca-path. + description: Inline server CA certificate data. implied-env: TEMPORAL_TLS_SERVER_CA_CERT_DATA + config-key: tls.server_ca_cert_data - name: tls-disable-host-verification type: bool description: Disable TLS host-name verification. implied-env: TEMPORAL_TLS_DISABLE_HOST_VERIFICATION + config-key: tls.disable_host_verification - name: tls-server-name type: string description: Override target TLS server name. implied-env: TEMPORAL_TLS_SERVER_NAME + config-key: tls.server_name - name: codec-endpoint type: string description: Remote Codec Server endpoint. implied-env: TEMPORAL_CODEC_ENDPOINT + config-key: codec.endpoint - name: codec-auth type: string description: Authorization header for Codec Server requests. implied-env: TEMPORAL_CODEC_AUTH + config-key: codec.auth - name: codec-header type: string[] - description: | - HTTP headers for requests to codec server. - Format as a `KEY=VALUE` pair. - May be passed multiple times to set multiple headers. + description: HTTP headers for codec server (KEY=VALUE, repeatable). - name: identity type: string - description: The identity of the user or client submitting this request. Defaults to "temporal-cli:$USER@$HOST". + description: Identity of the client submitting requests. diff --git a/internal/commandsgen/code.go b/internal/commandsgen/code.go index fb500751b..7cb22dc31 100644 --- a/internal/commandsgen/code.go +++ b/internal/commandsgen/code.go @@ -143,6 +143,12 @@ func (o *OptionSets) writeCode(w *codeWriter) error { w.writeLinef("FlagSet *%v.FlagSet", w.importPflag()) w.writeLinef("}\n") + // write description if present + if o.Description != "" { + w.writeLinef("func (v *%v) Description() string { return %q }", o.setStructName(), o.Description) + w.writeLinef("") + } + // write flags w.writeLinef("func (v *%v) BuildFlags(f *%v.FlagSet) {", o.setStructName(), w.importPflag()) @@ -150,6 +156,16 @@ func (o *OptionSets) writeCode(w *codeWriter) error { o.writeFlagBuilding("v", "f", w) w.writeLinef("}\n") + // write HideFlags if hide-from-help is set + if o.HideFromHelp { + w.writeLinef("func (v *%v) HideFlags() {", o.setStructName()) + w.writeLinef("if v.FlagSet == nil { return }") + for _, opt := range o.Options { + w.writeLinef("v.FlagSet.Lookup(%q).Hidden = true", opt.Name) + } + w.writeLinef("}\n") + } + return nil } @@ -275,9 +291,15 @@ func (c *Command) writeCode(w *codeWriter) error { if optSet != nil && optSet.ExternalPackage != "" { // External option-set: use type name with Options suffix w.writeLinef("s.%vOptions.BuildFlags(%v)", namify(include, true), flagVar) + if optSet.HideFromHelp { + w.writeLinef("s.%vOptions.HideFlags()", namify(include, true)) + } } else { // Internal option-set: use struct name w.writeLinef("s.%v.BuildFlags(%v)", setStructName(include), flagVar) + if optSet != nil && optSet.HideFromHelp { + w.writeLinef("s.%v.HideFlags()", setStructName(include)) + } } } @@ -422,8 +444,14 @@ func (o *Option) writeFlagBuilding(selfVar, flagVar string, w *codeWriter) error return fmt.Errorf("unrecognized data type %v", o.Type) } - // If there are enums, append to desc + // If there is an implied env var or config key, append to desc desc := o.Description + if o.ImpliedEnv != "" { + desc += fmt.Sprintf(" Env: %s.", o.ImpliedEnv) + } + if o.ConfigKey != "" { + desc += fmt.Sprintf(" Config: %s.", o.ConfigKey) + } if len(o.EnumValues) > 0 { desc += fmt.Sprintf(" Accepted values: %s.", strings.Join(o.EnumValues, ", ")) } diff --git a/internal/commandsgen/parse.go b/internal/commandsgen/parse.go index 2083a033d..ad2684fab 100644 --- a/internal/commandsgen/parse.go +++ b/internal/commandsgen/parse.go @@ -23,6 +23,7 @@ type ( Short string `yaml:"short,omitempty"` Default string `yaml:"default,omitempty"` ImpliedEnv string `yaml:"implied-env,omitempty"` + ConfigKey string `yaml:"config-key,omitempty"` Required bool `yaml:"required,omitempty"` Aliases []string `yaml:"aliases,omitempty"` EnumValues []string `yaml:"enum-values,omitempty"` @@ -63,6 +64,7 @@ type ( Description string `yaml:"description"` Options []Option `yaml:"options"` ExternalPackage string `yaml:"external-package"` + HideFromHelp bool `yaml:"hide-from-help,omitempty"` } // Commands represents the top-level structure holding commands and option sets. diff --git a/internal/temporalcli/commands.gen.go b/internal/temporalcli/commands.gen.go index 5186d70e3..361cdfbca 100644 --- a/internal/temporalcli/commands.gen.go +++ b/internal/temporalcli/commands.gen.go @@ -500,6 +500,7 @@ func NewTemporalActivityCommand(cctx *CommandContext, parent *TemporalCommand) * s.Command.AddCommand(&NewTemporalActivityUnpauseCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalActivityUpdateOptionsCommand(cctx, &s).Command) s.ClientOptions.BuildFlags(s.Command.PersistentFlags()) + s.ClientOptions.HideFlags() return &s } @@ -989,6 +990,7 @@ func NewTemporalBatchCommand(cctx *CommandContext, parent *TemporalCommand) *Tem s.Command.AddCommand(&NewTemporalBatchListCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalBatchTerminateCommand(cctx, &s).Command) s.ClientOptions.BuildFlags(s.Command.PersistentFlags()) + s.ClientOptions.HideFlags() return &s } @@ -1391,6 +1393,7 @@ func NewTemporalOperatorCommand(cctx *CommandContext, parent *TemporalCommand) * s.Command.AddCommand(&NewTemporalOperatorNexusCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalOperatorSearchAttributeCommand(cctx, &s).Command) s.ClientOptions.BuildFlags(s.Command.PersistentFlags()) + s.ClientOptions.HideFlags() return &s } @@ -2108,6 +2111,7 @@ func NewTemporalScheduleCommand(cctx *CommandContext, parent *TemporalCommand) * s.Command.AddCommand(&NewTemporalScheduleTriggerCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalScheduleUpdateCommand(cctx, &s).Command) s.ClientOptions.BuildFlags(s.Command.PersistentFlags()) + s.ClientOptions.HideFlags() return &s } @@ -2506,6 +2510,7 @@ func NewTemporalTaskQueueCommand(cctx *CommandContext, parent *TemporalCommand) s.Command.AddCommand(&NewTemporalTaskQueueUpdateBuildIdsCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalTaskQueueVersioningCommand(cctx, &s).Command) s.ClientOptions.BuildFlags(s.Command.PersistentFlags()) + s.ClientOptions.HideFlags() return &s } @@ -3207,6 +3212,7 @@ func NewTemporalWorkerCommand(cctx *CommandContext, parent *TemporalCommand) *Te s.Command.AddCommand(&NewTemporalWorkerDescribeCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalWorkerListCommand(cctx, &s).Command) s.ClientOptions.BuildFlags(s.Command.PersistentFlags()) + s.ClientOptions.HideFlags() return &s } @@ -3720,6 +3726,7 @@ func NewTemporalWorkflowCommand(cctx *CommandContext, parent *TemporalCommand) * s.Command.AddCommand(&NewTemporalWorkflowUpdateCommand(cctx, &s).Command) s.Command.AddCommand(&NewTemporalWorkflowUpdateOptionsCommand(cctx, &s).Command) s.ClientOptions.BuildFlags(s.Command.PersistentFlags()) + s.ClientOptions.HideFlags() return &s } diff --git a/internal/temporalcli/commands.go b/internal/temporalcli/commands.go index 116de4800..54054bb0f 100644 --- a/internal/temporalcli/commands.go +++ b/internal/temporalcli/commands.go @@ -414,7 +414,10 @@ func Execute(ctx context.Context, options CommandOptions) { } } -// getUsageTemplate returns a custom usage template with proper flag wrapping +// getUsageTemplate returns a custom usage template with proper flag wrapping. +// On the root command, global flags are hidden and a hint to "temporal options" +// is shown instead (similar to kubectl). On subcommands, local flags are shown +// normally and inherited flags are replaced with the same hint. // The default template can be found here: https://github.com/spf13/cobra/blob/v1.9.1/command.go#L1937-L1966 func getUsageTemplate() string { // Get terminal width, default to 80 if unable to determine @@ -444,19 +447,108 @@ Available Commands:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help") {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if not .AllChildCommandsHaveGroup}} Additional Commands:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}} - {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if and .HasAvailableLocalFlags .HasParent}} Flags: -{{.LocalFlags.FlagUsagesWrapped %d | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} - -Global Flags: -{{.InheritedFlags.FlagUsagesWrapped %d | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} +{{.LocalFlags.FlagUsagesWrapped %d | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} -`, flagWidth, flagWidth) + +Use "{{.Root.Name}} options" for global and connection options. +`, flagWidth) +} + +type flagRow struct { + Flag string `json:"flag"` + Env string `json:"env,omitempty"` + Config string `json:"config,omitempty"` + Description string `json:"description"` +} + +type flagRowNoConfig struct { + Flag string `json:"flag"` + Env string `json:"env,omitempty"` + Description string `json:"description"` +} + +// printFlagTable prints flags in a table using the existing printer package. +func printFlagTable(w io.Writer, flags *pflag.FlagSet) { + p := &printer.Printer{Output: w} + + // Determine which columns are needed + hasConfig := false + flags.VisitAll(func(f *pflag.Flag) { + _, _, config := parseFlagUsage(f.Usage) + if config != "" { + hasConfig = true + } + }) + + // Collect rows + var fullRows []flagRow + var shortRows []flagRowNoConfig + + flags.VisitAll(func(f *pflag.Flag) { + desc, env, config := parseFlagUsage(f.Usage) + + // Build flag name with short and type + flag := "--" + f.Name + if f.Shorthand != "" { + flag += ", -" + f.Shorthand + } + if typ := f.Value.Type(); typ != "bool" { + if typ == "stringArray" { + typ = "string[]" + } + flag += " " + typ + } + + // Add default if non-empty + if f.DefValue != "" && f.DefValue != "false" && f.DefValue != "0" && f.DefValue != "0s" && f.DefValue != "[]" { + desc += " (default " + f.DefValue + ")" + } + + fullRows = append(fullRows, flagRow{Flag: flag, Env: env, Config: config, Description: desc}) + shortRows = append(shortRows, flagRowNoConfig{Flag: flag, Env: env, Description: desc}) + }) + + opts := printer.StructuredOptions{Table: &printer.TableOptions{}} + if hasConfig { + _ = p.PrintStructured(fullRows, opts) + } else { + _ = p.PrintStructured(shortRows, opts) + } +} + +// parseFlagUsage extracts the base description, env var, and config key +// from a flag usage string. It looks for "Env: VALUE." and "Config: VALUE." +// suffixes that were added by the code generator. +// +// Env var values never contain dots, so we split on the first dot. +// Config key values may contain dots (e.g. "tls.server_name"), so we +// split on the last dot. +func parseFlagUsage(usage string) (desc, env, config string) { + desc = usage + // Extract config first (uses last dot) since it may appear after env + if i := strings.Index(desc, " Config: "); i >= 0 { + rest := desc[i+9:] + if j := strings.LastIndex(rest, "."); j >= 0 { + config = rest[:j] + desc = strings.TrimSpace(desc[:i] + rest[j+1:]) + } + } + // Extract env (uses first dot since env vars have no dots) + if i := strings.Index(desc, " Env: "); i >= 0 { + rest := desc[i+6:] + if j := strings.Index(rest, "."); j >= 0 { + env = rest[:j] + desc = strings.TrimSpace(desc[:i] + rest[j+1:]) + } + } + return } func (c *TemporalCommand) initCommand(cctx *CommandContext) { @@ -471,6 +563,31 @@ func (c *TemporalCommand) initCommand(cctx *CommandContext) { // Customize the built-in help command to support --all/-a for listing extensions customizeHelpCommand(&c.Command) + // Add "options" command to list global and connection flags (similar to kubectl options) + c.Command.AddCommand(&cobra.Command{ + Use: "options", + Short: "Print global and connection options inherited by all commands", + Long: "Print the list of global and connection flags available across commands.", + Run: func(cmd *cobra.Command, args []string) { + w := cmd.OutOrStdout() + var commonOpts cliext.CommonOptions + fmt.Fprintln(w, "Global options") + fmt.Fprintln(w) + fmt.Fprint(w, commonOpts.Description()) + fmt.Fprintln(w) + printFlagTable(w, cmd.Root().PersistentFlags()) + fmt.Fprintln(w) + var clientOpts cliext.ClientOptions + fmt.Fprintln(w, "Connection options") + fmt.Fprintln(w) + fmt.Fprint(w, clientOpts.Description()) + fmt.Fprintln(w) + connFlags := pflag.NewFlagSet("connection", pflag.ContinueOnError) + clientOpts.BuildFlags(connFlags) + printFlagTable(w, connFlags) + }, + }) + // Unfortunately color is a global option, so we can set in pre-run but we // must unset in post-run origNoColor := color.NoColor diff --git a/internal/temporalcli/commands.help_test.go b/internal/temporalcli/commands.help_test.go index b3cd10c00..2f977fe52 100644 --- a/internal/temporalcli/commands.help_test.go +++ b/internal/temporalcli/commands.help_test.go @@ -70,12 +70,10 @@ func TestHelp_AllFlag_ShowsExtensions(t *testing.T) { assert.Contains(t, out, "foo") // shown now! assert.NotContains(t, out, "bar-baz") // is under workflow - // Verify foo appears in Available Commands section (between "Available Commands:" and "Flags:") + // Verify foo appears in Available Commands section availableIdx := strings.Index(out, "Available Commands:") fooIdx := strings.Index(out, "foo") - flagsIdx := strings.Index(out, "Flags:") assert.Greater(t, fooIdx, availableIdx, "foo should appear after Available Commands:") - assert.Less(t, fooIdx, flagsIdx, "foo should appear before Flags:") assert.NoError(t, res.Err) // Non-executable extensions are skipped diff --git a/internal/temporalcli/commands.yaml b/internal/temporalcli/commands.yaml index 1d383d179..4a918aac8 100644 --- a/internal/temporalcli/commands.yaml +++ b/internal/temporalcli/commands.yaml @@ -4571,6 +4571,7 @@ option-sets: external-package: github.com/temporalio/cli/cliext - name: client external-package: github.com/temporalio/cli/cliext + hide-from-help: true - name: overlap-policy options: