clibuilder
TypeScript icon, indicating that this package has built-in type declarations

8.0.17 • Public • Published

CLI Builder

NPM version NPM downloads

GitHub Release Codecov Codacy Badge

Visual Studio Code Wallaby.js

A highly customizable command line application builder.

What's new in v8

Key highlights:

  • Support standalone CLI
    • name and version are now required and not read from package.json.
  • Plugins are loaded through config
    • This drastically improve startup time, as it does not scan node_modules anymore.
    • Also better support other package manager such as yarn PnP and pnpm.
  • keywords are now used for plugin lookup.
  • Distribute ESM along with CJS.

Feature Highlights

  • support default commands and sub-commands my-cli cmd1 cmd2 cmd3
  • configuration file support
  • plugin support: write commands in separate packages and reuse by multiple CLI
  • type inference and validation for config, arguments, and options
    using zod (exported as z)

Install

# npm
npm install clibuilder

# yarn
yarn add clibuilder

# pnpm
pnpm install clibuilder

#rush
rush add -p clibuilder

Usage

You can use clibuilder to create your command line application in many ways. The most basic way looks like this:

// Define your app
const app = cli({ name: 'app', version: '1.0.0' })
  .default({ run() { /* ...snip... */ }})

// Use your app
app.parse(process.argv)
  .catch(e => /* handle error */process.exit(e?.code || 1))

You can add additional named commands and sub-commands:

cli({ ... })
  .command({ name: 'hello', run() { this.ui.info('hello world') }})
  .command({
    name: 'repo',
    commands:[
      command({ name: 'create', run() { /* ..snip.. */ }})
    ]
  })

Command can have alias:

cli({ ... })
  .command({
    name: 'search-packages',
    alias: ['sp'],
    /* ..snip.. */
  })

// call as: `my-cli sp`

You can specify arguments:

cli({ ... }).default({
  arguments: [
    // type defaults to string
    { name: 'name', description: 'your name' }
  ],
  run(args) { this.ui.info(`hello, ${args.name}`) }
})

cli({ ... }).command({
  name: 'sum',
  arguments: [
    // using `zod` to specify number[]
    { name: 'values', description: 'values to add', type: z.array(z.number()) }
  ],
  run(args) {
    // inferred as number[]
    return args.values.reduce((p, v) => p + v, 0)
  }
})

Of course, you can also specify options:

cli({ ... }).default({
  options: {
    // type defaults to boolean
    'no-progress': { description: 'disable progress bar' },
    run(args) {
      if (args['no-progress']) this.ui.info('disable progress bar')
    }
  }
})

and you can add option alias too:

cli({ ... }).command({
  options: {
    project: {
      alias: ['p']
    }
  }
})

You can use z to mark argument and/or options as optional

cli({... }).default({
  arguments: [{ name: 'a', description: '', type: z.optional(z.string()) }],
  options: {
    y: { type: z.optional(z.number()) }
  }
})

If you invoke a command expecting a config, the config will be loaded. Each command defines their own config.

cli({ ... })
.default({
  config: z.object({ presets: z.string() }),
  run() {
    this.ui.info(`presets: ${this.config.presets}`)
  }
})

Config

Config file can be written in JSON, YAML, cjs, or mjs. Common filename are supported:

  • .{name}.<cjs|mjs|js|json|yaml|yml>
  • .{name}rc.<cjs|mjs|js|json|yaml|yml>
  • {name}.<cjs|mjs|js|json|yaml|yml>
  • {name}rc.<cjs|mjs|js|json|yaml|yml>

You can override the config name too:

cli({ config: 'alt-config.json' })

Plugins

One of the key features of clibuilder is supporting plugins. Plugins are defined inside the config:

{
  "plugins": ["my-cli-plugin"]
}

Defining Plugins

clibuilder allows you to build plugins to add commands to your application. i.e. You can build your application in a distributed fashion.

To create a plugin:

  • export a activate(ctx: PluginActivationContext) function
  • add the keywords in your package.json to make it searchable
import { command, PluginActivationContext } from 'clibuilder'

// in plugin package
const sing = command({ ... })
const dance = command({ ... })

export function activate({ addCommand }: PluginCli.ActivationContext) {
  addCommand({
    name: 'miku',
    commands: [sing, dance]
  })
}

// in plugin's package.json
{
  "keywords": ['your-app-plugin', 'vocaloid']
}

The CLI can search for plugins using the keywords values.

Testing

testCommand() can be used to test your command:

import { command, testCommand } from 'clibuilder'

test('some test', async () => {
  const { result, messages } = await testCommand(command({
    name: 'cmd-a',
    run() {
      this.ui.info('miku')
      return 'x'
    }
  }), 'cmd-a')
  expect(result).toBe('x')
  expect(messages).toBe('miku')
})

shebang

To make your CLI easily executable, you can add shebang to your script:

#!/usr/bin/env node

// your code

Package Sidebar

Install

npm i clibuilder

Weekly Downloads

172

Version

8.0.17

License

MIT

Unpacked Size

741 kB

Total Files

81

Last publish

Collaborators

  • unional