A command-line interface (CLI) framework for Node.js and web browser
The @carnesen/cli package includes runtime JavaScript files (ES2019 + CommonJS) and TypeScript type declarations (3.7+). It has no npm dependencies and is known to work with Node.js 12+ and all modern web browsers.
Easy to use: We ❤️ CLIs and want to use them everywhere for everything. This library makes it easy to create beautiful well-behaved CLIs for Node.js and for web browsers.
Isomorphic: As far as we know, this is the only isomorphic JavaScript CLI framework. Run your browser CLI in a terminal emulator like our online examples or in the browser's built-in terminal, the JavaScript console!
Intelligent types: This is first and foremost a TypeScript library. All components are intelligently typed, and our built-in parsers ensure that the types are respected at runtime.
Automatic documentation: Build your CLI with our factories, and we'll automatically generate pretty-good command-line usage out of the box. You can add description
s and custom <placeholder>
s to your command objects too, and we'll automatically fold the text to fit the user's terminal.
Built-in ANSI decoration: Bring your CLI to life with our built-in ANSI text decoration methods.
Hidden commands Add "hidden=true" to any argument/command/group, and we'll hide it in the automatic usage docs. We use this feature for easter eggs and internal/beta commands.
Automatic autocomplete (Coming soon!): Autocomplete supercharges a CLI. We've implemented automatic autocomplete in the live examples and plan to add this as a feature to the core library for the next release.
This library has 100% test coverage and heavy usage by its authors but should be considered 0.x beta software.
Questions, bugs, feature requests? Please file an issue or submit a pull request on this project's repository on GitHub, and check out our:
Install this library using npm
:
npm install @carnesen/cli
Here is a TypeScript Node.js CLI that does some basic arithmetic:
// src/multiply.ts
import {
Cli,
CliCommand,
CliNumberArrayArgGroup,
} from '@carnesen/cli';
const multiplyCommand = CliCommand({
name: 'multiply',
description: 'Multiply numbers and print the result',
positionalArgGroup: CliNumberArrayArgGroup({
required: true,
}),
action({ positionalValue: numbers }) {
return numbers.reduce((a, b) => a * b, 1);
},
});
// Export for unit testing
export const cli = Cli(multiplyCommand);
// If this module is the entrypoint for this Node.js process
if (require.main === module) {
cli.run();
}
Here's how that Node.js CLI behaves in a terminal:
The only Node.js-specific code is the if (require.main === module)
block. To instead make a web browser console CLI, replace that with:
(window as any).multiply = (line: string) => {
cli.runLine(line);
};
Here's that CLI in a JavaScript console:
The resolved value of 0
means the command finished successfully. A non-zero exit code means an error occurred. Try it you yourself at cli.carnesen.com! Here's how to open the console in Firefox and Google Chrome.
The general structure of a @carnesen/cli
command line is:
<command group> <command> \
<positional-args> \
--name <args> \
-- <double-dash-args>
Only <command>
is required. This section of the documentation describes each of these pieces in more detail.
A command defines an action function
or async function
together with its command-line arguments. For example, in cloud users list
, the command is list
. Some commands don't have any arguments. For example:
import { CliCommand } from '@carnesen/cli';
export const listUsersCommand = CliCommand({
name: 'list',
async action() {
// Fetch all users
}
})
CliCommand
is a factory function (not a constructor) that returns an object of shape ICliCommand
.
Most commands define arguments through the positionalArgGroup
, namedArgGroups
, and/or doubleDashArgGroup
properties as described below.
You can use command groups to organize the commands in your CLI. For example, in cloud users list
, cloud
and users
are command groups and list
is a command. Command groups are optional. Organize your CLI to suit your needs and taste:
list-cloud-users
: No command groupscloud list-users
: A single command groupcloud users list
: A hierarchical command treeimport { CliCommandGroup } from '@carnesen/cli';
import { listUsersCommand } from './list-users-command';
export const usersCommandGroup = CliCommandGroup({
name: 'users',
subcommands: [ listUsersCommand ]
})
export const rootCommandGroup = CliCommandGroup({
name: 'cloud',
subcommands: [ usersCommandGroup ]
})
CliCommandGroup
is a factory function (not a constructor) that returns an object of shape ICliCommandGroup
.
A command's positionalArgGroup
receives all the command-line arguments after the command but before the first argument that starts with --
. For example, in cloud users delete carl karen --force
, the positional arguments are carl
and karen
. The argument group's parsed value is the positionalValue
property of the action input:
import { CliCommand, CliStringArrayArgGroup } from '@carnesen/cli';
export const deleteCommand = CliCommand({
name: 'delete',
positionalArgGroup: CliStringArrayArgGroup({ required: true }),
async action({ positionalValue: usernames }) {
// The CliStringArrayArgGroup parser returns an array of strings e.g.
// ["carl", "karen"]
// Delete the users ...
}
})
The CliStringArrayArgGroup
parser throws a CliUsageError
when required == true
and no argument is provided. Conversely, if required == false
and no argument is provided, positionalValue
is undefined
.
A command's namedArgGroups
receives all the command-line arguments of the form --name value
. The parsed values are passed into the command's action as a property called namedValues
of shape { name: <value>, ... }
. For example, in cloud users list
let's add a command-line flag to filter out inactive users:
import { CliCommand, CliFlagArgGroup } from '@carnesen/cli';
export const listCommand = CliCommand({
name: 'list',
namedArgGroups: {
active: CliFlagArgGroup(),
},
async action({ namedValues: { active } }) {
// CliFlagArgGroup returns false unless --active was passed
// Fetch the users ...
}
})
All command-line arguments after a lone --
are passed to the command's doubleDashArgGroup
. After the lone --
, things like --name
aren't interpreted as argument group separators. This is particularly useful for passing arguments through to another CLI like do -- git --version
. The argument group's return value is the doubleDashValue
property of the action's input:
import { CliCommand, CliStringArrayArgGroup } from '@carnesen/cli';
export const doCommand = CliCommand({
name: 'do',
doubleDashArgGroup: CliStringArrayArgGroup(),
async action({ doubleDashValue: args }) {
// Do stuff ...
}
})
By default, @carnesen/cli
console.error
's the full exception thrown by the action
function or argument parser, stack trace and all. Two special error objects have non-default handling:
CliUsageError
: Throw this if you want to print the selected command's usage and exit without showing a stack trace. This is useful for defining custom argument parsers.CliTerseError
: Throw this if you want to prints the object's message
and exit without showing a stack traceIn any case, the command runner exits (returns) a non-zero numeric status code (1
) or the thrown exception's exitCode
if it's a number. CliTerseError
's second argument is exitCode
.