citty-universal
v0.2.2
Published
Elegant CLI Builder — citty with a runtime-independent parseArgs, so it works in any JavaScript runtime (not just Node.js).
Readme
🌆 citty-universal
Elegant CLI Builder.
citty-universal is a fork of citty with a runtime-independent implementation of parseArgs. Upstream citty relies on node:util.parseArgs, which limits it to Node.js. This fork bundles the @pkgjs/parseargs polyfill from source instead, so it works in any JavaScript runtime — Bun, Deno, Cloudflare Workers, browsers, edge runtimes, etc.
- Runs in any JavaScript runtime — no dependency on
node:util - Zero runtime dependencies, fast and lightweight
- Smart value parsing with typecast and boolean shortcuts
- Nested sub-commands with lazy and async loading
- Pluggable and composable API with auto generated usage
Usage
npx nypm add -D citty-universalimport { defineCommand, runMain } from "citty-universal";
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);node index.mjs john
# Greetings john!Sub Commands
Sub commands can be nested recursively. Use lazy imports for large CLIs to avoid loading all commands at once.
import { defineCommand, runMain } from "citty-universal";
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);Subcommands support meta.alias (e.g., ["i", "add"]) and meta.hidden: true to hide from help output.
Lazy Commands
For large CLIs, lazy load sub commands so only the executed command is imported:
const main = defineCommand({
meta: { name: "hello", version: "1.0.0", description: "My Awesome CLI App" },
subCommands: {
sub: () => import("./sub.mjs").then((m) => m.default),
},
});meta, args, and subCommands all accept Resolvable<T> values — a value, Promise, function, or async function — enabling lazy and dynamic resolution.
Hooks
Commands support setup and cleanup functions called before and after run(). Only the executed command's hooks run. cleanup always runs, even if run() throws.
const main = defineCommand({
meta: { name: "hello", version: "1.0.0", description: "My Awesome CLI App" },
setup() {
console.log("Setting up...");
},
cleanup() {
console.log("Cleaning up...");
},
run() {
console.log("Hello World!");
},
});Plugins
Plugins extend commands with reusable setup and cleanup hooks:
import { defineCommand, defineCittyPlugin, runMain } from "citty-universal";
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);Plugin setup hooks run before the command's setup (in order), cleanup hooks run after (in reverse). Plugins can be async or factory functions.
Arguments
Argument Types
| Type | Description | Example |
| ------------ | ---------------------------------------- | --------------------------- |
| positional | Unnamed positional args | cli <name> |
| string | Named string options | --name value |
| boolean | Boolean flags, supports --no- negation | --verbose |
| enum | Constrained to options array | --level=info\|warn\|error |
Common Options
| Option | Description |
| ------------- | ------------------------------------------------------------- |
| description | Help text shown in usage output |
| required | Whether the argument is required |
| default | Default value when not provided |
| alias | Short aliases (e.g., ["f"]). Not for positional |
| valueHint | Display hint in help (e.g., "host" renders --name=<host>) |
Example
const main = defineCommand({
args: {
name: { type: "positional", description: "Your name", required: true },
friendly: { type: "boolean", description: "Use friendly greeting", alias: ["f"] },
greeting: { type: "string", description: "Custom greeting", default: "Hello" },
level: {
type: "enum",
description: "Log level",
options: ["debug", "info", "warn", "error"],
default: "info",
},
},
run({ args }) {
console.log(`${args.greeting} ${args.name}! (level: ${args.level})`);
},
});Boolean Negation
Boolean args support --no- prefix. The negative variant appears in help when default: true or negativeDescription is set.
Case-Agnostic Access
Kebab-case args can be accessed as camelCase: args["output-dir"] and args.outputDir both work.
Built-in Flags
--help / -h and --version / -v are handled automatically. Disabled if your command defines args with the same names or aliases.
API
| Function | Description |
| ----------------------------- | -------------------------------------------------------------------------- |
| 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 |
Development
- Clone this repository
- Install latest LTS version of Node.js
- Enable Corepack using
corepack enable - Install Bun
- Install dependencies using
bun install - Run interactive tests using
bun run dev
License
Made with 💛 Published under MIT License.
