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

furious-commander

v1.7.1

Published

CLI framework with decorators

Downloads

134

Readme

Furious Commander

Tests

Furious Commander is a testable CLI framework, which uses decorators and classes to specify commands in any depth and easily define and use CLI arguments and options.

Install

 $ npm install --save furious-commander

Usage

You can create Command classes to construct the command tree, and assign functionalities.

Basically, there are two kind of Commands: LeafCommand and GroupCommand. These are also available as interfaces, which can help you to implement the required fields and methods.

There is a sample in the test/test-commands.ts file how you can utilize the possibilities of this framework in your application. (This file also used in Jest tests)

An example of a leaf command:

import { Option, ExternalOption, Argument, LeafCommand } from 'furious-commander'

export class TestLeafCommand implements LeafCommand {
  public readonly name = 'testCommand'

  public readonly description = 'This is a testcommand'

  @Option({ key: 'option-test-command-1', description: 'Test option 1 for TestLeafCommand' })
  public option1!: string

  @Argument({ key: 'argument-1', description: 'test argument for TestLeafCommand', required: true })
  public argument1!: string

  @ExternalOption('api-url')
  public apiUrl!: string

  @ExternalOption('group-option-1')
  public groupOption!: string

  public run(): void {
    console.log(`I'm ${this.name}. option-test-command-1: ${this.option1}.`)
    console.log(' I COMMAND!!!!!4!')
    // (...)
  }
}

and a group command:

import { Option, ExternalOption, Argument, GroupCommand } from 'furious-commander'

export class TestGroupCommand implements GroupCommand {
  public readonly name = 'testGroupCommand'

  public subCommandClasses = [TestLeafCommand]

  public readonly description = 'This is a GroupCommand'

  @Option({ key: 'group-option-1', description: 'Test option 1 for export class TestGroupCommand' })
  public option1!: string
}

Other "root level" options and configuration of the CLI can be passed on the invocation of the cli method. This method initializes all your passed root Command classes, until its last LeafCommands (and make it available for later). For this you can find example in test/index.spec.ts

example for cli method call with the command class above:

const commandBuilder = await cli({
  rootCommandClasses: [TestGroupCommand],
  optionParameters: [
    {
      key: 'api-url',
      description: 'URL of the EP that I will Command!',
      default: 'http://obedient-service.local',
    },
  ],
})

Through the commandBuilder object, the initialized classes are reachable.

