Composable Builders

Shows how to create reusable option builders that can be shared across commands. This pattern reduces duplication when multiple commands need the same options like verbosity, output format, or configuration paths.

Common Options

Start by defining reusable option builders for common patterns. These can be imported by any command that needs them.

1import { makeComposableBuilder } from 'cli-forge';
2
3/**
4 * Adds a --verbose flag for detailed output.
5 * This option is shared across many commands.
6 */
7export const withVerbose = makeComposableBuilder((args) =>
8  args.option('verbose', {
9    type: 'boolean',
10    alias: ['v'],
11    description: 'Enable verbose output',
12    default: false,
13  })
14);
15
16/**
17 * Adds a --config option for specifying a config file path.
18 */
19export const withConfig = makeComposableBuilder((args) =>
20  args.option('config', {
21    type: 'string',
22    alias: ['c'],
23    description: 'Path to configuration file',
24  })
25);
26
27/**
28 * Adds a --dry-run flag for preview mode.
29 */
30export const withDryRun = makeComposableBuilder((args) =>
31  args.option('dryRun', {
32    type: 'boolean',
33    description: 'Preview changes without executing',
34    default: false,
35  })
36);

Output Options

Group related options together. The output builders handle format selection and file output.

1import { makeComposableBuilder } from 'cli-forge';
2
3/**
4 * Output format choices. Using a const array allows TypeScript
5 * to infer the literal union type.
6 */
7export const OUTPUT_FORMATS = ['json', 'yaml', 'table', 'plain'] as const;
8export type OutputFormat = (typeof OUTPUT_FORMATS)[number];
9
10/**
11 * Adds a --format option for controlling output format.
12 * The choices are constrained to the OUTPUT_FORMATS array.
13 */
14export const withFormat = makeComposableBuilder((args) =>
15  args.option('format', {
16    type: 'string',
17    alias: ['f'],
18    description: 'Output format',
19    choices: OUTPUT_FORMATS,
20    default: 'plain' as OutputFormat,
21  })
22);
23
24/**
25 * Adds a --output option for specifying output file path.
26 */
27export const withOutputFile = makeComposableBuilder((args) =>
28  args.option('output', {
29    type: 'string',
30    alias: ['o'],
31    description: 'Write output to file instead of stdout',
32  })
33);

Build Command

The build command composes multiple option builders using the chain function. TypeScript correctly infers the combined type of all options in the handler.

1import { chain, makeComposableBuilder } from 'cli-forge';
2
3import { withVerbose, withDryRun } from '../builders/common';
4import { withFormat, withOutputFile } from '../builders/output';
5
6/**
7 * The build command composes multiple option builders.
8 * TypeScript correctly infers the combined type of all options.
9 */
10export const buildCommand = makeComposableBuilder((args) =>
11  args.command('build', {
12    description: 'Build the project',
13    builder: (cmd) =>
14      chain(
15        cmd,
16        withVerbose,
17        withDryRun,
18        withFormat,
19        withOutputFile
20      ).option('target', {
21        type: 'string',
22        description: 'Build target',
23        default: 'production',
24      }),
25
26    handler: (args) => {
27      // All composed options are available with correct types
28      if (args.verbose) {
29        console.log('Build configuration:');
30        console.log(`  verbose: ${args.verbose}`);
31        console.log(`  dryRun: ${args.dryRun}`);
32        console.log(`  format: ${args.format}`);
33        console.log(`  output: ${args.output ?? 'stdout'}`);
34        console.log(`  target: ${args.target}`);
35      }
36
37      if (args.dryRun) {
38        console.log('Dry run: would build project');
39        return;
40      }
41
42      console.log(`Building for ${args.target}...`);
43    },
44  })
45);

Serve Command

The serve command reuses withVerbose from common options, ensuring the --verbose flag behaves identically across commands.

1import { chain, makeComposableBuilder } from 'cli-forge';
2
3import { withVerbose, withConfig } from '../builders/common';
4
5/**
6 * The serve command reuses the same verbose option as build.
7 * This ensures consistent behavior across commands.
8 */
9export const serveCommand = makeComposableBuilder((args) =>
10  args.command('serve', {
11    description: 'Start development server',
12    builder: (cmd) =>
13      chain(cmd, withVerbose, withConfig).option('port', {
14        type: 'number',
15        alias: ['p'],
16        description: 'Port to listen on',
17        default: 3000,
18      }),
19
20    handler: (args) => {
21      if (args.verbose) {
22        console.log('Server configuration:');
23        console.log(`  verbose: ${args.verbose}`);
24        console.log(`  config: ${args.config ?? 'default'}`);
25        console.log(`  port: ${args.port}`);
26      }
27
28      console.log(`Starting server on port ${args.port}...`);
29    },
30  })
31);

Main CLI

The main entry point composes all commands together. Each command brings its own options, all composed from reusable builders.

1import { chain, cli } from 'cli-forge';
2
3import { buildCommand } from './commands/build';
4import { serveCommand } from './commands/serve';
5
6const subcommand = cli('subcommand', {
7  builder: (args) => args.option('foo', { type: 'string' }),
8});
9
10/**
11 * Main CLI that composes commands from separate modules.
12 * Each command uses shared option builders for consistency.
13 */
14const app = cli('composable-demo', {
15  description: 'Demonstrates composable option builders',
16  builder: (args) =>
17    chain(args, buildCommand, serveCommand).command(subcommand),
18});
19
20export default app;
21
22if (require.main === module) {
23  app.forge();
24}

Benefits of This Pattern

  1. Consistency: Options like --verbose behave identically everywhere
  2. Type Safety: The chain function preserves types through composition
  3. Maintainability: Change an option definition once, update everywhere
  4. Discoverability: Option builders serve as documentation for available options

All Example Files

FILE EXPLORER
builders/common.ts
1import { makeComposableBuilder } from 'cli-forge';
2
3/**
4 * Adds a --verbose flag for detailed output.
5 * This option is shared across many commands.
6 */
7export const withVerbose = makeComposableBuilder((args) =>
8  args.option('verbose', {
9    type: 'boolean',
10    alias: ['v'],
11    description: 'Enable verbose output',
12    default: false,
13  })
14);
15
16/**
17 * Adds a --config option for specifying a config file path.
18 */
19export const withConfig = makeComposableBuilder((args) =>
20  args.option('config', {
21    type: 'string',
22    alias: ['c'],
23    description: 'Path to configuration file',
24  })
25);
26
27/**
28 * Adds a --dry-run flag for preview mode.
29 */
30export const withDryRun = makeComposableBuilder((args) =>
31  args.option('dryRun', {
32    type: 'boolean',
33    description: 'Preview changes without executing',
34    default: false,
35  })
36);
37