Writing commands
Every team, repo, and workspace has unique needs. oneRepo shines when it comes to doing what you need at the right time.
oneRepo commands are an extended version of the Yargs command module, written as ESM and TypeScript compatible modules. By default, command files can be placed in a commands/
folder at the repository root and within any workspace and they will automatically be registered. This directory can be changed by setting the commands.directory
setting in the root configuration.
Commands are required to implement the following exports
:
Export | Type | Description |
---|---|---|
name | string | Array<string> | The command’s name. If provided as an array, all values will be considered aliases for the same command. Any value can also be '$0' , which is a special token that allows this command to be the default for the parent command. |
description | string | false | A helpful description. If set to false , the command will be hidden from help output unless --show-advanced is included with --help . Note: unlike in Yargs, this export must be description and not describe . |
builder | Builder<T> | A function that helps parse the command-line arguments |
handler | Handler<T> | Asynchronous function that is invoked for the command |
Tutorial
Section titled TutorialA basic command
Section titled A basic commandLet’s create a custom command for our team for creating new branches using a standard naming format. This will help make it quick and easy when working on many different things to organize and avoid conflicts with each other when pushing our branches for peer review.
-
Create a command
-
Add arguments
For our branch names, we want to follow the common structure:
<username>/[issue-number]/<description>
, where[issue-number]
is optional. Let’s start by setting up the input options for the issue number and description.If you’re using TypeScript, be sure to add the
type
for the arguments that will be found by thebuilder
and passed through to thehandler
. This can be done by defining each asBuilder<Args>
andHandler<Args>
, respectively. By doing so, thebuilder
will be checked viatsc
to ensure that all options are accounted for correctly and the first argument passed to ourhandler
function (argv
) will include the appropriate shape.Note the difference between optional and required arguments: the required
description
argument includes an optiondemandOption
set totrue
: -
Getting your username
You may have noticed that we skipped the
<username>
in our input arguments for creating new branches. That’s because we should be able to get the username from the operating system and reduce the extra burden on developers needing to add that in for every branch.To get the username, we can use Node’s
os.userInfo()
command:Now, when running our command, we can see our username. For this example, we’ll use the creator of JavaScript, Brendan Eich’s username,
brendaneich
: -
Logic & scripts
Now that we have the basic plumbing and all of the input information we need, it’s time to actually make our command create new branches.
One last time, let’s run our command and create a new branch for adding the command:
Nesting commands
Section titled Nesting commandsCommands may be nested for organization purposes. Nesting can help create a logical hierarchy and flow for users when working with various aspects of your monorepo’s tooling. Using our previous example of new-branch
it may make sense to create multiple commands for working with branches.
In this example, let’s make our new branch command accessible by running one branch new
and set up the plumbing for more sub-commands of branch
in the future.
-
Add a parent command
To create a parent command that can have nested sub-commands, use the
.commandDir()
function in thebuilder
to reference a relative path from the current file that holds the sub-commands. Let’s assume we want to create a suite of git branch management commands to make it easier for developers to use standard branch naming conventions and more:This function uses the same logic for finding commands as all other commands are found. You can configure various aspects of this logic using the
RootConfig
#commands
setting.Notice that we also have not defined or exported a
handler
function, however, we have defineddemandCommand(1)
. This signals to the CLI that when this command is run without a sub-command, that it should return the help documentation and exit immediately. -
Move the sub-command
Next ensure we have a sub-directory
./commands/branch
, as previously used for thecommandDir()
argument in ourbuilder
. Then we can create ournew
command with the basic 4-export structurecommand
,description
,builder
, andhandler
We should now have a file tree that looks like this:
Directorycommands/
Directorybranch/
- new.ts
- branch.ts
-
Update the command name
Lastly, we need to remember to update the command’s name. We do this by changing the
command
export variable:That’s it! We can now run our new branch command using the parent/sub-command pattern:
-
Adding more commands
Just like with the previous
commands/branch/new.ts
command file. We can continue adding commands to thecommands/branch
directory and they will automatically become sub-commands ofone branch
.
Best practices
Section titled Best practicesoneRepo provides a robust API and suite of tools to flesh out your commands. Please refer to the full API documentation for available methods, namespaces, and interfaces.
Input arguments
Section titled Input argumentsoneRepo exports a handful of builders
for common input arguments and getters
for file and workspace querying based on the builders
’ input arguments.
- Import the
builders
namespace fromonerepo
- For TypeScript, ensure you include the builder types in your arguments type definition.
- Wrap the return value from your exported
builder
with the builders helpers. - The
getters
from the handler extra arguments will automatically be affected by the input arguments used.
Adding the withWorkspaces
and withAffected
builder composition functions to our exported builder
, a few options are automatically added to our command. Looking at the --help
output will describe each one of them in detail:
Finally, running our command, we can see list of workspaces based on those added input argument flags:
Write helpful documentation
Section titled Write helpful documentationThe more explanation and context that you can provide, the better it will be for your peers using commands. Consider adding epilogues and examples along with the required description
.
You can also generate Markdown documentation of the full CLI using the docgen plugin!
Logging
Section titled LoggingoneRepo provides a robust Logger
to all commands and methods. This logger is responsible for tracking output and ensuring that all subprocess output is buffered and redirected appropriately for a better debugging experience.
The logger
instance is primarily available in command handlers via the HandlerExtra
. Logger verbosity is controlled via the global counter argument --verbosity
, or -v
.
Generally speaking, it is best practice to wrap your logs in steps. This will help for better scannability and timing when debugging any issues:
Subprocesses
Section titled SubprocessesoneRepo includes advanced child process spawning via the run
and batch
functions. These async functions work like Node.js child_process.spawn
, but are promise-based asynchronous functions that handle redirecting and buffering output as well as failure tracking for you. These should be used in favor of the direct Node.js builtins.
If the command you’re trying to run is installed by a third party node module through your package manager (NPM, Yarn, or pNPM), you are encouraged to use graph.packageManager.run
and graph.packageManager.batch
functions. These will determine the correct install path to the executable and avoid potential issues across package manager install locations.
Run single processes
Section titled Run single processesBatching processes
Section titled Batching processesOften it will make sense to run many things at once. The batch
function handles automatic resource sharing and prevents running too many processes at once for your current machine.
File operations
Section titled File operationsoneRepo includes many functions for reading and writing files that ensure safe operation. Developers should use these as much as possible. Check the file
API documentation for a full list of available helpers.
Limiting workspaces
Section titled Limiting workspacesBy using the builders
helpers, your command will automatically have extra input arguments available that help limit which workspaces will be returned from the getWorkspaces
function. By default, --affected
will be set to true
unless you specify another option when invoking the command.
Automated tests
Section titled Automated testsAvoiding writing tooling that is easily mis-interpreted and prone to breaking by writing functional tests around your custom commands. The oneRepo suite includes the ability to set up a mock environment to run commands with CLI flags and assert on their behaviors within both Jest and Vitest.
To get started, install the @onerepo/test-cli
package as a development dependency:
More examples
Section titled More examplesDid you also know that oneRepo plugins are written nearly identical to the way commands are custom written in your monorepo? The official oneRepo plugins are the best source for up-to-date, working examples of commands.
- Determining appropriate files and default setup for a single process: @onerepo/plugin-eslint
- Batching multiple processes against affected or input workspaces: @onerepo/plugin-typescript