diff --git a/README.md b/README.md index 0416213..6633461 100644 --- a/README.md +++ b/README.md @@ -145,14 +145,38 @@ ktui [flags] ktui status [flags] ktui export [flags] ktui config -ktui profile -ktui update +ktui keys +ktui profile +ktui update +ktui completion ktui version -ktui help [status|config|profile|keys|update|export] +ktui help [command] ``` 连接到多个 Komari 站点时使用 `ktui profile ...` 管理,并可用 `--profile name` 临时选择。TUI 专属显示参数使用 `--mode sheet|line`。`--realtime-window 1m|5m|10m` 控制 realtime 图表横轴时间范围。一次性拉取摘要使用 `ktui status`,更新检查和安装分别使用 `ktui update check`、`ktui update install`。 +## Shell 补全 + +`ktui completion` 可以生成 Tab 补全脚本。加载后可以补全子命令、常用 flags、枚举值、配置键和已保存的 profile 名称。 + +当前终端临时启用: + +```sh +source <(ktui completion bash) +# zsh: +source <(ktui completion zsh) +# fish: +ktui completion fish | source +``` + +PowerShell: + +```powershell +ktui completion powershell | Out-String | Invoke-Expression +``` + +持久启用时,把对应命令放进 `~/.bashrc`、`~/.zshrc`、fish 配置或 PowerShell profile;也可以把 `ktui completion zsh` 输出保存为 `$fpath` 目录里的 `_ktui` 文件。 + ## 配置文件 初始化配置: diff --git a/cmd/ktui/cobra.go b/cmd/ktui/cobra.go new file mode 100644 index 0000000..c10bbc5 --- /dev/null +++ b/cmd/ktui/cobra.go @@ -0,0 +1,541 @@ +package main + +import ( + "fmt" + "os" + "strings" + "time" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "ktui/internal/config" + "ktui/internal/update" +) + +var ( + completionShells = []string{"bash", "zsh", "fish", "powershell"} + + cobraExportFormats = []string{"markdown", "csv", "json"} + cobraConfigKeys = []string{ + "profile", + "url", + "api-key", + "interval", + "timeout", + "realtime-window", + "chart-y-axis", + "warn-cpu", + "warn-ram", + "warn-disk", + "warn-expiry-days", + "mode", + "ascii", + "no-color", + } +) + +func execute() error { + return newRootCommand().Execute() +} + +func newRootCommand() *cobra.Command { + root := &cobra.Command{ + Use: "ktui", + Short: "Komari terminal UI", + SilenceErrors: true, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + return handleTUI(changedFlagArgs(cmd)) + }, + } + root.CompletionOptions.DisableDefaultCmd = true + root.PersistentFlags().String("config", defaultConfigPath(), "config file path") + must(root.MarkPersistentFlagFilename("config")) + + addTUIFlags(root) + addConnectionFlagCompletions(root) + addTUIFlagCompletions(root) + + root.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { + return setConfigEnvFromCommand(cmd) + } + + root.AddCommand( + newStatusCommand(), + newExportCommand(), + newConfigCommand(), + newProfileCommand(), + newUpdateCommand(), + newVersionCommand(), + newKeysCommand(), + newCompletionCommand(), + ) + return root +} + +func newStatusCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "status", + Short: "Fetch once and print a node summary", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return handleStatus(changedFlagArgs(cmd)) + }, + } + addStatusFlags(cmd) + addConnectionFlagCompletions(cmd) + return cmd +} + +func newExportCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "export ", + Short: "Export current node status", + ValidArgs: cobraExportFormats, + Args: cobra.RangeArgs(0, 1), + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return handleExport(nil) + } + next := append([]string{args[0]}, changedFlagArgs(cmd)...) + return handleExport(next) + }, + } + addStatusFlags(cmd) + cmd.Flags().StringP("output", "o", "", "write export to a file instead of stdout") + must(cmd.MarkFlagFilename("output")) + addConnectionFlagCompletions(cmd) + return cmd +} + +func newConfigCommand() *cobra.Command { + initCmd := &cobra.Command{ + Use: "init", + Short: "Create the config file", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return handleConfig(append([]string{"init"}, changedFlagArgs(cmd)...)) + }, + } + initCmd.Flags().Bool("force", false, "overwrite existing config") + must(initCmd.RegisterFlagCompletionFunc("force", cobra.NoFileCompletions)) + + cmd := &cobra.Command{ + Use: "config", + Short: "Manage persistent settings", + RunE: func(cmd *cobra.Command, args []string) error { + return handleConfig(nil) + }, + } + cmd.AddCommand( + initCmd, + &cobra.Command{ + Use: "path", + Short: "Print the config path", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return handleConfig([]string{"path"}) + }, + }, + &cobra.Command{ + Use: "show", + Short: "Print the config file with secrets redacted", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return handleConfig([]string{"show"}) + }, + }, + &cobra.Command{ + Use: "set ", + Short: "Set a config value", + Args: cobra.ExactArgs(2), + ValidArgsFunction: configSetCompletions, + RunE: func(cmd *cobra.Command, args []string) error { + return handleConfig([]string{"set", args[0], args[1]}) + }, + }, + &cobra.Command{ + Use: "help", + Short: "Show config help", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return handleConfig([]string{"help"}) + }, + }, + ) + return cmd +} + +func newProfileCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "profile", + Short: "Manage Komari connection profiles", + RunE: func(cmd *cobra.Command, args []string) error { + return handleProfile(nil) + }, + } + cmd.AddCommand( + &cobra.Command{ + Use: "list", + Short: "List configured profiles", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return handleProfile([]string{"list"}) + }, + }, + &cobra.Command{ + Use: "current", + Short: "Print the active profile", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return handleProfile([]string{"current"}) + }, + }, + &cobra.Command{ + Use: "use ", + Short: "Switch active profile", + Args: cobra.ExactArgs(1), + ValidArgsFunction: profileNameArgCompletions, + RunE: func(cmd *cobra.Command, args []string) error { + return handleProfile([]string{"use", args[0]}) + }, + }, + newProfileAddCommand(), + &cobra.Command{ + Use: "rename ", + Short: "Rename a profile", + Args: cobra.ExactArgs(2), + ValidArgsFunction: profileRenameArgCompletions, + RunE: func(cmd *cobra.Command, args []string) error { + return handleProfile([]string{"rename", args[0], args[1]}) + }, + }, + &cobra.Command{ + Use: "remove ", + Short: "Remove a profile", + Args: cobra.ExactArgs(1), + ValidArgsFunction: profileNameArgCompletions, + RunE: func(cmd *cobra.Command, args []string) error { + return handleProfile([]string{"remove", args[0]}) + }, + }, + &cobra.Command{ + Use: "help", + Short: "Show profile help", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return handleProfile([]string{"help"}) + }, + }, + ) + return cmd +} + +func newProfileAddCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "add ", + Short: "Add a profile", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + next := append([]string{"add", args[0]}, changedFlagArgs(cmd)...) + return handleProfile(next) + }, + } + cmd.Flags().String("url", "", "Komari base URL") + cmd.Flags().String("api-key", "", "Komari API key") + cmd.Flags().Bool("use", false, "make this profile active") + must(cmd.RegisterFlagCompletionFunc("url", cobra.NoFileCompletions)) + must(cmd.RegisterFlagCompletionFunc("api-key", cobra.NoFileCompletions)) + must(cmd.RegisterFlagCompletionFunc("use", cobra.NoFileCompletions)) + cmd.ValidArgsFunction = cobra.NoFileCompletions + return cmd +} + +func newUpdateCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "update", + Short: "Check or install ktui updates", + RunE: func(cmd *cobra.Command, args []string) error { + return handleUpdate(nil) + }, + } + cmd.AddCommand( + newUpdateCheckCommand(), + newUpdateInstallCommand(), + &cobra.Command{ + Use: "help", + Short: "Show update help", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return handleUpdate([]string{"help"}) + }, + }, + ) + return cmd +} + +func newUpdateCheckCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "check", + Short: "Check for a ktui update", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return handleUpdate(append([]string{"check"}, changedFlagArgs(cmd)...)) + }, + } + addUpdateFlags(cmd, false) + return cmd +} + +func newUpdateInstallCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "install", + Short: "Install a ktui update", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return handleUpdate(append([]string{"install"}, changedFlagArgs(cmd)...)) + }, + } + addUpdateFlags(cmd, true) + return cmd +} + +func newVersionCommand() *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Print version information", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + printVersion() + }, + } +} + +func newKeysCommand() *cobra.Command { + return &cobra.Command{ + Use: "keys", + Short: "Show TUI key bindings", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + printKeysHelp() + }, + } +} + +func newCompletionCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "completion ", + Short: "Generate shell completion scripts", + ValidArgs: completionShells, + Args: cobra.RangeArgs(0, 1), + DisableFlagsInUseLine: true, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return cmd.Help() + } + switch args[0] { + case "bash": + return cmd.Root().GenBashCompletion(os.Stdout) + case "zsh": + return cmd.Root().GenZshCompletion(os.Stdout) + case "fish": + return cmd.Root().GenFishCompletion(os.Stdout, true) + case "powershell": + return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) + default: + return fmt.Errorf("unknown completion shell %q: use bash, zsh, fish, or powershell", args[0]) + } + }, + } + return cmd +} + +func addTUIFlags(cmd *cobra.Command) { + flags := cmd.Flags() + addConnectionFlags(flags, 10*time.Second) + flags.Duration("interval", 5*time.Second, "refresh interval") + flags.Duration("realtime-window", time.Minute, "realtime chart time window: 1m, 5m, or 10m") + flags.String("chart-y-axis", "absolute", "percent chart Y axis mode: absolute or relative") + flags.String("mode", "sheet", "view mode: sheet or line") + flags.Bool("ascii", false, "use ASCII-only rendering for terminals/fonts with Unicode issues") + flags.Bool("no-color", false, "disable ANSI color and inverse video") +} + +func addStatusFlags(cmd *cobra.Command) { + addConnectionFlags(cmd.Flags(), 10*time.Second) +} + +func addConnectionFlags(flags *pflag.FlagSet, timeoutDefault time.Duration) { + flags.String("profile", config.DefaultProfile, "profile name") + flags.String("url", "", "Komari base URL") + flags.String("api-key", "", "Komari API key (sent as Bearer token)") + flags.Duration("timeout", timeoutDefault, "HTTP timeout") +} + +func addUpdateFlags(cmd *cobra.Command, withTag bool) { + cmd.Flags().String("api-url", update.DefaultAPIBaseURL, "Gitea repository API URL") + cmd.Flags().Duration("timeout", 60*time.Second, "update HTTP timeout") + if withTag { + cmd.Flags().String("tag", "", "install a specific release tag instead of the latest release") + must(cmd.RegisterFlagCompletionFunc("tag", cobra.NoFileCompletions)) + } + must(cmd.RegisterFlagCompletionFunc("api-url", cobra.NoFileCompletions)) + must(cmd.RegisterFlagCompletionFunc("timeout", cobra.NoFileCompletions)) +} + +func addConnectionFlagCompletions(cmd *cobra.Command) { + must(cmd.RegisterFlagCompletionFunc("profile", profileFlagCompletions)) + must(cmd.RegisterFlagCompletionFunc("url", cobra.NoFileCompletions)) + must(cmd.RegisterFlagCompletionFunc("api-key", cobra.NoFileCompletions)) + must(cmd.RegisterFlagCompletionFunc("timeout", cobra.NoFileCompletions)) +} + +func addTUIFlagCompletions(cmd *cobra.Command) { + must(cmd.RegisterFlagCompletionFunc("interval", cobra.NoFileCompletions)) + must(cmd.RegisterFlagCompletionFunc("realtime-window", cobra.FixedCompletions([]cobra.Completion{"1m", "5m", "10m"}, cobra.ShellCompDirectiveNoFileComp))) + must(cmd.RegisterFlagCompletionFunc("chart-y-axis", cobra.FixedCompletions([]cobra.Completion{"absolute", "relative"}, cobra.ShellCompDirectiveNoFileComp))) + must(cmd.RegisterFlagCompletionFunc("mode", cobra.FixedCompletions([]cobra.Completion{"sheet", "line"}, cobra.ShellCompDirectiveNoFileComp))) + must(cmd.RegisterFlagCompletionFunc("ascii", cobra.NoFileCompletions)) + must(cmd.RegisterFlagCompletionFunc("no-color", cobra.NoFileCompletions)) +} + +func changedFlagArgs(cmd *cobra.Command) []string { + args := []string{} + cmd.Flags().Visit(func(flag *pflag.Flag) { + if flag.Name == "config" { + return + } + args = append(args, "--"+flag.Name+"="+flag.Value.String()) + }) + return args +} + +func setConfigEnvFromCommand(cmd *cobra.Command) error { + value, err := cmd.Flags().GetString("config") + if err != nil { + return err + } + value = strings.TrimSpace(value) + if value == "" { + return nil + } + return os.Setenv("KTUI_CONFIG", value) +} + +func defaultConfigPath() string { + path, err := config.Path() + if err != nil { + return "" + } + return path +} + +func profileFlagCompletions(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { + return profileCompletions(cmd, toComplete), cobra.ShellCompDirectiveNoFileComp +} + +func profileNameArgCompletions(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { + if len(args) > 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return profileCompletions(cmd, toComplete), cobra.ShellCompDirectiveNoFileComp +} + +func profileRenameArgCompletions(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { + if len(args) == 0 { + return profileCompletions(cmd, toComplete), cobra.ShellCompDirectiveNoFileComp + } + if len(args) == 1 { + return profileCompletionsExcept(cmd, toComplete, args[0]), cobra.ShellCompDirectiveNoFileComp + } + return nil, cobra.ShellCompDirectiveNoFileComp +} + +func configSetCompletions(cmd *cobra.Command, args []string, toComplete string) ([]cobra.Completion, cobra.ShellCompDirective) { + if len(args) == 0 { + return filterCompletions(cobraConfigKeys, toComplete), cobra.ShellCompDirectiveNoFileComp + } + if len(args) == 1 { + return filterCompletions(configValueCompletionsForCobra(cmd, args[0]), toComplete), cobra.ShellCompDirectiveNoFileComp + } + return nil, cobra.ShellCompDirectiveNoFileComp +} + +func configValueCompletionsForCobra(cmd *cobra.Command, key string) []string { + switch strings.TrimSpace(strings.ToLower(key)) { + case "profile": + return completionProfileNamesForCommand(cmd) + case "mode": + return []string{"sheet", "line"} + case "realtime-window", "realtime_window": + return []string{"1m", "5m", "10m"} + case "chart-y-axis", "chart_y_axis": + return []string{"absolute", "relative"} + case "ascii", "no-color", "no_color": + return []string{"true", "false"} + default: + return nil + } +} + +func profileCompletions(cmd *cobra.Command, toComplete string) []cobra.Completion { + return filterCompletions(completionProfileNamesForCommand(cmd), toComplete) +} + +func profileCompletionsExcept(cmd *cobra.Command, toComplete string, exclude string) []cobra.Completion { + names := completionProfileNamesForCommand(cmd) + out := make([]string, 0, len(names)) + for _, name := range names { + if name != exclude { + out = append(out, name) + } + } + return filterCompletions(out, toComplete) +} + +func completionProfileNamesForCommand(cmd *cobra.Command) []string { + cfg, err := loadCompletionConfig(cmd) + if err != nil { + return []string{config.DefaultProfile} + } + return cfg.ProfileNames() +} + +func loadCompletionConfig(cmd *cobra.Command) (config.Config, error) { + path, _ := cmd.Flags().GetString("config") + path = strings.TrimSpace(path) + if path == "" { + cfg, _, err := config.Load() + return cfg, err + } + old, hadOld := os.LookupEnv("KTUI_CONFIG") + if err := os.Setenv("KTUI_CONFIG", path); err != nil { + return config.Config{}, err + } + defer func() { + if hadOld { + _ = os.Setenv("KTUI_CONFIG", old) + } else { + _ = os.Unsetenv("KTUI_CONFIG") + } + }() + cfg, _, err := config.Load() + return cfg, err +} + +func filterCompletions(values []string, prefix string) []cobra.Completion { + out := make([]cobra.Completion, 0, len(values)) + for _, value := range values { + if strings.HasPrefix(value, prefix) { + out = append(out, value) + } + } + return out +} + +func must(err error) { + if err != nil { + panic(err) + } +} diff --git a/cmd/ktui/cobra_test.go b/cmd/ktui/cobra_test.go new file mode 100644 index 0000000..9823071 --- /dev/null +++ b/cmd/ktui/cobra_test.go @@ -0,0 +1,101 @@ +package main + +import ( + "bytes" + "path/filepath" + "strings" + "testing" + + "ktui/internal/config" +) + +func TestCobraRootCommandTree(t *testing.T) { + root := newRootCommand() + for _, name := range []string{"status", "export", "config", "profile", "update", "version", "keys", "completion"} { + cmd, _, err := root.Find([]string{name}) + if err != nil { + t.Fatalf("find %q error = %v", name, err) + } + if cmd == nil || cmd.Name() != name { + t.Fatalf("find %q = %v", name, cmd) + } + } +} + +func TestCobraChangedFlagArgsSkipsConfig(t *testing.T) { + root := newRootCommand() + if err := root.ParseFlags([]string{"--mode", "line", "--config", "custom.json", "--ascii"}); err != nil { + t.Fatal(err) + } + args := changedFlagArgs(root) + if !hasString(args, "--mode=line") || !hasString(args, "--ascii=true") { + t.Fatalf("changedFlagArgs = %#v, want changed root flags", args) + } + for _, arg := range args { + if strings.HasPrefix(arg, "--config=") { + t.Fatalf("changedFlagArgs should skip config: %#v", args) + } + } +} + +func TestCobraCompletionUsesConfiguredProfiles(t *testing.T) { + path := filepath.Join(t.TempDir(), "config.json") + + cfg, err := config.AddProfile(config.Default(), "prod", "https://prod.example.com", "") + if err != nil { + t.Fatal(err) + } + t.Setenv("KTUI_CONFIG", path) + if _, err := config.Save(cfg); err != nil { + t.Fatal(err) + } + + root := newRootCommand() + status, _, err := root.Find([]string{"status"}) + if err != nil { + t.Fatal(err) + } + if err := status.ParseFlags([]string{"--config", path}); err != nil { + t.Fatal(err) + } + got := profileCompletions(status, "pr") + if !hasCompletion(got, "prod") { + t.Fatalf("profile completions = %#v, want prod", got) + } +} + +func TestCobraHiddenCompletionCommand(t *testing.T) { + root := newRootCommand() + var out bytes.Buffer + root.SetOut(&out) + root.SetErr(&out) + root.SetArgs([]string{"__complete", "completion", ""}) + + if err := root.Execute(); err != nil { + t.Fatal(err) + } + text := out.String() + for _, want := range []string{"bash", "zsh", "fish", "powershell"} { + if !strings.Contains(text, want) { + t.Fatalf("hidden completion output missing %q:\n%s", want, text) + } + } +} + +func hasString(values []string, want string) bool { + for _, value := range values { + if value == want { + return true + } + } + return false +} + +func hasCompletion(values []string, want string) bool { + for _, value := range values { + if value == want { + return true + } + } + return false +} diff --git a/cmd/ktui/main.go b/cmd/ktui/main.go index b237d9f..ce41827 100644 --- a/cmd/ktui/main.go +++ b/cmd/ktui/main.go @@ -30,13 +30,7 @@ const ( ) func main() { - args, configPath := splitConfigArg(os.Args[1:]) - if configPath != "" { - if err := os.Setenv("KTUI_CONFIG", configPath); err != nil { - fatal(err) - } - } - if err := handleCommand(args); err != nil { + if err := execute(); err != nil { fatal(err) } } @@ -802,7 +796,7 @@ func handleProfile(args []string) error { func handleHelp(args []string) error { if len(args) > 1 { - return fmt.Errorf("usage: ktui help [status|config|profile|keys|update|export]") + return fmt.Errorf("usage: ktui help [status|config|profile|keys|update|export|completion]") } if len(args) == 0 { printHelp() @@ -880,8 +874,9 @@ Usage: ktui config ktui profile ktui update + ktui completion ktui version - ktui help [status|config|profile|keys|update|export] + ktui help [status|config|profile|keys|update|export|completion] Connection flags: --profile NAME profile name @@ -908,6 +903,7 @@ Examples: ktui version ktui update check ktui update install + ktui completion bash ktui export markdown ktui export csv --output nodes.csv ktui config init diff --git a/cmd/ktui/main_test.go b/cmd/ktui/main_test.go index 8017b61..0d6e95a 100644 --- a/cmd/ktui/main_test.go +++ b/cmd/ktui/main_test.go @@ -62,11 +62,13 @@ func TestHelpTextUsesRefactoredCommands(t *testing.T) { "ktui export [flags]", "ktui profile ", "ktui update ", + "ktui completion ", "--profile NAME", "--mode MODE", "--realtime-window DURATION", "ktui update check", "ktui update install", + "ktui completion bash", } { if !strings.Contains(helpText, want) { t.Fatalf("help text missing %q:\n%s", want, helpText) diff --git a/go.mod b/go.mod index aff62ed..483d279 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,10 @@ module ktui go 1.26 + +require ( + github.com/spf13/cobra v1.10.2 + github.com/spf13/pflag v1.0.9 +) + +require github.com/inconshreveable/mousetrap v1.1.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a6ee3e0 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=