citty

repository·main·Indexed 22 days ago

https://github.com/unjs/citty

An elegant, zero-dependency CLI builder for Node.js. citty provides smart value parsing, nested sub-commands with lazy loading, and a pluggable API with automatic usage generation. It features a type-safe command definition system via defineCommand, a comprehensive execution lifecycle (setup, run, cleanup), and utilities for argument parsing, usage rendering, and plugin integration.

Tokens
4.6K
Snippets
8
Records
34
Agent score
79%

What's inside citty

  1. Argument Access and Boolean Negation

    main

    Case-Agnostic Access

    Kebab-case arguments can be accessed using camelCase. For example, an argument defined as output-dir can be accessed via args.outputDir or args['output-dir'].

    Boolean Negation

    Boolean arguments support the --no- prefix for negation. The negative variant is automatically displayed in the help output if default: true is set or if a negativeDescription is provided.

  2. How command hooks (setup and cleanup) work

    main

    Commands support setup and cleanup functions:

    • setup(): Called before run().
    • cleanup(): Called after run(). It is guaranteed to run even if run() throws an error.

    Only the hooks of the specifically executed command (and its parents/plugins) will run.

    const main = defineCommand({
      meta: { name: "hello", description: "My CLI App" },
      setup() {
        console.log("Setting up...");
      },
      cleanup() {
        console.log("Cleaning up...");
      },
      run() {
        console.log("Hello World!");
      },
    });
  3. How to lazy load sub-commands

    main

    For large CLIs, you can prevent loading all commands at once by providing a function that returns a Promise to the subCommands object. This ensures only the executed command is imported.

    Additionally, meta, args, and subCommands all accept Resolvable<T> values (a value, Promise, function, or async function) for dynamic resolution.

    const main = defineCommand({
      meta: { name: "hello", version: "1.0.0", description: "My Awesome CLI App" },
      subCommands: {
        sub: () => import("./sub.mjs").then((m) => m.default),
      },
    });
  4. How nested sub-commands work

    main

    Commands can be nested recursively using the subCommands property in the defineCommand configuration.

    Sub-commands support:

    • meta.alias: An array of short aliases (e.g., ["i", "add"]). Note: Aliases cannot be used for positional arguments.
    • meta.hidden: true: Hides the sub-command from the help output.
    import { defineCommand, runMain } from "citty";
    
    const sub = defineCommand({
      meta: { name: "sub", description: "Sub command" },
      args: {
        name: { type: "positional", description: "Your name", required: true },
      },
      run({ args }) {
        console.log(`Hello ${args.name}!`);
      },
    });
    
    const main = defineCommand({
      meta: { name: "hello", version: "1.0.0", description: "My Awesome CLI App" },
      subCommands: { sub },
    });
    
    runMain(main);
  5. How to use plugins to extend commands

    main

    Plugins allow you to reuse setup and cleanup logic across multiple commands. You define a plugin using defineCittyPlugin and include it in the plugins array of a command.

    Execution Order:

    • Plugin setup hooks run before the command's setup (in the order they are defined).
    • Plugin cleanup hooks run after the command's cleanup (in reverse order).
    import { defineCommand, defineCittyPlugin, runMain } from "citty";
    
    const logger = defineCittyPlugin({
      name: "logger",
      setup({ args }) {
        console.log("Logger setup, args:", args);
      },
      cleanup() {
        console.log("Logger cleanup");
      },
    });
    
    const main = defineCommand({
      meta: { name: "hello", description: "My CLI App" },
      plugins: [logger],
      run() {
        console.log("Hello!");
      },
    });
    
    runMain(main);
  6. Install citty

    main

    You can add citty to your project using your preferred package manager. For example, using nypm:

    npx nypm add -D citty
  7. Understand the command execution lifecycle

    main

    When runCommand is called, the following lifecycle stages occur in order:

    1. Argument Parsing: rawArgs are parsed against the command's args definition.
    2. Plugin Setup: For each plugin in cmd.plugins, the plugin.setup(context) hook is called.
    3. Command Setup: The cmd.setup(context) hook is called.
    4. Subcommand Resolution:
      • If a subcommand is matched in rawArgs, runCommand is called recursively for that subcommand.
      • If no subcommand is matched but a cmd.default is defined, the default subcommand is executed.
    5. Command Run: The cmd.run(context) function is executed.
    6. Command Cleanup: The cmd.cleanup(context) hook is called.
    7. Plugin Cleanup: Plugin plugin.cleanup(context) hooks are called in reverse order of their setup.

    Note on Errors: If run fails, the error is caught and rethrown after cleanup hooks have attempted to run. If multiple errors occur during cleanup, a new error is thrown containing all cleanup errors as causes.

  8. Create a basic CLI with defineCommand and runMain

    main

    Use defineCommand to define the metadata, arguments, and lifecycle hooks of your CLI, then pass the command object to runMain to execute it.

    runMain handles usage support and graceful error handling automatically.

    import { defineCommand, runMain } from "citty";
    
    const main = defineCommand({
      meta: {
        name: "hello",
        version: "1.0.0",
        description: "My Awesome CLI App",
      },
      args: {
        name: {
          type: "positional",
          description: "Your name",
          required: true,
        },
        friendly: {
          type: "boolean",
          description: "Use friendly greeting",
        },
      },
      setup({ args }) {
        console.log(`now setup ${args.command}`);
      },
      cleanup({ args }) {
        console.log(`now cleanup ${args.command}`);
      },
      run({ args }) {
        console.log(`${args.friendly ? "Hi" : "Greetings"} ${args.name}!`);
      },
    });
    
    runMain(main);
  9. Reference: Argument Options

    main

    The following options can be used within an argument definition:

    OptionDescription
    descriptionHelp text shown in usage output
    requiredWhether the argument is required
    defaultDefault value when not provided
    aliasShort aliases (e.g., ["f"]). Not for positional
    valueHintDisplay hint in help (e.g., "host" renders --name=<host>)
  10. Reference: Public API

    main

    The following functions are exported by citty:

    FunctionDescription
    defineCommand(def)Type helper for defining commands
    runMain(cmd, opts?)Run a command with usage support and graceful error handling
    createMain(cmd)Create a wrapper that calls runMain when invoked
    runCommand(cmd, opts)Parse args and run command/sub-commands; access result from return value
    parseArgs(rawArgs, argsDef)Parse input arguments and apply defaults
    renderUsage(cmd, parent?)Render command usage to a string
    showUsage(cmd, parent?)Render usage and print to console
    defineCittyPlugin(def)Type helper for defining plugins
  11. Reference: Argument Types

    main

    When defining args in defineCommand, you can specify the following types:

    TypeDescriptionExample
    positionalUnnamed positional argscli <name>
    stringNamed string options--name value
    booleanBoolean flags, supports --no- negation--verbose
    enumConstrained to options array--level=info|warn|error
  12. Handle argument parsing errors

    main

    When parseArgs encounters validation failures, it throws a CLIError. You can catch this error to handle missing or invalid inputs gracefully.

    Common error scenarios:

    • Missing required argument: Throws CLIError with code EARG and message Missing required argument: --<name>.
    • Missing required positional argument: Throws CLIError with code EARG and message Missing required positional argument: <NAME>.
    • Invalid enum value: Throws CLIError with code EARG and a message specifying the expected options.

    Error Code: EARG