Cliffy Documentation
repository·main·Indexed 22 days ago
https://github.com/c4spar/cliffyA 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.
What's inside Cliffy
- 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.
How subcommands and command nesting work
mainCliffy supports hierarchical command structures. You can add subcommands to a command using the
.command()method. There are two primary ways to define subcommands:- 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>"). - Pre-defined Command Instances: Create a standalone
Commandinstance 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 aCommandor aPromise<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(); }- Inline Definition: Pass a name and a description (or a command instance) directly to
Create a simple CLI with Command
mainUse the
Commandclass 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);Build a CLI application with the Command class
mainThe
Commandclass 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:
- Instantiate
new Command(). - Configure the command using methods like
.name(),.description(),.globalOption(), etc. - Define subcommands using
.command(). - Define the logic for the command using
.action(). - 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(); }- Instantiate
Configure LoggerOptions
mainWhen calling
createLogger, you can provide aLoggerOptionsobject to control output behavior:spinner: An optionalSpinnerinstance. 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. Whentrue, messages sent tologger.log()will be printed to the console. Whenfalse(default),logger.log()calls are ignored.
export interface LoggerOptions { spinner?: Spinner; verbose?: boolean; }Configure SpinnerOptions
mainWhen creating a new
Spinnerinstance, you can pass aSpinnerOptionsobject 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 viaspinner.message = "new message"while active.interval(number): The time in milliseconds between animation frames. Defaults to75.color(Color): The color of the spinner. Defaults to the terminal default.
const spinner = new Spinner({ spinner: ["|", "/", "-", "\\"], message: "Processing...", interval: 100, color: "cyan" });Configure option environment variable linking
mainYou can link a command option to an environment variable using the
envproperty inGlobalOptionOptions. This allows the option to be populated from the environment with the precedence:flag > env var > default.Supported
envconfigurations:true: Derives the name from the option name (e.g.,--install-rootbecomesINSTALL_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-rootlook forDENO_INSTALL_ROOT).
Configure the output environment variable for binary upgrades
mainThe
--outputflag (used for installing upgraded binaries to a specific path) can be controlled via an environment variable. This is configured using theoutputEnvoption in theUpgradeCommandconstructor.Supported values for
outputEnv:true: Uses the environment variableOUTPUT.string: Uses the exact name provided (e.g.,"MY_CUSTOM_PATH").{ prefix: string }: Prepends a prefix toOUTPUT(e.g.,{ prefix: "MYCLI_" }results inMYCLI_OUTPUT).
Cliffy available packages and runtimes
mainCliffy is split into several specialized modules. Most modules support Deno, Node, and Bun, while some are specific to Deno.
Package Description Supported Runtimes ansiChainable ansi escape sequences Deno, Node, Bun commandCreate complex, type-safe CLI tools with validation, help, and shell completions Deno, Node, Bun flagsParse command line arguments (used by command)Deno, Node, Bun keycodeParser ansi key codes Deno, Node, Bun keypressListen to keypress events (Promise, AsyncIterator, EventTarget) Deno, Node, Bun promptCreate simple and powerful interactive prompts Deno, Node, Bun tableCreate CLI tables with borders, padding, and nesting Deno, Node, Bun testingExperimental helper functions for testing Deno Migrate GitHub provider imports for version 2.0
mainThe current way of importing
GithubProviderand 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/githubinstead 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";Fix Missing Command Name for Shell Completions
mainIf you encounter a
MissingCommandNameCompletionsErrorwhile trying to generate shell completions, it means the CLI does not know its own name. You can fix this in two ways:- In code: Use
cmd.name("<command-name>")to set the name of the main command. - Via CLI: Use the
--nameoption from thecompletionscommand:<command> completions <shell> --name <cli-name>
- In code: Use
Resolve Duplicate Option Name errors
mainIf 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
overrideoption totruewhen calling the.option()method.