cmd-ts Documentation

repository·main·Indexed 18 days ago

https://github.com/schniz/cmd-ts

A type-driven command line argument parser for TypeScript (v0.15.0) that provides static and runtime typechecking. It allows developers to define CLI commands with complex types using a composable API. Features include built-in primitive types (string, number, boolean), custom type decoding via the Type<A, B> interface, and specialized battery packs for File System and URL validation.

Tokens
15.1K
Snippets
60
Records
81
Agent score
61%

What's inside cmd-ts

  1. What is cmd-ts

    main
    cmd-ts is a type-driven command line argument parser for TypeScript. It acts as an adapter between the user's shell and your application code. Unlike traditional parsers that treat all arguments as strings, cmd-ts uses a Type construct to provide both static (TypeScript) and runtime typechecking. This ensures that arguments like numbers, integers, or file paths are validated before they reach your application logic, providing better autocomplete for developers and clearer error messages for users.
  2. Understand cmd-ts Battery Packs

    main

    cmd-ts uses the concept of "Battery Packs" to provide optional, specialized functionality that is not required for the core library to function. These packs are designed to minimize core dependencies and are only imported when needed. Battery packs may have their own specific dependencies or be optimized for specific runtimes (such as Node.js or the browser).

    Available battery packs include:

    • File System: For filesystem-related operations.
    • URL: For URL parsing and manipulation.
  3. How custom type decoding works

    main

    You can extend cmd-ts by defining custom types that transform a string input into a complex object (like a File Stream, a Date, or a UUID). This allows you to move validation and transformation logic into the parser, providing consistent error handling and type safety in your handler.

    A custom type is an object implementing the Type<string, T> interface, which requires an async from(str: string): Promise<T> method. If the input is invalid, you can throw an error from within from, which cmd-ts will catch and report as a CLI error.

    import { Type } from 'cmd-ts';
    import fs from 'fs';
    
    // Type<string, Stream> reads as "A type from `string` to `Stream`"
    const ReadStream: Type<string, Stream> = {
      async from(str) {
        if (!fs.existsSync(str)) {
          throw new Error('File not found');
        }
    
        return fs.createReadStream(str);
      },
    };
  4. Understand the Parser and Combinator model in cmd-ts

    main

    In cmd-ts, command-line applications are built by composing small, specialized parsers into larger structures. An argument parser is a struct that provides a parse function and an optional register function.

    By combining these parsers, you can build complex CLI tools with nested commands, options, and arguments. The library provides several built-in primitives to facilitate this composition:

    The library provides the following building blocks:
    - `positional` and `restPositionals`: Read arguments by their position.
    - `option` and `multioption`: Read binary `--key value` arguments.
    - `flag` and `multiflag`: Read unary `--key` arguments.
    - `command`: Compose multiple arguments into a single command-line application.
    - `subcommands`: Compose multiple command-line applications into one unified application.
    - `binary`: Transform a command-line application into a UNIX-executable-ready command.
  5. Create subcommands with `subcommands()`

    main

    The subcommands combinator allows you to group multiple command instances into a single container command. When the container command is executed, the first argument provided by the user determines which of the registered subcommands will be run. You can nest subcommands within other subcommands to create complex, hierarchical command structures.

    Configuration Options

    OptionRequiredDescription
    nameYesA name for the container command.
    versionNoThe version of the container command.
    cmdsYesAn object where keys are the subcommand names and values are command or subcommands instances.
    import { command, subcommands, run } from 'cmd-ts';
    
    const cmd1 = command({
      /* ... */
    });
    const cmd2 = command({
      /* ... */
    });
    
    // Create a subcommand group
    const subcmd1 = subcommands({
      name: 'my subcmd1',
      cmds: { cmd1, cmd2 },
    });
    
    // Nest subcommands within another subcommand group
    const nestingSubcommands = subcommands({
      name: 'nesting subcommands',
      cmds: { subcmd1 },
    });
    
    run(nestingSubcommands, process.argv.slice(2));
  6. Use `onMissing` for dynamic argument fallbacks

    main

    The onMissing property allows a custom type to determine its own fallback value when an argument is not provided by the user. This is useful for searching standard configuration paths or environment variables. onMissing is triggered if the argument is omitted and no defaultValue is specified.

    const ConfigFile: Type<string, Config> = {
      async from(str) {
        if (!fs.existsSync(str)) {
          throw new Error(`Config file not found: ${str}`);
        }
        return JSON.parse(fs.readFileSync(str, 'utf8'));
      },
      
      displayName: 'config-file',
      
      async onMissing() {
        // Look for config in standard locations when not provided
        const candidates = [
          './config.json',
          path.join(os.homedir(), '.myapp', 'config.json'),
          '/etc/myapp/config.json'
        ];
        
        for (const candidate of candidates) {
          if (fs.existsSync(candidate)) {
            return JSON.parse(fs.readFileSync(candidate, 'utf8'));
          }
        }
        
        return { debug: false, verbose: false };
      },
    };
  7. How the `command` combinator works

    main
    In cmd-ts, a command is a combinator that aggregates multiple parsers into a single unit. It defines a command structure that can be executed via its run function, which processes raw user input according to the defined arguments and handlers.
  8. How cmd-ts handles type-driven parsing

    main

    The core philosophy of cmd-ts is to move validation from userland into the parser itself using the Type construct. This approach solves common CLI issues by ensuring:

    • Numeric validation: Automatically errors if a string is provided where a number is expected.
    • Integer validation: Automatically errors if a float is provided where an integer is expected.
    • Path validation: Automatically errors if a provided path does not exist or is not a readable file.

    By using these types, your command implementation receives strongly-typed values instead of raw strings, reducing the need for manual parsing and error handling within your business logic.

  9. Handle Node.js binary and command paths with binary()

    main

    When running a standard Node.js executable, the first two arguments in process.argv are often the path to the Node executable and the path to the command script itself. To prevent these from being treated as unexpected positional arguments by your command, use the binary() helper to wrap your command definition. This helper automatically ignores the first two elements of the argument array.

    import { binary, command, run } from 'cmd-ts';
    
    const myCommand = command({
      /* ... */
    });
    
    // Wrap the command to ignore the first two arguments (node path and script path)
    const binaryCommand = binary(myCommand);
    
    run(binaryCommand, process.argv);
  10. Basic usage of cmd-ts

    main

    To create a CLI, use the command function to define the command's metadata (name, description, version), its arguments, and a handler function. Arguments can be defined as positional or option. Use run to execute the command with process.argv.slice(2).

    import { command, run, string, number, positional, option } from 'cmd-ts';
    
    const cmd = command({
      name: 'my-command',
      description: 'print something to the screen',
      version: '1.0.0',
      args: {
        number: positional({ type: number, displayName: 'num' }),
        message: option({
          long: 'greeting',
          type: string,
        }),
      },
      handler: (args) => {
        args.message; // string
        args.number; // number
        console.log(args);
      },
    });
    
    run(cmd, process.argv.slice(2));
  11. Implement dynamic defaults with `onMissing`

    main

    The onMissing callback allows you to dynamically generate values when a flag is absent. This is ideal for checking environment variables, reading configuration files, or performing async lookups. It serves as a fallback if defaultValue is not provided.

    onMissing can be either a synchronous or asynchronous function.

    import { command, flag } from 'cmd-ts';
    
    const verboseFlag = flag({
      long: 'verbose',
      short: 'v',
      description: 'Enable verbose output',
      onMissing: () => {
        // Check environment variable as fallback
        return process.env.NODE_ENV === 'development';
      },
    });
    
    const debugFlag = flag({
      long: 'debug',
      short: 'd',
      description: 'Enable debug mode',
      onMissing: async () => {
        // Async example: check config file or make API call
        const config = await loadConfig();
        return config.debug || false;
      },
    });
    
    const cmd = command({
      name: 'my app',
      args: { 
        verbose: verboseFlag,
        debug: debugFlag,
      },
      handler: ({ verbose, debug }) => {
        console.log(`Verbose: ${verbose}, Debug: ${debug}`);
      },
    });