Skip to main content

Command Structure

Commands in doc-kit are modules that export a command object conforming to the Command interface:
interface Command {
  name: string;
  description: string;
  options: { [key: string]: Option };
  action: (options: any) => Promise<void>;
}
Each command consists of:
  • name - The command name used in the CLI (e.g., generate, interactive)
  • description - A short description shown in help text
  • options - An object mapping option names to their definitions
  • action - The async function that executes when the command is run

Creating a New Command

Step 1: Create the Command File

Create a new file in bin/commands/ with your command name:
// bin/commands/my-command.mjs
import logger from '../../src/logger/index.mjs';

/**
 * @type {import('./types').Command}
 */
export default {
  name: 'my-command',
  description: 'Does something useful',

  options: {
    // Define your options here (see next section)
  },

  async action(opts) {
    logger.info('Starting my-command', opts);

    // Your command logic here

    logger.info('Completed my-command');
  },
};

Step 2: Register the Command

Add your command to the exports in bin/commands/index.mjs:
import generate from './generate.mjs';
import interactive from './interactive.mjs';
import myCommand from './my-command.mjs'; // Add this

export default [
  generate,
  interactive,
  myCommand, // Add this
];

Step 3: CLI Auto-Loading

The CLI in bin/cli.mjs automatically loads commands from bin/commands/index.mjs, so no changes are needed there if you followed step 2.

Command Options

Options define the flags and parameters your command accepts. Each option has:
interface Option {
  flags: string[];      // CLI flags (e.g., ['-i', '--input <value>'])
  desc: string;         // Description for help text
  prompt?: PromptConfig; // Interactive mode configuration
}

Defining Options

options: {
  input: {
    flags: ['-i', '--input <patterns...>'],
    desc: 'Input file patterns (glob)',
    prompt: {
      type: 'text',
      message: 'Enter input glob patterns',
      variadic: true,
      required: true,
    },
  },

  force: {
    flags: ['-f', '--force'],
    desc: 'Force overwrite existing files',
    prompt: {
      type: 'confirm',
      message: 'Overwrite existing files?',
      initialValue: false,
    },
  },

  mode: {
    flags: ['-m', '--mode <mode>'],
    desc: 'Operation mode',
    prompt: {
      type: 'select',
      message: 'Choose operation mode',
      options: [
        { label: 'Fast', value: 'fast' },
        { label: 'Thorough', value: 'thorough' },
      ],
    },
  },
}

Flag Syntax

The flag syntax follows standard CLI conventions:
  • <value> - Required argument
  • [value] - Optional argument
  • <values...> - Variadic (multiple values)
  • [values...] - Optional variadic
Examples:
// Required single value
flags: ['-o', '--output <path>']
// Usage: doc-kit my-command --output ./dist

// Optional single value
flags: ['-c', '--config [path]']
// Usage: doc-kit my-command --config ./config.json
//    or: doc-kit my-command (uses default)

// Required multiple values
flags: ['-i', '--input <patterns...>']
// Usage: doc-kit my-command --input "src/**/*.js" "lib/**/*.js"

// Boolean flag (no value)
flags: ['-f', '--force']
// Usage: doc-kit my-command --force

Option Types

The prompt field configures how options are presented in interactive mode.

Text Input

Single-line text input:
prompt: {
  type: 'text',
  message: 'Enter a value',
  initialValue: 'default',
  required: true,
}
Example:
output: {
  flags: ['-o', '--output <path>'],
  desc: 'Output directory',
  prompt: {
    type: 'text',
    message: 'Where should we output the files?',
    initialValue: './dist',
    required: true,
  },
}

Confirmation

Yes/no confirmation:
prompt: {
  type: 'confirm',
  message: 'Are you sure?',
  initialValue: false,
}
Example:
force: {
  flags: ['-f', '--force'],
  desc: 'Force overwrite existing files',
  prompt: {
    type: 'confirm',
    message: 'Overwrite existing files?',
    initialValue: false,
  },
}

Select (Single Choice)

Single choice from a list:
prompt: {
  type: 'select',
  message: 'Choose one',
  options: [
    { label: 'Option 1', value: 'opt1' },
    { label: 'Option 2', value: 'opt2' },
  ],
}
Example:
format: {
  flags: ['--format <type>'],
  desc: 'Output format',
  prompt: {
    type: 'select',
    message: 'Choose output format',
    options: [
      { label: 'JSON', value: 'json' },
      { label: 'Markdown', value: 'md' },
      { label: 'HTML', value: 'html' },
    ],
  },
}

Multi-Select (Multiple Choices)

Multiple choices from a list:
prompt: {
  type: 'multiselect',
  message: 'Choose multiple',
  options: [
    { label: 'Choice A', value: 'a' },
    { label: 'Choice B', value: 'b' },
  ],
}
Example:
target: {
  flags: ['-t', '--target <generators...>'],
  desc: 'Generators to run',
  prompt: {
    type: 'multiselect',
    message: 'Which generators do you want to run?',
    options: [
      { label: 'JSON Simple', value: 'json-simple' },
      { label: 'Legacy HTML', value: 'legacy-html' },
      { label: 'Web', value: 'web' },
    ],
  },
}

Interactive Prompts

