npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@why-ts/cli

v0.4.1

Published

Building robust and maintainable command-line interfaces (CLIs) in TypeScript.

Downloads

104

Readme

A Type-safe and Hackable Command-line Library

Building robust and maintainable command-line interfaces (CLIs) in TypeScript.

Quick Start

import { command, option as o, program } from '@why-ts/cli';

(async () => {
  const output = await program({ description: 'Example CLI' })
    .command(
      'say',
      command({ description: 'Prints a message to the console' })
        .option(
          'what',
          o.string({ required: true, description: 'The message to print' })
        )
        .handle(({ args }) => {
          console.log(args.what);
          return 0;
        })
    )
    .run(process.argv.slice(2))
    .catch(() => process.exit(1));

  if (output.kind === 'command')
    console.log(`Result: ${output.result}`);
})();

Build and run the script:

> node index.js say --what="Hello, World!"

Hello, World!
Result: 0
> node index.js --help

Example CLI

Available Commands:

  say     Prints a message to the console   
> node index.js say --help

Prints a message to the console

Required Flags:

  --what     [string]     The message to print   

Commands

Commands are constructed with the command() call and added to a Program with a string name. Command handler is registered by providing a callback to the .handle() function.

import { program, command, option as o } from '@why-ts/cli';
const output = await program()
  .command(
    'ls',
    command().handle(() => console.log('Running list'))
  )
  .run(process.argv.slice(2));

Command objects are immutable. Any function that returns a Command instance is always a new instance.

const command1 = command();
const command2 = command1.option('foo', o.string());
const command3 = command2.handle(console.log);
console.log(command1 === command2); // false
console.log(command2 === command3); // false

Command Handler

A command hander is a function that gets invoked when user specifies to run the command (first entry in the string array passed to .run() is the command name).
The handler function will receive the following arguments:

  • args: Parsed type-safe args
  • argv: Raw args passed to program.run()
  • logger: Logger instance (see more in the Configuration section below)
  • prompter: Prompter instance (see more in the Configuration section below)

Since Commands are immutable, it is possible redefine/override a handler and invoke the "parent" one.
This is usually useful for implementing options and logic that are shared by multiple commands.

Examples
const command = command().handle(({args, argv}) => {
  console.log(args); // typed args
  console.log(argv); // raw argv string array
})

Share options and logic between multiple commands:

const common = command()
  .option('working-directory', o.string())
  .handle(({args}) => {
    if(args.workingDirectory)
      process.chdir(args.workingDirectory);
});

const foo = common
  .options('foo', o.string())
  .handle((params, _super) => {
    await _super(params); // change directory logic is implemented in base command
    console.log(params.args.foo);
  });

Note that in the following example, the first handler is shadowed because it is not explicitly called via the _super pattern in the second handler

command()
  .handle(() => console.log('foo'))
  .handle(() => console.log('bar'));

Command Alias

To specify alias(es) for a command, use an array as the name. The first name will be used to type the output object.

import { program, command, option as o } from '@why-ts/cli';
const output = await program()
  .command(
    ['foo', 'f'],
    command().handle(() => console.log('Running foo'))
  )
  .run(process.argv.slice(2));
  
// output.command == 'foo'
> node index.js foo

Running foo
> node index.js f

Running foo

Command Metadata

Metadata for a command (e.g. description) can be specified in the command() constructor call, or overridden via the .meta() call

Examples
const c1 = command({ description: 'Foo' });
const c2 = c1.meta({ description: 'Bar' });
const c3 = c1.meta((base) => ({ ...base, description: `${base.description} & Bar` }));

Options (Flags)

Options are are constructed with the option.<type>() call and added to a Command with a string name. The options can be accessed in the handler via the args field.

The following types are currently supported:

  • o.string(): single string option, e.g. --foo=orange => {foo: 'orange'}
  • o.strings(): multiple string options, e.g. --foo=orange --foo=apple=> {foo: ['orange', 'apple']}
  • o.number(): single number option, e.g. --foo=42 => {foo: 42}
  • o.numbers(): multiple number options, e.g. --foo=42 --foo=100=> {foo: [42, 100]}
  • o.date(): single date option, value will be passed to new Date()
  • o.dates(): multiple date options
  • o.boolean(): boolean option, e.g. --foo => {foo: true} (The default Parser will also interpret --no-<name>, i.e. --no-foo => {foo: false})
  • o.choice(['orange', 'apple']): only allow the specified string values, e.g. --foo=lemon will throw an error
import { command, option as o } from '@why-ts/cli';
command()
  .option('foo', o.string())
  .option('bar', o.number())
  .option('baz', o.boolean())
  .handle(({ args }) => console.log(args)); // type of `args`: {foo?:string, bar?:number, baz?:boolean}
Examples

Basic usage:

> ts-node index.ts --foo=orange --bar=7 --baz
# {foo: 'orange', bar: 7, baz: true}

Boolean fields will produce a false value for 0, n & false:

