How to combine two Cobra CLIs into one?

349 Views Asked by At

I have the following commands for CLI A (see the real repository):

$ cli-a -h

Usage:
  cli-a [command]

Available Commands:
  command-a1       command description
  command-a2       command description
  command-a3       command description

Flags:
      --config string   config file (default is $HOME/.cli-a.json)
  -h, --help            help for cli-a

Use "cli-a [command] --help" for more information about a command.

And, for CLI B, I have the following commands (see the real repository):

$ cli-b -h

Usage:
  cli-b [command]

Available Commands:
  command-b1       command description
  command-b2       command description
  command-b3       command description

Flags:
      --kubeconfig string   path to kubeconfig file
  -h, --help                help for cli-b

Use "cli-b [command] --help" for more information about a command.

What I want to do now is, I want the CLI B (and all its subcommands) to be CLI A's subcommands like this:

$ cli-a -h

Usage:
  cli-a [command]

Available Commands:
  command-a1       command description
  command-a2       command description
  command-a3       command description
  cli-b            description about cli-b

Flags:
      --config string   config file (default is $HOME/.cli-a.json)
  -h, --help            help for cli-a

Use "cli-a [command] --help" for more information about a command.

So when user run $ cli-a cli-b -h, they will see:

$ cli-a cli-b -h

Usage:
  cli-a cli-b [command]

Available Commands:
  command-b1       command description
  command-b2       command description
  command-b3       command description

Flags:
      --kubeconfig string   path to kubeconfig file
  -h, --help                help for cli-b

Use "cli-a cli-b [command] --help" for more information about a command.

Both repos are in different Go module name/repository. I still want to offer CLI B to users to download if they don't prefer to download and use all features in CLI A.

Most of the work for CLI B (features, bug fixes and etc) will be done in the CLI B repository and I'm thinking to combine/merge them two in my CLI A's CI pipeline (need to think how to do this as well).

If possible, I don't want to change/update anything in CLI A codebase everytime I made changes to CLI B codebase (business logic, CLI flags & etc). I want the CLI A to act just like a proxy server for CLI B (I'm using an analogy here) - where all the business logic are kept inside CLI B codebase.

And, if you look closer at example outputs above, when user runs $ cli-a cli-b -h or $ cli-a cli-b command-bN [-h], I don't want the CLI A's root flag (--config) to appear and take effect. Instead, I want CLI B's root flag (--kubeconfig) to appear and take effect.

Questions

  1. Is this doable with Cobra?
  2. What's the best way to achieve this without adding too much code in CLI A's repository?
  3. How's the project structure/tree should look like?
  4. Any good examples for me to refer?
  5. On high level, what should I do in CLI A's build/CI pipeline to make this happen?

I'd really appreciate all inputs, pointers, examples and external resources here. Thank you in advance.

1

There are 1 best solutions below

0
On

I have done something like this, you can maintain both the cli's as different module, there is no need to merge the code.

All you have to do in in CLI-A's root command is

cliARootCommand.AddCommand(
       cliBPackage.NewRootCommand(),
)

And for your second part of the question where the CliA's persistent flag not being part of CliB. We disable flag parsing in CliB and in the PreRunE of CliB's root command, we can mark all parent flag hidden and then parse the flags.

func NewRootCommand() *cobra.Command {
    rootCommand := &cobra.Command{
        Use: "cli-b",
        PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
            // marking all parent flag hidden
            cmd.Parent().Flags().VisitAll(
                func(f *pflag.Flag) {
                    cmd.Flags().MarkHidden(f.Name)
                },
            )

            // enabling flag parsing
            cmd.DisableFlagParsing = false

            if err := cmd.ParseFlags(args); err != nil {
                return err
            }
            if cmd.Flag("help").Changed {
                err := cmd.Help()
                if err != nil {
                    return err
                }
                os.Exit(0)
            }
            return nil
        },
        RunE: func(cmd *cobra.Command, args []string) error {
            return cmd.help()
        },
    }
    
    //Diasbling flag parsing
    rootCommand.DisableFlagParsing = true

    return rootCommand
}