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, type-safe 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 color decoration: Bring your CLI to life with our color
helpers injected automatically into your command action
function.
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-only/beta commands.
Automatic autocomplete (Coming some day!): 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 in a future release.
REPL (Coming some day!): The live examples are implemented as custom REPL. In a future release we'll lift that code into the framework to make it just as easy to build a REPL as it is to build a CLI.
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:
Previous versions of the software are available on GitHub and the npm registry. Previous versions of the online documentation are available on the Internet Archive:
Install this library using npm
:
npm install @carnesen/cli
Here is a TypeScript Node.js CLI that does some basic arithmetic:
// src/multiply.ts
// The "c" export is a compact API sufficient for most CLIs
import { c } from '@carnesen/cli';
// This CLI has just one command
const multiplyCommand = c.command({
name: 'multiply',
description: 'Multiply numbers and print the result',
positionalArgGroup: c.numberArray(),
action({ positionalValue: numbers }) {
return numbers.reduce((a, b) => a * b, 1);
},
});
// Export for unit testing
export const cli = c.cli(multiplyCommand);
if (require.main === module) {
// This module is the entrypoint for this Node.js process
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> \
--<named arg group 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 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 { c } from '@carnesen/cli';
export const listUsersCommand = c.command({
name: 'list',
async action() {
// Code here to fetch and return all users ...
}
})
[[c.command
]] is an alias for CCliCommand.create
, a factory function that returns a CCliCommand
.
Most commands declare 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. 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 { c } from '@carnesen/cli';
import { listUsersCommand } from './list-users-command';
export const usersCommandGroup = c.commandGroup({
name: 'users',
subcommands: [ listUsersCommand ]
})
export const rootCommandGroup = c.commandGroup({
name: 'cloud',
subcommands: [ usersCommandGroup ]
})
[[c.commandGroup
]] is an alias for CCliCommandGroup.create
, a factory function that returns a CCliCommandGroup
.
An argument group declares a collection of zero or more consecutive command-line arguments parsed together as a single well-typed value. In the example "multiply 1 2 3", the c.numberArray
arg group receives strings from the command-line ["1", "2", "3"]
and parses a number[]
value [1, 2, 3]
. Argument groups require at least one argument unless they're configured with optional: true
in which case zero arguments is OK and the parsed value type includes | undefined
. The @carnesen/cli
package provides argument group factories for many parsed value types (bigint
, bigint[]
, boolean
, number
, number[]
, string
, string[]
, unknown
(JSON)), but you can also define your own custom argument group types by extending CCliArgGroup
.
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 { c } from '@carnesen/cli';
export const deleteCommand = c.command({
name: 'delete',
positionalArgGroup: c.stringArray(),
async action({ positionalValue: usernames }) {
// ^^ `usernames` is typed `string[]`
//
// If we had provided `{optional: true}` to `c.stringArray()`,
// `usernames` would be typed `string[] | undefined`
//
// Code here to delete the users ...
}
})
A command's namedArgGroups
declares argument groups that receive arguments preceded by a named argument group separator --name
. For example in cloud users delete karen --force
, --force
is a named argument group separator with no subsequent arguments. The force
named argument group receives undefined
(no argument provided). The parsed values are passed into the command's action
as a property namedValues
of shape { <name>: <parsed value>, ... }
. Here's an example adding a command-line flag to cloud users list
:
import { c } from '@carnesen/cli';
export const listCommand = c.command({
name: 'list',
namedArgGroups: {
active: c.flag({
description: "If passed, only list active users",
}),
},
async action({ namedValues: { active } }) {
// `active` is true` if --active was passed
// or `false` otherwise
//
// Code here to fetch the users ...
}
})
All command-line arguments after a lone argument --
(the double-dash argument group separator argument) are passed to the command's doubleDashArgGroup
. After the lone --
, things like --name
aren't interpreted as named argument group separators. This is particularly useful for passing arguments through to another program. Suppose we make a CLI for executing commands on a remote system. We might try a command line like exec-remote git --version
. The problem there is that @carnesen/cli
interprets --version
as a named argument group separator. Instead we want to treat it just as a string that gets passed to the git
command on the remote system. Inspired by npm run-script, we'll instead have the user do exec-remote -- git --version
. The doubleDashArgGroup
's action
receives the parsed double-dash arguments as doubleDashValue
:
import { c } from '@carnesen/cli';
export const doCommand = c.command({
name: 'do',
doubleDashArgGroup: c.stringArray(),
async action({ doubleDashValue: args }) {
// Code here to 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:
CCliUsageError
: 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.CCliTerseError
: 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. CCliTerseError
's second argument is exitCode
.