Cliffy Documentation

repository·main·Indexed 22 days ago

https://github.com/c4spar/cliffy

A modular TypeScript toolkit for building professional command-line interfaces, compatible with Deno, Node, and Bun. Cliffy provides specialized packages for command parsing, interactive prompts, terminal styling, and table creation. It features a chainable Command class for defining type-safe CLI tools with support for subcommands, custom type registration, shell completions, and a comprehensive error hierarchy for validation and configuration.

Tokens
14K
Snippets
46
Records
74
Agent score
78%

What's inside Cliffy

  1. Overview of Cliffy toolkit

    main
    Cliffy is a TypeScript-first, runtime-agnostic command-line toolkit designed for building complex CLIs. It is compatible with Deno, Node, and Bun runtimes. The toolkit is modular, allowing you to use specific packages for different CLI requirements such as command parsing, interactive prompts, or terminal styling.
  2. How subcommands and command nesting work

    main

    Cliffy supports hierarchical command structures. You can add subcommands to a command using the .command() method. There are two primary ways to define subcommands:

    1. Inline Definition: Pass a name and a description (or a command instance) directly to .command(). The name can include argument definitions (e.g., "add <todo>").
    2. Pre-defined Command Instances: Create a standalone Command instance and register it as a subcommand. This is useful for modularity.

    When using .command(), any options or arguments defined after the call are scoped to that subcommand. If you want to return to configuring the parent command, use the .reset() method.

    Lazy Loading: You can pass a function to .command() that returns a Command or a Promise<Command>. This allows for lazy-loading subcommands, which can improve startup performance for large CLI tools.

    import { Command } from "./mod.ts";
    
    // Define a standalone subcommand
    export const addCommand = new Command<{ verbose?: boolean }>()
      .description("Add todo.")
      .arguments("<todo>")
      .action(({ verbose }, todo: string) => {
        if (verbose) {
          console.log("Add todo '%s'.", todo);
        }
      });
    
    // Register it to the main CLI
    export const cli = new Command()
      .name("todo")
      .description("Todo cli.")
      .globalOption("--verbose", "Enable verbose output.")
      .command("add", addCommand);
    
    if (import.meta.main) {
      await cli.parse();
    }
  3. Create a simple CLI with Command

    main

    Use the Command class to build a basic CLI application. You can define the command's name, description, and version, and then provide an .action() callback that executes when the command is run. Finally, call .parse() with the process arguments (e.g., Deno.args) to execute the command.

    import { Command } from "@cliffy/command";
    
    await new Command()
      .name("hello-world")
      .description("A simple Hello World CLI.")
      .version("v1.0.0")
      .action(() => {
        console.log("Hello, World!");
      })
      .parse(Deno.args);
  4. Build a CLI application with the Command class

    main

    The Command class is a chainable factory used to create both main entrypoint commands and subcommands. You can define the command name, description, global options, environment variables, and subcommands in a single fluent chain.

    Key lifecycle steps:

    1. Instantiate new Command().
    2. Configure the command using methods like .name(), .description(), .globalOption(), etc.
    3. Define subcommands using .command().
    4. Define the logic for the command using .action().
    5. Execute the parser using .parse().

    Note: Options and arguments belonging to the main command should be registered before the first subcommand is registered. Any configuration applied after a .command() call applies to that specific child command.

    import { Command } from "./mod.ts";
    
    export const cli = new Command()
      .name("todo")
      .description("Todo cli.")
      .globalOption("--verbose", "Enable verbose output.")
      .globalEnv("VERBOSE=<value>", "Enable verbose output.")
      .command("add <todo>", "Add todo.")
      .action(({ verbose }, todo: string) => {
        if (verbose) {
          console.log("Add todo '%s'.", todo);
        }
      })
      .command("delete <id>", "Delete todo.")
      .action(({ verbose }, id: string) => {
        if (verbose) {
          console.log("Delete todo with id '%s'.", id);
        }
      });
    
    if (import.meta.main) {
      await cli.parse();
    }
  5. Configure LoggerOptions

    main

    When calling createLogger, you can provide a LoggerOptions object to control output behavior:

    • spinner: An optional Spinner instance. If provided, the logger will call .stop() on the spinner before printing a message and .start() immediately after to maintain the UI state.
    • verbose: A boolean. When true, messages sent to logger.log() will be printed to the console. When false (default), logger.log() calls are ignored.
    export interface LoggerOptions {
      spinner?: Spinner;
      verbose?: boolean;
    }
  6. Configure SpinnerOptions

    main

    When creating a new Spinner instance, you can pass a SpinnerOptions object to customize its behavior:

    • spinner (string[]): The sequence of characters used for the animation. Defaults to ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"].
    • message (string): The text displayed next to the spinner. This can be updated via spinner.message = "new message" while active.
    • interval (number): The time in milliseconds between animation frames. Defaults to 75.
    • color (Color): The color of the spinner. Defaults to the terminal default.
    const spinner = new Spinner({
      spinner: ["|", "/", "-", "\\"],
      message: "Processing...",
      interval: 100,
      color: "cyan"
    });
  7. Configure option environment variable linking

    main

    You can link a command option to an environment variable using the env property in GlobalOptionOptions. This allows the option to be populated from the environment with the precedence: flag > env var > default.

    Supported env configurations:

    • true: Derives the name from the option name (e.g., --install-root becomes INSTALL_ROOT).
    • string: Sets the environment variable name explicitly (e.g., env: 'DENO_INSTALL_ROOT').
    • { prefix: string }: Prepends a prefix to the derived name (e.g., { prefix: 'DENO_' } makes --install-root look for DENO_INSTALL_ROOT).
  8. Configure the output environment variable for binary upgrades

    main

    The --output flag (used for installing upgraded binaries to a specific path) can be controlled via an environment variable. This is configured using the outputEnv option in the UpgradeCommand constructor.

    Supported values for outputEnv:

    • true: Uses the environment variable OUTPUT.
    • string: Uses the exact name provided (e.g., "MY_CUSTOM_PATH").
    • { prefix: string }: Prepends a prefix to OUTPUT (e.g., { prefix: "MYCLI_" } results in MYCLI_OUTPUT).
  9. Cliffy available packages and runtimes

    main

    Cliffy is split into several specialized modules. Most modules support Deno, Node, and Bun, while some are specific to Deno.

    PackageDescriptionSupported Runtimes
    ansiChainable ansi escape sequencesDeno, Node, Bun
    commandCreate complex, type-safe CLI tools with validation, help, and shell completionsDeno, Node, Bun
    flagsParse command line arguments (used by command)Deno, Node, Bun
    keycodeParser ansi key codesDeno, Node, Bun
    keypressListen to keypress events (Promise, AsyncIterator, EventTarget)Deno, Node, Bun
    promptCreate simple and powerful interactive promptsDeno, Node, Bun
    tableCreate CLI tables with borders, padding, and nestingDeno, Node, Bun
    testingExperimental helper functions for testingDeno
  10. Migrate GitHub provider imports for version 2.0

    main

    The current way of importing GithubProvider and its related types is deprecated and will be removed in version 2.0. To ensure compatibility with future versions, update your imports to pull directly from @cliffy/upgrade/provider/github instead of the current entry point.

    Deprecated symbols to migrate:

    • GithubProvider (class/type)
    • GithubProviderOptions (type)
    • GithubVersions (type)
    • GithubTokenResolver (type)
    • GithubAssetResolver (type)
    // DEPRECATED: Avoid this pattern
    import { GithubProvider } from "@cliffy/upgrade/provider/github"; 
    // Note: The file content indicates this specific export path is being redirected.
    
    // RECOMMENDED: Import directly from the source module
    import { 
      GithubProvider, 
      type GithubProviderOptions, 
      type GithubVersions, 
      type GithubTokenResolver, 
      type GithubAssetResolver 
    } from "@cliffy/upgrade/provider/github";
  11. Fix Missing Command Name for Shell Completions

    main

    If you encounter a MissingCommandNameCompletionsError while trying to generate shell completions, it means the CLI does not know its own name. You can fix this in two ways:

    1. In code: Use cmd.name("<command-name>") to set the name of the main command.
    2. Via CLI: Use the --name option from the completions command:
      <command> completions <shell> --name <cli-name>
  12. Resolve Duplicate Option Name errors

    main

    If you encounter a DuplicateOptionNameError, it means you are trying to register an option with a name that has already been defined on that command.

    To resolve this, you can explicitly allow the new option to override the existing one by setting the override option to true when calling the .option() method.