> ts-node index.ts --baz=false
# {baz: false}

Boolean false values can also be specified with the --no-<name> option:

> ts-node index.ts --no-baz
# {baz: false}

Aliases

Option aliases can be specified by using an array as the option name. The first name will be used to type the args object.

command()
  .option(['foo', 'f'], o.string())
  .handle(({ args }) => console.log(args)); // type of `args`: {foo?:string}
Examples
> ts-node index.ts -f=orange
# {foo: 'orange'}

Mandatory Options

Options can be marked as required.
If user did not specify the option via command line, an error will be thrown.

import { command, option as o } from '@why-ts/cli';
command()
  .option('foo', o.string({ required: true }))
  .option('bar', o.number({ required: true }))
  .option('baz', o.boolean({ required: true }))
  .handle(({ args }) => console.log(args)); // type of `args`: {foo:string, bar:string, baz:boolean}
Examples
> ts-node index.ts --bar=7 --baz
# Error: --foo is required

String Choices

Only allow a specified list of strings. When user provides values other than the specified ones, a UsageError will be thrown.

import { command, option as o } from '@why-ts/cli';
command()
  .option('foo', o.choice(['apple', 'orange']))
  .handle(({ args }) => console.log(args)); // type of `args`: {foo:'apple'|'orange'}

Multiple Values

Allow user to specify a option more than once. Values are represented as an array at runtime.

import { command, option as o } from '@why-ts/cli';
command()
  .option('foo', o.strings())
  .option('bar', o.numbers())
  .handle(({ args }) => console.log(args)); // type of `args`: {foo?:string[], bar?:string[]}

Example:

> ts-node index.ts --foo=orange --foo=apple --bar=7 --bar=42
# {foo: ['orange', 'apple'], bar: [7, 42]}

Environment Variables

An option can fallback to an environment variable if not specified via command line

(See "Option Value Flow" section for the order of execution)

import { command, option as o } from '@why-ts/cli';
command()
  .option('foo', o.string({ env: 'MY_FOO' }))
  .option('bar', o.number({ env: 'MY_BAR' }))
  .handle(({ args }) => console.log(args));

By default, enviroment variables will be read from process.env and basic transformation is applied. Provide a custom Env implementation to customize the behavior. (See more in Configuration section)

Examples
> MY_FOO=apple MY_BAR=42 ts-node index.ts
# {foo: 'apple', bar: 42}

Custom Fallback Value

An option can be configured to have a fallback.

(See "Option Value Flow" section for the order of execution)

import { command, option as o } from '@why-ts/cli';
command()
  .option('foo', o.string({ fallback: () => 'orange' }))
  .option('bar', o.number({ fallbaack: () => 42 }))
  .handle(({ args }) => console.log(args));

Custom Validation

While this library provides basic validations on user input (e.g. make sure a number value is provided to a number option), custom validations can be added over that.

(See "Option Value Flow" section for the order of execution)

import { command, option as o } from '@why-ts/cli';
command()
  // the validate function supports other return types for more control including
  // custom error message and value transformation, see inline code documentation
  .option('bar', o.number({ validate: (v) => v > 10 }))
  .handle(({ args }) => console.log(args));
Examples
> ts-node index.ts --bar=5
# Error: --bar is invalid

Option Value Flow

Here is the order of how an option value is parsed and validated:

  1. Parse value from command line
  2. If undefined, read value from environment variable if env name is defined
  3. If still undefined, invoke fallback if defined
  4. If still undefined and required, throw an error
  5. Run built-in validation
  6. Run custom validation if defined

Runtime Configuration

There are various runtime configurations to customise program behaviour. Specific configurable functionality is listed in the following sub-sections. They can be provided/overridden at multiple places, listed below with lower-priority first:

At Program definition:

program({logger: ...})

At Command definition:

command({logger: ...})

At execution:

program().run(argv, {logger:...})

If none is provided anywhere, it will fallback to an internal default.

Argument Parsing

  • Represented by the Parser interface.
  • Controls how shell arguments (string array) are parsed into typed values
  • The default implementation is based on the minimist package.

Environment Variable Handling

  • Represented by the Env interface.
  • Controls how environment variables are retrieved and interpreted.
  • The default implementation reads values from process.env. Then the value is transformed with parseFloat for numbers and splitting at comma(,) for array values.

Logging

  • Represented by the Logger interface.
  • Controls how output is logged to screen.
  • The default implementation is console.log and console.error

Prompt for User Input

  • Represented by the Prompter interface.
  • Controls how input is captured from user.
  • The default implementation is based on Node.js readline module.

Help Formatter

Controls how the help text is formatted.

TODO

  • More data types (e.g. Set, Map, etc)
  • allow customisation of falsy values
  • Shell completion
  • Auto prompt for missing options if {required: 'prompt'}
  • Locale
  • Option relationships (is it possible to represent them at type level?)
    • dependencies (y is required if x is defined)
    • validation (takes y into account when validating x)
    • exclusivity (x and y cannot be specified together)