The interactive command automatically uses the prompt configuration from your options. When users run:
doc-kit interactive
They’ll be prompted to:
  1. Select a command
  2. Answer all prompts for that command’s options

Prompt Configuration

All prompt types support these fields:
  • message - Question to ask the user (required)
  • type - Input type: text, confirm, select, or multiselect (required)
  • initialValue - Default value (optional)
  • required - Whether the field must have a value (optional, default: false)
Type-specific fields:
  • text: variadic - Allow multiple values (default: false)
  • select/multiselect: options - Array of { label, value } objects

Making Options Interactive-Friendly

Always provide helpful messages and sensible defaults:
threads: {
  flags: ['-p', '--threads <number>'],
  desc: 'Number of threads to use (minimum: 1)',
  prompt: {
    type: 'text',
    message: 'How many threads to allow',
    initialValue: String(cpus().length),  // Smart default
  },
},

Complete Example

Here’s a complete command that validates documentation links:
// bin/commands/validate-links.mjs
import { readFile } from 'node:fs/promises';
import { glob } from 'glob';
import logger from '../../src/logger/index.mjs';

export default {
  name: 'validate-links',
  description: 'Check for broken links in documentation',

  options: {
    input: {
      flags: ['-i', '--input <patterns...>'],
      desc: 'Documentation file patterns to check',
      prompt: {
        type: 'text',
        message: 'Enter file patterns to check (space-separated)',
        initialValue: 'docs/**/*.md',
        variadic: true,
        required: true,
      },
    },

    strict: {
      flags: ['--strict'],
      desc: 'Fail on warnings',
      prompt: {
        type: 'confirm',
        message: 'Treat warnings as errors?',
        initialValue: false,
      },
    },

    format: {
      flags: ['--format <type>'],
      desc: 'Output format',
      prompt: {
        type: 'select',
        message: 'Choose output format',
        options: [
          { label: 'Console', value: 'console' },
          { label: 'JSON', value: 'json' },
          { label: 'Markdown', value: 'md' },
        ],
      },
    },
  },

  async action({ input, strict, format }) {
    logger.info('Validating documentation links', { input, strict, format });

    // Find all matching files
    const files = await glob(input);
    logger.info(`Found ${files.length} files to check`);

    // Validate each file
    const results = [];
    for (const file of files) {
      const content = await readFile(file, 'utf-8');
      const links = extractLinks(content);
      const broken = await checkLinks(links);

      if (broken.length > 0) {
        results.push({ file, broken });
      }
    }

    // Output results
    if (format === 'json') {
      console.log(JSON.stringify(results, null, 2));
    } else if (format === 'md') {
      outputMarkdown(results);
    } else {
      outputConsole(results);
    }

    // Exit with error if strict mode and issues found
    if (strict && results.length > 0) {
      logger.error(`Found ${results.length} files with broken links`);
      process.exit(1);
    }

    logger.info('Validation complete');
  },
};

function extractLinks(content) {
  // Extract markdown links: [text](url)
  const regex = /\[([^\]]+)\]\(([^)]+)\)/g;
  const links = [];
  let match;
  while ((match = regex.exec(content)) !== null) {
    links.push(match[2]);
  }
  return links;
}

async function checkLinks(links) {
  // Check if links are reachable
  // (Implementation details omitted)
  return [];
}

function outputMarkdown(results) {
  console.log('# Broken Links\n');
  for (const { file, broken } of results) {
    console.log(`## ${file}\n`);
    for (const link of broken) {
      console.log(`- ${link}`);
    }
    console.log('');
  }
}

function outputConsole(results) {
  for (const { file, broken } of results) {
    logger.warn(`${file}: ${broken.length} broken link(s)`);
    for (const link of broken) {
      console.log(`  - ${link}`);
    }
  }
}

Usage

# Direct usage
doc-kit validate-links --input "docs/**/*.md" --strict --format json

# Interactive mode
doc-kit interactive
# > Select command: validate-links
# > Enter file patterns: docs/**/*.md
# > Treat warnings as errors? No
# > Choose output format: Console

Best Practices

DO: Provide Helpful Descriptions

options: {
  threads: {
    flags: ['-p', '--threads <number>'],
    desc: 'Number of threads to use (minimum: 1)', // ✅ Clear constraint
    // ...
  },
}

DO: Use Smart Defaults

import { cpus } from 'node:os';

prompt: {
  type: 'text',
  message: 'How many threads?',
  initialValue: String(cpus().length), // ✅ System-aware default
}

DO: Validate Input

async action({ threads }) {
  const numThreads = parseInt(threads, 10);

  if (numThreads < 1) {
    logger.error('Threads must be at least 1');
    process.exit(1);
  }

  // Continue with valid input
}

DON’T: Use Unclear Messages

prompt: {
  message: 'Value?', // ❌ Too vague
}

DON’T: Forget Error Handling

async action(opts) {
  try {
    await doWork(opts);
  } catch (error) {
    logger.error('Command failed', error);
    process.exit(1); // ✅ Exit with error code
  }
}

Next Steps

Architecture

Understand how commands integrate with the generator system

Comparators

Learn how to create build comparators for CI/CD

Build docs developers (and LLMs) love