On this page

This page shows how to add a command to the doc-kit CLI (@doc-kit/cli, in packages/cli). Add one for a new action. For a new output, write a generator instead: doc-kit generate --target already runs custom generators.

The CLI is built on Commander. Each command is a module in packages/cli/bin/commands/ whose default export is a Commander Command. packages/cli/bin/cli.mjs adds every command listed in packages/cli/bin/commands/index.mjs to the program. The commands that exist today are documented on the CLI page.

import logger from '@doc-kit/core/logger/index.mjs';
import { Command, Option } from 'commander';

import { errorWrap } from '../utils.mjs';

export default new Command('my-command')
  .description('Does something useful')
  .addOption(new Option('-f, --force', 'Overwrite existing files'))
  .action(
    errorWrap(async opts => {
      logger.info(`Running with force: ${Boolean(opts.force)}`);
    })
  );

Wrap every action in errorWrap from packages/cli/bin/utils.mjs. It awaits the action, logs anything it throws (synchronously or not) with the doc-kit logger, and exits with code 1. To fail a command, throw an Error.

Log through the logger from @doc-kit/core/logger/index.mjs rather than console. A preAction hook in cli.mjs sets its level from --log-level before any command runs.

Import the command in packages/cli/bin/commands/index.mjs and add it to the exported array:

import generate from './generate.mjs';
import myCommand from './my-command.mjs';
import preview from './preview.mjs';

export default [generate, preview, myCommand];

Nothing else needs to change. Commander builds the help text from the command's description and options, so doc-kit --help lists the new command and doc-kit my-command --help shows its options.

Define options with Commander's Option class, as generate.mjs does, and positional arguments with .argument(). The action receives the positional arguments first, then the parsed options, then the Command itself.

export default new Command('my-command')
  .argument('<name>', 'What to act on')
  .addOption(new Option('-o, --output <directory>', 'The output directory'))
  .addOption(new Option('--count <number>', 'How many').argParser(Number))
  .addOption(
    new Option('--mode <mode>', 'How to run')
      .choices(['fast', 'safe'])
      .default('safe')
  )
  .action(
    errorWrap(async (name, opts) => {
      // opts.output, opts.count, opts.mode
    })
  );
SyntaxMeaning
--flagBoolean, true when present
--name <value>Takes a required value
--name [value]Takes an optional value
--name <values...>Takes one or more values, collected in an array

Values arrive as strings unless you add an .argParser(). Multi-word flags are camelCased on the options object, so --git-ref becomes opts.gitRef. See the Commander options documentation for the full API.

--log-level is defined on the program, not on each command, so it works on either side of the command name (doc-kit --log-level debug my-command or doc-kit my-command --log-level debug). Don't add it to your command.

  • Try it from the repository root with node --run run -- my-command --help, which runs node packages/cli/bin/cli.mjs.
  • Keep the command thin. @doc-kit/cli publishes only bin/ (see files in packages/cli/package.json), and node --run test only picks up tests under packages/*/src/ and scripts/, never bin/. Put logic worth testing in @doc-kit/core, with its tests in a __tests__ directory next to it.
  • Document it with a section on the CLI page (docs/cli.md), using the same option-list format as the existing commands, and in packages/cli/README.md if it changes how people use the package. If it adds a flag that sets configuration, name the key it sets, as the generate options do.
  • Add a changeset with node --run changeset, since @doc-kit/cli is published. See Adding a changeset.