Create a Hybrid Single-Multi Command Node.js CLI with Oclif and TypeScript

Shawn Wang
InstructorShawn Wang
Share this video with your friends

Social Share Links

Send Tweet
Published 4 years ago
Updated 3 years ago

Sometimes you still want to support the API and quick developer experience of a single command, while still supporting multi commands in your CLI. This is handy when, for example, you want your user to do something useful with just npx mycli.

Here's a quick hack that lets you do that, while also deepening your understanding of how your CLI works with TypeScript, Node.js and Oclif under the hood.

Instructor: [0:00] This is a really neat trick I discovered while experimenting with CLIs. By default, multi-command CLIs with oclif just show a list of commands when you run the CLI by itself. It just shows the documentation.

[0:16] It's not actually doing anything useful if you just run that by itself. You have to run something like mycli init. Then it actually does some command that executes some logic. What if you wanted to have that experience just from the get-go so that the user doesn't have to memorize your command off the bat and you have the simplest possible command?

[0:41] For example, if you publish to npm, you would just say, "mynpx mycli." That would actually do something productive. That has a bigger wow factor as well. That looks like a single command, but you might want to keep the multi-command structure and behavior of oclif. You need some sort of hybrid between single and multi-commands. This is actually doable.

[1:05] The way you do this is you go into the bin directory of the oclif command. You check out the run file. This is where we actually get some insight into how oclif works with TypeScript under the hood and initializes its commands.

[1:21] It uses ts-node. It registers that as a hook into Node. It transforms all the TypeScript stuff on the fly only in development mode. Then it requires, based on development, whether it's [inaudible] . It does require .run. That's the .run export that you see in the index.ts file.

[1:46] This is how multi commands work. Theoretically, we can actually have a list of recognized commands, for example init, serve, and build. Then we can put an if block saying that if there's a process argv, if the length is more than two, which means that there is an additional multi-command selected and it's a recognized command, then we run the multi-command code.

[2:13] Else, we could run the code in single-command mode and basically just point directly to whatever command we want to alias as the top-level command. For me, I'm just going to say command/init. That should be good enough.

[2:31] The behavior of this is very interesting. Now we can actually run yarn mycli. That will give the same result as mycli init. This is now a perfect alias for yarn mycli init. That does the exact same thing, but I still have the ability to do yarn mycli build. That gives me the build command.

Stephen Weiss
Stephen Weiss
~ 4 years ago

Is there a reason to not create the hybrid by checking if we only have two elements in argv instead of checking more? The concern I have with this approach is that we need to maintain a list of the registeredCommands which feels very easy to get out of sync.

On the other hand, we could flip this so that we have a base if no additional arguments are passed and otherwise use the standard behavior:

if (process.argv.length == 2) {
  require(`../${dev ? "src" : "lib"}/commands/init`)
    .run()
    .catch(require("@oclif/errors/handle"));
} else {
  require(`../${dev ? "src" : "lib"}`)
    .run()
    .catch(require("@oclif/errors/handle"));
}
Markdown supported.
Become a member to join the discussionEnroll Today