If you build successfully your cli project (e.g. #!/usr/bin/env node presents at the top of the source file...) You can call the command with the setup above:

 $ <your-cli-app> testGroupCommand testCommand testargument --api-url http://localhost:8080

This will invoke the testCommand's (async) run method, after its fields initialized by Furious Commander.

Conditional Required

When an option is required, you can simply specify { required: true } and the parser will print a helpful error message every time the user tries running your application without providing that option.

This may not always be your use-case though. Let's say you have an --interactive global option, which enables some features, such as allowing the user to provide a value (for that option) in your application logic, after the parser has run.

For such cases, the required property can take { when: string } and { unless: string } values, where the strings represent the dependee keys.

Following the previous example, you may want to specify { required: { unless: 'interactive' } }, and the option will no longer throw error when running with the --interactive option (or its alias).

The reverse of this situation when you have these features enabled by default, but your application has a --silent or --quiet mode, where you do not allow any interactivity and the required options must all be passed beforehand. To indicate this, use { required: { when: 'quiet' } }.

Aggregation

The framework support aggregated relations between independent commands. It means, one command is able to reach the properties of other command. The configurations of the referenced aggregated command will be applied to the base class, that defines the relation. In order to build up this relation, an arbitrary field of the class has to be decorated with Aggregation.

A sample class to show how it is possible

import { Aggregation } from 'furious-commander'

export class TestCommand {

  @Aggregation(['group-command', 'furious-command']) // CLI path of the referenced Command
  public aggregatedRelation!: FuriousCommand // Type in here the class of the aggregated command

  // (...)
}

By this, TestCommand can invoke any function of FuriousCommand, and FuriousCommand all Options and Arguments have been initialized by the framework. Of course, all required parameters of aggregated relations have to be defined in the CLI call. For instance, FuriousCommand has required argument argument-1, when calling TestCommand, argument-1 has to be passed as well even if it is not defined directly in TestCommand.

You can see this functionality in action on test should reach aggregated command fields.

Allow only either one param

It is possible to allow only one parameter of the given option/argument key set. This key set consists required parameters for the successful run, but only one of them has to be defined.

export class TestCommand13 implements LeafCommand {
  public readonly name = 'testCommand13'

  public readonly description = 'This is the testcommand13'

  @Option({ key: 'option1', description: 'Test option1 for TestCommand13', required: true, conflicts: 'option2' })
  public option1!: string;

  @Option({ key: 'option2', description: 'Test option2 for TestCommand13', required: true, conflicts: 'option1' })
  public option2!: string;

  public run(): void {
    // (...)
  }
}

By this setup I cannot call TestCommand13 with both options option1 and option2, but I have to pass at least one of these.

Check where the value of an option or argument originates from

Suppose you have an option which has a default value and may also be specified via process.env. To indicate this, the option receives the default and envKey properties.

import { Option } from 'furious-commander'

@Option({ key: 'host', description: 'Host', envKey: 'HOST', default: 'http://localhost' })
public host!: string;

Later on, you may want to know where its value came from - whether it was specified with --host explicitly, got its value from process.env, or used the default value.

To learn this information, use Utils.getSourcemap() after calling the cli.

import { Utils } from 'furious-commander'

process.env.HOST = '...'

const sourcemap = Utils.getSourcemap()

sourcemap.host // 'env'

sourcemap[key] holds values 'explicit', 'env', 'default' or undefined accordingly.

Customised messages

There are two ways to customise the content and style of printed messages: the application and printer options.

Printer

To control how the framework prints help and error messages, implement the Printer interface and pass it to cli.

The following example uses ANSI escape codes to make important messages bold, dim messages grayed out, and adds > and ! characters before headings and errors respectively.

{
  print: text => console.log(text),
  printError: text => console.error('! ' + text),
  printHeading: text => console.log('> ' + text),
  formatDim: text => '\x1b[2m' + text + '\x1b[0m',
  formatImportant: text => '\x1b[1m' + text + '\x1b[0m',
  getGenericErrorMessage: () => 'Failed to run command!',
}

Application

Some messages refer to your application by its name or its command for customised, user friendly messages and usage examples.

This can be controlled by passing an application property of interface Application when invoking cli.

{
  name: 'Furious Commander',
  command: 'furious-commander',
  version: '1.0.0',
  description: 'Powered by furious technologies'
}

Having done that, help will include messages such as these:

Furious Commander 1.0.0 - Powered by furious technologies

and

Usage:

furious-commander http get <url> [OPTIONS]

Human-Readable Numbers

Numbers and BigInts may be formatted with underscores (_) at will for better readability:

--price 100_000_000

Underscores are simply omitted before the parsing cycle. They can appear anywhere and you may have as many of them as you want.

Furthermore, you can use the following shorthand units:

| Unit     | Lower | Upper | Example  |
|----------|-------|-------|----------|
| Thousand | k     | K     | 120K     |
| Million  | m     | M     | 84m      |
| Billion  | b     | B     | 200_000B |
| Trillion | t     | T     | 19t      |

Hex-Strings

There is a built-in hex-string type that does the following checks and transformations:

  • Ensures only a-f and 0-9 characters are given
  • Allows uppercase, but transforms to lowercase
  • Allows 0x prefix, but omits it
  • Checks for even length, unless oddLength: true property is given
  • Obeys length, minimumLength and maximumLength rules

Autocomplete

All commands support autocomplete out of the box. The currently supported shells are bash, zsh and fish. bash uses complete and COMPREPLY, zsh uses compdef and compadd, fish uses complete (which is different from bash complete).

If an option or argument should accept and autocomplete a path, add the autocompletePath property.

To enable autocompletion, the end user has to install it automatically or manually.

--generate-completion will print the shell, configuration path and the autocomplete script.

--install-completion appends it to the file at the configuration path additionally.

Autocomplete only needs to be installed once, as the suggestions are handled dynamically by the application - meaning future changes will be picked up automatically.

This is achieved by calling the application with the --compgen flag, as well as --compfish, --compzsh or --compbash accordingly, and the actual line (or "buffer") that is being written by the end user.

Setup your project

In order to use decorators in your project (until it's not available in vanilla JS) you should use typescript with the following configuration:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    // (...)
  }
  // (...)
}

For babel use @babel/plugin-proposal-decorators (suggested with legacy: true) followed by @babel/plugin-proposal-class-properties plugins.

Test

 $ npm run test