Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Registering git style subcommands leveraging standalone executable files #1417

Closed
brandongregoryscott opened this issue Dec 17, 2020 · 9 comments

Comments

@brandongregoryscott
Copy link

Context:
I am unsure if there is something incorrect with my setup/proposed use-case, or if this is a feature that isn't supported yet by the API. May be related to #714. I am working off of version 6.0.0.

Use-case:
I would like to be able to register commands (parent and nested) that exist as standalone executable files from the entrypoint or 'top-level' command file.

Problem:
The command() overload that takes the { executableFile?: string } object only exists on the commander.CommanderStatic instance and not the addCommand() function which is used to add prepared subcommands.

I have an example repository here, which has a workaround solution in the main branch (ie, I register the subcommand cli-dotnet-version.js in the standalone file for the parent, cli-dotnet.js), whereas in the issue branch, I would really like to be able to register both dotnet and its subcommand version in cli.js so I can run ./cli.js dotnet version

In the issue branch, the output from running the cli has both commands at the entrypoint level and version does not get nested under dotnet:

./cli.js
Usage: cli [options] [command]

Options:
  -h, --help      display help for command

Commands:
  dotnet          Parent command housing operations around the dotnet cli
  version         Displays dotnet installation info
  help [command]  display help for command
./cli.js dotnet
Usage: cli-dotnet [options]

This is my custom version of the dotnet command

Options:
-h, --help  display help for command

whereas the output from the main branch is what I'm looking for:

./cli.js
Usage: cli [options] [command]

Options:
-h, --help      display help for command

Commands:
dotnet          Parent command housing operations around the dotnet cli
help [command]  display help for command
./cli.js dotnet
Usage: cli-dotnet [options] [command]

This is my custom version of the dotnet command

Options:
  -h, --help      display help for command

Commands:
  version         Displays dotnet info
  help [command]  display help for command
@shadowspawn
Copy link
Collaborator

I am not seeing the problem on the main branch. You have a hierarchy of external subcommands. Each level only knows about its direct subcommand. The top level program does not know that dotnet has a version subcommand, but it does not need to as it passes the arguments on to that subcommand to process.

It works the way I expect when I try:

$ ./cli.js
# displays help from cli.js, including command dotnet

$ ./cli.js dotnet
# displays dotnet help from cli-dotnet.js, including command version

$ ./cli.js dotnet version
# runs cli-dotnet-version.js

@brandongregoryscott
Copy link
Author

@shadowspawn Correct - the main branch does not have the issue, but the issue branch does. I was hoping to be able to create the same structure of commands starting from the very top command (cli.js). If that's not possible, I can work around it - it'd just be awesome to keep all of the command registrations in one place and only worry about wiring up options in each of the standalone files.

@shadowspawn
Copy link
Collaborator

Ah thanks, I am understanding question better on second read. I'll try explaining, and may lead to more questions about how things could be arranged.

With stand-alone executables, you can't wire it all up at the top level. There isn't a full "command" added for a stand-alone executable, just a placeholder that there is an external command.

In particular, note:

  • program.command('internal'): returns a new command to configure
  • program.command('external', 'external description'): returns the program, nothing more to configure for subcommand

So on your issue branch, dotnet and program are referring to the same object: https://github.com/brandongregoryscott/commanderjs-git-style-subcommand-example/blob/issue/cli.js

.addCommand() is for adding a command object that you constructed and configured separately, so allows some code separation but does not help with wiring up stand-alone executables.

@brandongregoryscott
Copy link
Author

Gotcha. That's what I was thinking based on the docs and from playing around with a few different configurations of the functions available.

Not sure if this is something that's on the roadmap, but I would love to see it. My team has invested in a custom CLI project to ease and automate our day-to-day engineering and devops, and it has grown significantly in the last year. (Just completed a TypeScript port of ~130 files) We've gotten by so far by using standalone executable files that use multiple options to wire up logic that is extracted out into shared modules, but I think we've hit a point with some of the commands that it makes sense to pull out another level of commands to group the operations by.

I can move forward with registering the subcommands in the standalone files for now - thanks for taking a look and clarifying. Feel free to close this out unless you'd like to keep it around as a feature request. Cheers! 🍻

@shadowspawn
Copy link
Collaborator

In your example, will the intermediate command dotnet be something with its own functionality, or might it just dispatch through to the leaf commands like [dotnet-] version?

(I am wondering whether the setup of dotnet could be hoisted into cli.js pretty much like you tried on the branch, just by not making it external.)

@brandongregoryscott
Copy link
Author

Hmmm... well, in our actual project, I think we might need both. For example, we currently have a deploy command in our cli project that basically acts as a proxy to leaf commands for specific deployment infrastructures (aws-s3, aws-beanstalk, etc), with no other business logic attached. See the command here: https://github.com/AndcultureCode/AndcultureCode.Cli/blob/main/and-cli-deploy.js

What spawned this issue was moreso for our github command, which is a wrapper around Github's octokit package/API. We originally added in some functionality to clone/fork repositories for our organization to ease the burden of setup for engineers trying to get into OSS, and we tacked on some additional functionality during Hacktoberfest to programatically add/remove topics to those repositories.

We have all of that in a flat github command currently, and I'd like to break out in a similar structure to the gh cli, ie github issue list, github issue create, github repo list, etc. I think most of the functionality we currently have could be pushed down to the subcommands where they are relevant, aside from maybe our --auth flag and possibly the --repo flag which denotes acting on a specific repo vs. all repos we have.

Apologies for the long-winded response - another reason we would like to keep registering the commands the same way (externally) is to support a 'plugin' structure - we built out an abstraction that allows consumers to pull in all (or some) of our base commands and add custom commands for their own project-specific needs, which hinges on referencing the executable file in the node_modules folder when it is being imported as a plugin project.

@shadowspawn
Copy link
Collaborator

shadowspawn commented Dec 18, 2020

Interesting, thanks. Nice to see how easy it is to scan a directory with files for subcommands.

This may not be relevant to your setup, but the hoisting I had in mind (in cli.js) is like:

const dotnet = program
  .command("dotnet")
  .description("Parent command housing operations around the dotnet cli");

dotnet.command("version", "Displays dotnet installation info", {
    executableFile: "cli-dotnet-version.js",
});

(Requires Commander v6.1 or higher which fixes a bug calling external subcommands from a nested command, your repo has Commander v6.0.0.)

@brandongregoryscott
Copy link
Author

I see @shadowspawn. By registering the command that way, it delegates to the action function (or displaying the help menu if no options are provided), correct? cli-dotnet.js could be removed entirely. Interesting. I think that technically works for our use-case (being able to register subcommands from the top-level file). I started hacking on a solution to abstract nested commands being registered in their parent command, so either route will require some work. Thanks for the food for thought 👍

@shadowspawn
Copy link
Collaborator

An answer was provided, and no further activity in a month. Closing this as resolved.

Feel free to open a new issue if it comes up again, with new information and renewed interest.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants