--- url: https://jinju.js.org/guide/getting-started.md description: >- Install Jinju and build a first command-line app with typed flags, args and commands. --- # Getting started Jinju is a toolkit for building Node command-line apps in TypeScript. You define flags, args and commands as separate values, compose them into a program, and get back fully typed input in each command's `run` handler. Help pages, error messages and exit codes are handled for you. ## Requirements * Node `>=22.12` * ESM projects work as usual. CommonJS projects can `require('jinju')`, which loads the ESM build. ## Install ::: code-group ```sh [npm] npm install jinju ``` ```sh [pnpm] pnpm add jinju ``` ```sh [yarn] yarn add jinju ``` ::: ## Your first CLI Create `cli.ts`: ```ts import { arg, command, flag, jinju, packageVersion } from 'jinju'; const greet = command({ description: 'Greet someone', args: [arg.string({ name: 'name', description: 'Who to greet' }).required()], flags: { shout: flag.boolean({ alias: 's', description: 'Shout the greeting' }), times: flag.number({ description: 'How many times', min: 1, integer: true }).default(1), }, run: ({ args: [name], flags }) => { // name: string const text = `Hello, ${name}!`; for (let i = 0; i < flags.times; i++) console.log(flags.shout ? text.toUpperCase() : text); }, }); const app = jinju({ name: 'greet', version: packageVersion(import.meta.url), commands: { $: greet }, }); await app.run(); ``` `$` is the command for a level of the tree, so here `greet` runs without a subcommand name. Hover over `name` and `flags` above: their types come from the definitions, so `name` is a `string` because the arg is `.required()`, and `times` is a `number` because it has a default. Run it: ```sh $ npx tsx cli.ts World --shout HELLO, WORLD! $ npx tsx cli.ts --help Greet someone Usage: greet [flags] Arguments: Who to greet (required) Flags: -h, --help Show help -s, --shout Shout the greeting --times How many times (default: 1) --version Show version $ npx tsx cli.ts error: Missing required argument Run 'greet --help' for usage. $ npx tsx cli.ts World --times 0 error: Invalid value '0' for --times: must be at least 1 Run 'greet --help' for usage. ``` `jinju()` creates the [program](./program), and `app.run()` runs it. `run()` reads `process.argv.slice(2)` by default, sets `process.exitCode` and resolves to the exit code: `0` on success and `1` on errors. You can also pass the arguments yourself, which is useful in [tests](./testing): ```ts import { command, jinju } from 'jinju'; const greet = command({ description: 'Greet someone', run: () => {} }); const app = jinju({ name: 'greet', commands: { $: greet } }); const code = await app.run(['World', '--shout']); ``` ## Publishing a CLI To ship your CLI as a package, point `bin` in `package.json` at the built file and add a shebang to it: ```json { "name": "greet", "version": "1.0.0", "type": "module", "bin": { "greet": "./dist/cli.js" } } ``` ```ts #!/usr/bin/env node ``` `packageVersion(import.meta.url)` reads `version` from the nearest `package.json` above the file, so `--version` always matches what you published. ## Using an AI assistant The docs are also published for AI assistants and coding agents. Point yours at [`llms.txt`](/llms.txt), an index of every page, or [`llms-full.txt`](/llms-full.txt), the whole site in one file. Every page is also available as Markdown: add `.md` to its URL, or use the **Copy page** menu under its title to copy it or open it in ChatGPT or Claude. In those copies, the types shown on hover here are written into the examples as comments. ## Next steps * [Flags](./flags): every flag type, modifiers and switches * [Args](./args): positional arguments, including variadic args * [Commands](./commands) and [Program](./program): building a command tree * [Scopes](./scopes): sharing flags and args between commands --- --- url: https://jinju.js.org/guide/flags.md description: >- Named options such as --port 8080: value types, switches, modifiers (.required(), .multiple(), .default()), aliases and custom parsers. --- # Flags Flags are named options: `--port 8080`, `-v`, `--dry-run`. You create them with the `flag.*` creators and add them to a command (or [scope](./scopes)) under `flags`. The object key becomes the flag's name, converted from camelCase to kebab-case. ```ts import { command, flag } from 'jinju'; command({ description: 'Serve files', flags: { port: flag.number({ alias: 'p', description: 'Port to listen on', integer: true }), dryRun: flag.boolean({ description: "Don't write anything" }), // --dry-run }, run: ({ flags }) => {}, // flags: { // port: number | undefined; // dryRun: boolean; // } }); ``` ## Value flags These take a value, written as `--name value`, `--name=value`, `-n value` or `-nvalue`: | Creator | Type | Options | Notes | | ---------------------------- | ----------------- | -------------------------- | -------------------------------------------------- | | `flag.string(options)` | `string` | `choices?` | With `choices`, the type is the union of them | | `flag.number(options)` | `number` | `min?`, `max?`, `integer?` | Rejects non-numbers, and values outside the bounds | | `flag.url(options)` | `URL` | | Parsed with `new URL()` | | `flag.email(options)` | `string` | | Checks the value looks like an email address | | `flag.directory(options)` | `string` | `exists?` | Resolved to an absolute path | | `flag.file(options)` | `string` | `exists?` | Resolved to an absolute path | | `flag.custom(parse, name?)` | Your parser's | Your parser's options | See [custom parsers](#custom-parsers) | | `flag.schema(schema, name?)` | The schema output | | See [Standard Schema](./schemas) | `choices` narrows the type: ```ts import { flag, type InferFlagType } from 'jinju'; const mode = flag.string({ description: 'Mode', choices: ['fast', 'safe'] }); type Mode = InferFlagType; // type Mode = "fast" | "safe" | undefined ``` With `exists: true`, `flag.directory` and `flag.file` also check that the path exists and is the right kind of entry. ## Switches `flag.boolean` and `flag.count` are switches. They never take the next argument as a value. | Creator | Default | Behaviour | | ----------------------- | ------- | ------------------------------------------------------------------- | | `flag.boolean(options)` | `false` | `--x` → `true`, `--no-x` → `false`. The last occurrence wins. | | `flag.count(options)` | `0` | Counts occurrences: `-vvv` → `3`. `--no-x` resets the count to `0`. | A switch can also be set explicitly with `--x=`, using `true`/`false`, `1`/`0`, `yes`/`no` or `on`/`off`. `--no-x=` inverts the value. The built-in `--help` and `--version` follow the same rules, so `--help=false` doesn't show help. A switch always has a value, so its type is never `undefined`. To default a boolean to `true`, use `.default(true)`. Help then shows it as `--[no-]name`. ## Shared options Every flag creator takes these options: | Option | Description | | ------------- | ---------------------------------------------------------------------------------------------------------------- | | `description` | Shown in help. Required. | | `alias` | Another name, without dashes. One character gives `-x`, longer names give `--xyz`. | | `hidden` | Leave the flag out of help. It's still accepted. | | `env` | An environment variable to read when the flag isn't given. See [Environment variables](./environment-variables). | ## Modifiers Flags are immutable: each modifier returns a new flag, so one definition can be reused with different settings. | Modifier | Effect | | ---------------------- | --------------------------------------------------------------------------------- | | `.required(on = true)` | Must be provided. Clears any default. | | `.multiple(on = true)` | Repeatable, collected into an array. Converts any existing default (`v` ↔ `[v]`). | | `.default(value)` | Used when absent. Clears `required`. Must be a non-empty array when `multiple`. | Switches only have `.default(value)`. The modifiers decide the type `run` receives: | Definition | Type | | --------------------------------------- | ------------------------- | | `flag.url(o)` | `URL \| undefined` | | `flag.url(o).multiple()` | `URL[] \| undefined` | | `flag.url(o).default(url)` | `URL` | | `flag.url(o).required().multiple()` | `URL[]` | | `flag.string({choices: ['a', 'b'], …})` | `'a' \| 'b' \| undefined` | ```ts import { command, flag } from 'jinju'; const tag = flag.string({ alias: 't', description: 'Tag to apply' }); command({ description: 'Publish', flags: { tag: tag.multiple().default(['latest']), registry: flag.url({ description: 'Registry' }).required(), }, run: ({ flags }) => {}, // flags: { // tag: string[]; // registry: URL; // } }); ``` A `.multiple()` flag is repeated on the command line (`-t a -t b`). Giving a flag that isn't `.multiple()` more than once is an error. ## Custom parsers `flag.custom(parse, name?)` turns a parser into a flag creator. The parser receives the raw string and the options passed to the creator, and returns the value. Throw to reject the input: the error message is shown to the user. Parsers may be async. ```ts import { command, flag } from 'jinju'; const duration = flag.custom((input: string, { max }: { max?: number }) => { const match = /^(\d+)(ms|s|m)$/.exec(input); if (!match) throw new Error('must be a duration like 500ms, 10s or 5m'); const ms = Number(match[1]) * { ms: 1, s: 1000, m: 60_000 }[match[2] as 'ms' | 's' | 'm']; if (max !== undefined && ms > max) throw new Error(`must be at most ${max}ms`); return ms; }, 'duration'); command({ description: 'Wait', flags: { timeout: duration({ description: 'Timeout', max: 60_000 }).default(5000) }, run: ({ flags }) => {}, // flags: { // timeout: number; // } }); ``` The second argument names the value in help: `--timeout `. `arg.custom` accepts the same parsers. ## Command-line syntax * `--name value`, `--name=value`, `-n value`, `-nvalue` and `-n=value` all set a value. * Short switches can be bundled: `-abc` is `-a -b -c`, and `-vvv` counts three times. A bundle can end with a value flag: `-vp 8080`. * Negative numbers are values, not flags: `--offset -5`. * `--` ends flag parsing. Everything after it is a positional value, even if it starts with `-`. * A value flag at the end of the input, or followed by another flag, is a "requires a value" error. ## Errors Invalid input is reported with the flag's label and the parser's message, and every problem is reported at once: ```sh $ app sync . --mode slow --times 0.5 error: Invalid value 'slow' for --mode: must be one of fast, safe error: Invalid value '0.5' for --times: must be an integer Run 'app sync --help' for usage. ``` Unknown flags are errors unless the program sets `strict: false`. See [Errors](./errors) to change how errors look. --- --- url: https://jinju.js.org/guide/args.md description: >- Positional values such as src and dest in cp src dest: the args tuple, optional and required args, variadic .multiple() args and how values are allocated. --- # Args Args are positional values: the `src` and `dest` in `cp src dest`. A command's `args` is a tuple, and `run` receives a typed tuple in the same order. ```ts import { arg, command } from 'jinju'; command({ description: 'Rename a file', args: [ arg.file({ name: 'from', description: 'File to rename', exists: true }).required(), arg.string({ name: 'to', description: 'New name' }).required(), ], run: ({ args: [from, to] }) => {}, // from: string // to: string }); ``` ## Creators Args use the same creators and parsers as [flags](./flags#value-flags): `arg.string`, `arg.number`, `arg.url`, `arg.email`, `arg.directory`, `arg.file`, `arg.custom` and `arg.schema`. There are no switches. Every arg takes these options: | Option | Description | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `description` | Shown in help. Required. | | `name` | The name shown in usage and help, e.g. `'files'` → ``. Defaults to the value type (``). It also binds the arg to a [dynamic segment](./dynamic-segments). | ## Modifiers Args are optional by default. They have the same modifiers as flags, with the same effect on their types: | Definition | Type | | ----------------------------------- | ----------------------- | | `arg.string(o)` | `string \| undefined` | | `arg.string(o).required()` | `string` | | `arg.string(o).default('x')` | `string` | | `arg.file(o).multiple()` | `string[] \| undefined` | | `arg.file(o).multiple().required()` | `string[]` | ## Variadic args At most one arg can be `.multiple()`, in any position. It takes every value the other args don't: * args before it fill from the left * args after it fill from the right * the `.multiple()` arg takes the middle So `[sources.multiple().required(), dest.required()]` behaves like `cp`: ```ts import { arg, command } from 'jinju'; const copy = command({ description: 'Copy files into a directory', args: [ arg.file({ name: 'source', description: 'Files to copy' }).multiple().required(), arg.directory({ name: 'dest', description: 'Destination' }).required(), ], run: ({ args: [sources, dest] }) => {}, // sources: string[] // dest: string }); // app a.txt b.txt out/ → sources: ['…/a.txt', '…/b.txt'], dest: '…/out' ``` When there are too few values, required args are filled first, then optional args from left to right. Values left over when there's no `.multiple()` arg are "Unexpected argument" errors. Declaring two `.multiple()` args is a type error, and throws a `ConfigError` at runtime: ```ts import { arg, command } from 'jinju'; command({ description: 'Broken', args: [arg.string({ description: 'a' }).multiple(), arg.string({ description: 'b' }).multiple()], // error TS2322: Type '[Arg, Arg]' is not assignable to type 'readonly [Arg, Arg] & { '~error': "Only one arg can be .multiple()"; }'. // Property ''~error'' is missing in type '[Arg, Arg]' but required in type '{ '~error': "Only one arg can be .multiple()"; }'. run: () => {}, }); ``` ## Help Usage lines show optional args in brackets and `.multiple()` args with `...`: ```sh Usage: app copy [flags] ... ``` --- --- url: https://jinju.js.org/guide/commands.md description: >- Commands bundle flags, args and a run handler; how they are named, what run receives, aliases and exit codes. --- # Commands A command bundles flags, args and a `run` handler. Commands don't have names of their own: a command is named by its key when you add it to the [program](./program)'s command tree. ```ts import { arg, command, flag } from 'jinju'; const deploy = command({ description: 'Deploy the app', aliases: ['ship'], args: [arg.string({ name: 'env', description: 'Environment', choices: ['staging', 'prod'] })], flags: { force: flag.boolean({ alias: 'f', description: 'Skip checks' }) }, run: async ({ args: [env], flags, argv }) => { // env: "staging" | "prod" | undefined if (env === 'prod' && !flags.force) return 2; // exit code }, }); ``` ## Config `command({ name?, description, aliases?, hidden?, flags?, args?, run })` | Option | Description | | ------------- | --------------------------------------------------------------- | | `description` | Shown in help. Required. | | `name` | Overrides the name taken from the command's key. A single word. | | `aliases` | Other names that route to this command. | | `hidden` | Leave the command out of help listings. It can still be run. | | `flags` | An object of [flags](./flags). Keys become kebab-case names. | | `args` | A tuple of [args](./args). | | `run` | The handler. Receives `{ flags, args, argv }`. | ## The `run` handler `run` receives a context with: * `flags`: an object of parsed flag values, typed from the definitions * `args`: a tuple of parsed arg values, in order * `argv`: the raw argv the command parsed, with the command path removed `run` may be async. Its return value sets the exit code: * returning nothing (or resolving to nothing) exits with `0` * returning a number exits with that number * throwing a [`JinjuError`](./errors#throwing-from-run) reports it like an input error, with exit code `1` * any other error is rethrown from `app.run()` `run` is only called when all input is valid. All parsing and validation errors are collected first and reported together. ## Names A command's name comes from its key in the tree, converted from camelCase to kebab-case: `{ dryRun }` routes `app dry-run`. Set `name` to choose a different one, and `aliases` to accept more: ```ts import { command, jinju } from 'jinju'; const remove = command({ name: 'rm', aliases: ['del'], description: 'Remove', run: () => {} }); jinju({ commands: { remove } }); // app rm, app del ``` Two commands with the same name or alias at the same level are a `ConfigError`. Commands can't define `help` or `version` flags, which are built in. ## Sharing flags and args To give several commands the same flags or leading args, create them from a [scope](./scopes). `command()` is the same as creating a command from an empty scope. --- --- url: https://jinju.js.org/guide/program.md description: >- jinju() builds a program from a command tree: its options, nested commands, running it with argv and rendering help. --- # Program `jinju()` builds a program from a tree of commands. The program's methods then [run it](#running) or [render its help](#rendering-help). Keep defining the program separate from running it. The definition is plain configuration, and the program can be run as many times as you like, with any input. ```ts import { command, jinju, packageVersion } from 'jinju'; const init = command({ description: 'Create a project', run: () => {} }); const build = command({ description: 'Build the project', run: () => {} }); const watch = command({ description: 'Rebuild on changes', run: () => {} }); const app = jinju({ name: 'app', version: packageVersion(import.meta.url), description: 'My app', commands: { init, build: { $: build, watch }, // app build, app build watch }, }); await app.run(); ``` ## Config `jinju({ name?, version?, description?, commands, strict?, help?, error?, env?, stdout?, stderr?, exit? })` | Option | Default | Description | | ------------- | ----------------------- | ---------------------------------------------------------------- | | `commands` | | The command tree. Required. | | `name` | The script's file name | Shown in usage and error hints. | | `version` | | Printed by `--version`. Without it, there's no `--version` flag. | | `description` | | Shown at the top of the root help page. | | `strict` | `true` | Reject unknown flags. With `false`, they're ignored. | | `help` | `new HelpTemplate()` | Renders help pages. See [Help](./help). | | `error` | `new ErrorTemplate()` | Renders errors. See [Errors](./errors). | | `env` | `process.env` | Where flags' `env` variables are read from. | | `stdout` | `process.stdout.write` | Receives help and version text. | | `stderr` | `process.stderr.write` | Receives error output. | | `exit` | Sets `process.exitCode` | Receives the exit code. | See [Testing](./testing) for how `env`, `stdout`, `stderr` and `exit` help in tests. ## The command tree * Each key is a command name. Its value is a command, or an object of subcommands. * `$` is the command for that level itself. In `{ build: { $: build, watch } }`, `app build` runs `build` and `app build watch` runs `watch`. * A level without `$` is a group: running it shows its help, which lists its subcommands. * A `:name` key is a [dynamic segment](./dynamic-segments), which reads a value before the command name. * `{ $: command }` at the root makes a single-command CLI. Mistakes such as duplicate names, `$` holding a subtree or invalid names throw a `ConfigError` when `jinju()` is called. Mistakes in dynamic segments are also [compile-time errors](./dynamic-segments#compile-time-checks). ## The program `jinju()` returns a [`Program`](../api/program#program): | Member | Description | | ------------- | --------------------------------------------------------------------------- | | `run(argv?)` | Parses argv, routes it to a command and runs it. Resolves to the exit code. | | `help(path?)` | Returns the help text for a command path, without printing it. | | `name` | The program's name: `name` from the config, or the script's file name. | ### Running `run()` reads `process.argv.slice(2)` by default: ```ts import { command, jinju } from 'jinju'; const build = command({ description: 'Build the project', run: () => {} }); const app = jinju({ name: 'app', commands: { build } }); await app.run(); // node app.js build --watch → ['build', '--watch'] ``` Pass argv yourself to run the program with other input, such as in [tests](./testing), when embedding a CLI in another tool, or when your arguments come from somewhere other than `process.argv`: ```ts import { command, jinju } from 'jinju'; const build = command({ description: 'Build the project', run: () => {} }); const app = jinju({ name: 'app', commands: { build } }); const code = await app.run(['build', '--help']); // const code: number ``` `run()` resolves to the [exit code](#exit-codes) and also passes it to `exit`. It rejects only when a `run` handler throws an error that isn't a `JinjuError`. ### Rendering help `help(path?)` returns the help text for a command path, e.g. `app.help(['build', 'watch'])`, or the root page with no path. This is useful to generate docs or snapshot help in tests. It throws an `UnknownCommandError` for an unknown name. ## Routing `run()` takes the longest command path that matches argv. Static names win over dynamic segments. Flags can come before the command name when a command further down the tree inherits them from a [scope](./scopes): `app -v build`. A flag that no command below that point inherits, or `--`, ends the command path. ## Built-in flags * `--help`/`-h` prints the help page for the matched level, and exits with `0`. * `--version` prints `version`, and exits with `0`. It only exists when `version` is set. Running a group (a level without `$`) prints its help. It exits with `0` when there was no input at all (just `app`), and with `1` otherwise (e.g. `app build` when `build` is a group). An unknown command name there is an error. ## Exit codes `app.run()` resolves to the exit code and passes it to `exit`, which by default sets `process.exitCode`, so Node exits with it once pending work is done. | Code | When | | ---------- | -------------------------------------------------- | | `0` | Success, `--help` or `--version` | | `1` | Invalid input, or a `JinjuError` thrown from `run` | | any number | Returned from `run` | ## Reading the version `packageVersion(from)` reads `version` from the nearest `package.json` at or above `from`, usually your CLI's `import.meta.url`. It throws a `ConfigError` if there's no `package.json` or it has no `version`. --- --- url: https://jinju.js.org/guide/scopes.md description: >- Share flags and args across several commands with scope(), with their types flowing through to each command. --- # Scopes A scope groups the flags and args that several commands share. Commands created from a scope inherit them, and their types flow through to each command's `run`. ```ts import { arg, flag, scope } from 'jinju'; const app = scope({ flags: { verbose: flag.count({ alias: 'v', description: 'Increase verbosity' }) }, }); const project = app.scope({ args: [arg.string({ name: 'project', description: 'Project' }).required()], }); const deploy = project.command({ description: 'Deploy', args: [arg.string({ name: 'env', description: 'Environment' })], run: ({ args: [project, env], flags: { verbose } }) => {}, // project: string // env: string | undefined // verbose: number }); // app -v deploy my-site staging ``` ## API * `scope({ flags?, args? })` creates a scope. * `scope.command(config)` creates a command that inherits the scope's flags and args. It takes the same config as [`command()`](./commands#config). * `scope.scope(config)` creates a nested scope that adds more flags and args. `command()` is the same as a command from an empty scope. ## Flags A scope's flags merge with each command's own. If a command defines a flag with the same key, its own definition replaces the inherited one: ```ts import { flag, scope } from 'jinju'; const app = scope({ flags: { output: flag.string({ description: 'Output file' }) } }); app.command({ description: 'Export', flags: { output: flag.string({ description: 'Output file' }).default('out.json') }, run: ({ flags: { output } }) => {}, // output: string }); ``` Inherited flags are also accepted **before the command name**: `app -v deploy …` works when `deploy` inherits `verbose`. If the command that matches doesn't declare a flag given before its name, that's an "Unknown flag" error. A flag name that takes a value in one scope but is a switch in another must come after the command name, because Jinju can't know whether it takes the next word. ## Args Scope args come first: `run` receives `[...scopeArgs, ...commandArgs]`, read after the command name. The [one `.multiple()` rule](./args#variadic-args) applies across the whole tuple, including inherited args. Scope args are also how [dynamic segments](./dynamic-segments) work: a segment's value fills the scope's arg of the same name. ## Help Help lists every flag a command accepts (its own, inherited and built-in) in one table. Custom [help templates](./help) can tell them apart with `HelpFlag.source`, which is `'command'`, `'scope'` or `'builtin'`. --- --- url: https://jinju.js.org/guide/dynamic-segments.md description: >- Read a value before the command name, like a route parameter: app my-site deploy, with :name keys in the command tree. --- # Dynamic segments A `:name` key in the command tree reads a value **before** the command name, like a route parameter. `app my-site deploy` can run `deploy` for the project `my-site`. The value goes into the commands' arg called `name`, which comes from a [scope](./scopes): ```ts import { arg, command, jinju, scope } from 'jinju'; const list = command({ description: 'List projects', run: () => {} }); const project = scope({ args: [arg.string({ name: 'project', description: 'Project' }).required()], }); const deploy = project.command({ description: 'Deploy', args: [arg.string({ name: 'env', description: 'Environment' })], run: ({ args: [project, env] }) => {}, // project: string // env: string | undefined }); jinju({ commands: { list, ':project': { deploy } } }); // app my-site deploy staging ``` ## How it works * Segment values are ordinary args. They're parsed and validated by the arg, and come first in `args`. * Static names win over a segment: `app list` runs `list`, not a project named `list`. * Segments nest: `{ ':project': { ':env': { logs } } }` fills the leading args in order, so `logs` must start with `project` and then `env` args. * Help shows segments in the path, e.g. `app deploy []`. The root help lists the segment with its arg's description. * Error hints use the values that were typed: `Run 'app my-site deploy --help' for usage.` ## Rules * A level can have one segment. * The segment's arg can't be `.multiple()`. * Each command under a segment must have the matching named arg at the right position. * A level's `$` command can't take positional args of its own next to a segment, because a word there could be either. ## Compile-time checks Mistakes in the tree are type errors on `commands`, worded like the `ConfigError` that `jinju()` throws for them at runtime: ```ts import { arg, command, jinju } from 'jinju'; const deploy = command({ description: 'Deploy', args: [arg.string({ name: 'env', description: 'Environment' })], run: () => {}, }); jinju({ commands: { ':project': { deploy } } }); // error TS2322: Type '{ ':project': { deploy: Command, [Arg]>; }; }' is not assignable to type '{ readonly ':project': { readonly deploy: Command, [Arg]>; }; } & { '~error': "Command ' deploy' must have an arg named 'project' at position 1 for segment ':project'"; }'. // Property ''~error'' is missing in type '{ ':project': { deploy: Command, [Arg]>; }; }' but required in type '{ '~error': "Command ' deploy' must have an arg named 'project' at position 1 for segment ':project'"; }'. ``` The checks cover: * `$` not being a command: `'$' must be a command (at 'sync')` * a level with more than one segment * a level's `$` command taking its own positional args alongside a segment * a command under a segment without the matching named arg in the right position, e.g. `Command ' deploy' must have an arg named 'project' at position 1 for segment ':project'` * a segment's arg being `.multiple()` The arg checks need literal names, as in the examples. Args named with a plain `string` variable are only checked at runtime. --- --- url: https://jinju.js.org/guide/schemas.md description: >- Use any Standard Schema (zod, valibot, arktype) to parse and validate flag and arg values. --- # Standard Schema Any [Standard Schema](https://standardschema.dev) can be used as a parser: zod, valibot, arktype and others. `flag.schema(schema, name?)` and `arg.schema(schema, name?)` return a creator, like [`custom`](./flags#custom-parsers): ```ts import { command, flag } from 'jinju'; import { z } from 'zod'; const port = flag.schema(z.coerce.number().int().min(1), 'port'); const tags = flag.schema(z.enum(['alpha', 'beta'])); command({ description: 'Serve', flags: { port: port({ description: 'Port' }).default(80), tag: tags({ description: 'Tag' }).multiple(), }, run: ({ flags }) => {}, // flags: { // port: number; // tag: ("alpha" | "beta")[] | undefined; // } }); ``` The optional second argument names the value in help: `--port `. It defaults to `value`. ## Types The value's type comes from the schema's output, and the modifiers apply as usual. ## Values arrive as strings Command-line values and environment variables are always strings, so schemas for other types must coerce, e.g. `z.coerce.number()` or `z.stringbool()`. A schema whose input can't be a string, such as `z.number()`, `z.boolean()` or `z.date()`, is a type error: ```ts import { flag } from 'jinju'; import { z } from 'zod'; flag.schema(z.number()); // error TS2345: Argument of type 'ZodNumber' is not assignable to parameter of type 'ZodNumber & { '~error': "Schema input must accept a string (command-line values are strings); use a coercing schema"; }'. // Property ''~error'' is missing in type '_ZodNumber<$ZodNumberInternals>' but required in type '{ '~error': "Schema input must accept a string (command-line values are strings); use a coercing schema"; }'. ``` Coercing schemas (input `unknown`), enums and string schemas are accepted, whether written inline or declared beforehand. ## No `undefined` outputs `undefined` already means "not provided", so a schema that can output `undefined` (such as `.optional()`) is a type error. Use `.default()` on the flag or arg instead. ## Errors Validation errors use the schema's issue messages, joined with `; `: ```sh error: Invalid value '0' for --port: Too small: expected number to be >=1 ``` ## No dependency Jinju doesn't depend on any schema library or on `@standard-schema/spec`. It ships a compatible `StandardSchemaV1` type, so any schema that follows the spec works. --- --- url: https://jinju.js.org/guide/environment-variables.md description: >- Let any flag fall back to an environment variable with the env option, and supply variables yourself in tests. --- # Environment variables Any flag can fall back to an environment variable. Set `env` to the variable's name: ```ts import { command, flag } from 'jinju'; command({ description: 'Deploy', flags: { token: flag.string({ description: 'API token', env: 'API_TOKEN' }).required(), port: flag.number({ description: 'Port', env: 'PORT' }).default(8080), verbose: flag.count({ alias: 'v', description: 'Verbosity', env: 'VERBOSE' }), }, run: ({ flags }) => {}, }); ``` ## Priority The command line wins, then the variable, then the default. A value from the variable satisfies `.required()`. ## Parsing Values from the environment are parsed and validated like command-line values, so `flag.number({ env: 'PORT' })` checks that `PORT` is a number. Errors name where the value came from: ```sh error: Invalid value 'abc' for --port (from $PORT): must be a number ``` * `flag.boolean` accepts `true`/`false`, `1`/`0`, `yes`/`no` and `on`/`off`, in any case. * `flag.count` accepts a whole number: `VERBOSE=2`. * Empty values (`API_TOKEN=`) count as unset. * A `.multiple()` flag gets a single value from its variable. ## Help Help shows the variable after the description: ```sh Flags: --token API token (required; env: API_TOKEN) ``` ## Where variables come from Variables are read from `process.env` by default. Pass `env` to `jinju()` to supply them yourself, for example in [tests](./testing): ```ts import { command, jinju } from 'jinju'; const deploy = command({ description: 'Deploy', run: () => {} }); const app = jinju({ commands: { deploy }, env: { API_TOKEN: 'test' } }); await app.run(['deploy']); ``` --- --- url: https://jinju.js.org/guide/help.md description: >- The --help pages generated from your definitions, and customising them by extending HelpTemplate. --- # Help `--help` pages are generated from your definitions: ```sh $ app sync --help Sync files Usage: app sync [flags] app sync Commands: repeat Sync files repeatedly Arguments: Path to the files (required) Flags: -h, --help Show help --mode Sync mode (choices: fast, safe; default: safe) -r, --repeat Repeat the operation -v, --verbose Increase verbosity (env: VERBOSE) --version Show version ``` Pages are rendered by a `HelpTemplate`. To customise them, pass an instance as `help` to `jinju()`, either with options or by extending the class. ## Options ```ts import { HelpTemplate, jinju, type CommandTree } from 'jinju'; declare const commands: CommandTree; jinju({ commands, help: new HelpTemplate({ sortFlags: false, showHidden: true }) }); ``` | Option | Default | Description | | ------------ | ------- | ------------------------------------------------------------------ | | `sortFlags` | `true` | Sort flags by long name. With `false`, they keep definition order. | | `showHidden` | `false` | Include hidden commands and flags. | ## Extending the template Override any method to change part of the page, and call `super` to build on the default: ```ts import { type CommandTree, type HelpData, type HelpFlag, HelpTemplate, jinju } from 'jinju'; declare const commands: CommandTree; class MyHelp extends HelpTemplate { override formatFlag(flag: HelpFlag) { const [label, description] = super.formatFlag(flag); return [label, flag.source === 'builtin' ? `${description} (built-in)` : description] as const; } override sections(data: HelpData) { return [`${data.program.name} v${data.program.version}`, ...super.sections(data)]; } } jinju({ version: '1.0.0', help: new MyHelp({ sortFlags: false }), commands }); ``` | Method | Default | | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- | | `render(data)` | Joins `sections(data)` with blank lines and adds a trailing newline. Override it to replace the layout entirely | | `sections(data)` | `[description, usage, commands, args, flags]`, with empty sections left out | | `formatUsage`, `formatCommands`, `formatArgs`, `formatFlags` | Each renders one section from `data` | | `commands(data)`, `flags(data)` | Choose what to list: hidden entries are left out unless `showHidden`, and flags are sorted unless `sortFlags: false` | | `formatCommand`, `formatArg`, `formatFlag` | One `[left, right]` table row each | | `formatTags`, `formatValue`, `formatPath` | ` (required; default: 3)`, default values, ` deploy` | | `formatTable(rows)`, `formatSection(title, lines)` | Layout primitives | The text `render` returns is printed as-is, so it should end with a newline. ## Help data `HelpData` holds the building blocks of a page, and the template decides what to use and how. It makes no layout decisions: nothing is filtered or sorted. | Field | Contents | | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `program` | `name`, `version`, `description` | | `path` | The command path as `{ name, dynamic }` parts. `formatPath(path)` gives ` deploy` | | `description`, `aliases`, `runnable` | This level's description, its command's aliases, and whether it has a command to run | | `usage` | Usage lines, e.g. `app deploy [flags] []` | | `commands` | Each subcommand: `name`, `label`, `description`, `aliases`, `hidden`, `dynamic`, full `path`, `runnable`, `group` (has subcommands) | | `args` | Each arg in order: `name`, `label` (``), `usage` (`[]`), `source` (`command` or `scope`), `segment` | | `flags` | Each flag in definition order, hidden ones included: `key`, `name`, `alias`, `kind` (`flag` or `switch`), `env`, `source` (`command`, `scope` or `builtin`), `hidden`, `label` (`-n, --dry-run `) | Args and flags also carry `description`, `type`, `required`, `multiple`, `hasDefault`, the raw `default`, `choices`, and `options`. `options` is everything passed to the creator, including `min`, `max`, `integer`, `exists` and the options of custom parsers. See the [Help API](../api/help) for every type. --- --- url: https://jinju.js.org/guide/errors.md description: >- How invalid input is reported (every error at once, with exit code 1), throwing JinjuError from run, and customising output with ErrorTemplate. --- # Errors When input is invalid, Jinju collects every problem, prints them to stderr with a usage hint, and exits with code `1`. `run` isn't called. ```sh $ app my-site deploy prod --port 1 error: Unknown flag '--port' error: Unexpected argument '1' Run 'app my-site deploy --help' for usage. ``` ## Error classes Every error is a subclass of `JinjuError`, with its own details: | Class | Message | Properties | | ---------------------- | --------------------------------------- | -------------------------- | | `UnknownCommandError` | `Unknown command 'x'` | `command` | | `UnknownFlagError` | `Unknown flag '--x'` | `flag` | | `UnexpectedArgError` | `Unexpected argument 'x'` | `arg` | | `MissingRequiredError` | `Missing required --x` / `argument ` | `label` | | `MissingValueError` | `--x requires a value` | `label` | | `DuplicateFlagError` | `--x can only be provided once` | `label` | | `InvalidValueError` | `Invalid value 'v' for --x: ` | `label`, `input`, `reason` | | `ConfigError` | Describes a mistake in your definitions | | `ConfigError` is different from the rest: it's thrown synchronously, when you create commands, scopes or the program, for mistakes such as two flags with the same alias. It means the CLI itself is wrong, not the user's input. ## Throwing from `run` Throw a `JinjuError` from `run` to report a problem the same way as input errors, with exit code `1`: ```ts import { arg, command, JinjuError } from 'jinju'; command({ description: 'Open a project', args: [arg.string({ name: 'project', description: 'Project' }).required()], run: ({ args: [project] }) => { if (project === 'secret') throw new JinjuError(`No access to '${project}'`); }, }); ``` Other errors aren't caught: they reject the promise returned by `app.run()`. ## Customising output Error output is rendered by an `ErrorTemplate`, customised the same way as [help](./help): pass an instance as `error`, with options or a subclass. ```ts import { type CommandTree, ErrorTemplate, jinju, type JinjuError, MissingRequiredError, } from 'jinju'; declare const commands: CommandTree; class MyErrors extends ErrorTemplate { override formatError(error: JinjuError) { return error instanceof MissingRequiredError ? `✗ please provide ${error.label}` : super.formatError(error); } } jinju({ error: new MyErrors({ hint: false }), commands }); ``` | Option | Default | Description | | ------ | ------- | --------------------------------------- | | `hint` | `true` | Add a `Run '… --help' for usage.` line. | | Method | Default | | ------------------------------------------ | ------------------------------------------------------------------- | | `render(data)` | Joins `sections(data)` with blank lines and adds a trailing newline | | `sections(data)` | `[errors, hint]`; the hint is left out with `hint: false` | | `formatErrors(data)`, `formatError(error)` | One `error: ` line per error | | `formatHint(data)`, `formatCommand(data)` | `Run 'app my-site deploy --help' for usage.` | ## Error data `ErrorData` has: * `program`: the program's `name`, `version` and `description` * `errors`: the `JinjuError` instances, in the order they were found * `path`: the matched command path * `words`: the command path as typed, e.g. `['my-site', 'deploy']` * `help`: the matched level's full [help data](./help#help-data), for showing usage or suggesting names --- --- url: https://jinju.js.org/guide/testing.md description: >- Test a CLI in-process by injecting argv, env, stdout, stderr and the exit code. --- # Testing A program's input and output can all be injected, so a CLI can be tested in-process without spawning Node or touching `process`: * `run(argv)` takes argv directly * `stdout` and `stderr` receive output * `exit` receives the exit code instead of setting `process.exitCode` * `env` replaces `process.env` for flags' environment variables ```ts import { arg, command, jinju } from 'jinju'; import { expect, test } from 'vitest'; const greet = command({ description: 'Greet someone', args: [arg.string({ name: 'name', description: 'Who to greet' }).required()], run: ({ args: [name] }) => { console.log(`Hello, ${name}!`); }, }); const setup = () => { const output = { stdout: '', stderr: '' }; const app = jinju({ name: 'greet', commands: { $: greet }, env: {}, stdout: (text) => (output.stdout += text), stderr: (text) => (output.stderr += text), exit: () => {}, }); return { app, output }; }; test('requires a name', async () => { const { app, output } = setup(); expect(await app.run([])).toBe(1); expect(output.stderr).toContain('Missing required argument '); }); ``` `stdout` only receives Jinju's own output (help and version). Output your `run` handlers write is up to you. For testable handlers, pass dependencies such as a logger in from the outside. ## Testing commands directly A command's `parse(argv, options?)` method parses input without a program, and resolves to `{ ok: true, value }` or `{ ok: false, errors }`: ```ts import { command, flag } from 'jinju'; const serve = command({ description: 'Serve', flags: { port: flag.number({ description: 'Port' }).default(8080) }, run: () => {}, }); const result = await serve.parse(['--port', '3000']); if (result.ok) result.value.flags; // { port: 3000 } ``` ## Snapshotting help `app.help(path?)` returns a help page as a string, which is handy for snapshot tests: ```ts expect(app.help(['sync'])).toMatchSnapshot(); ``` --- --- url: https://jinju.js.org/guide/typescript.md description: >- The types for naming what Jinju infers: InferFlagType, InferFlags, InferArgs and more. --- # TypeScript Everything in Jinju is typed from your definitions: there are no separate interfaces to keep in sync. This page covers the types you can use to name those types yourself. ## Inferring values ```ts import { arg, flag, type InferArgs, type InferFlags, type InferFlagType } from 'jinju'; const port = flag.number({ description: 'Port' }).default(8080); type Port = InferFlagType; // type Port = number const flags = { port, host: flag.string({ description: 'Host' }), }; type Flags = InferFlags; // type Flags = { // port: number; // host: string | undefined; // } const args = [arg.file({ description: 'Files' }).multiple().required()] as const; type Args = InferArgs; // type Args = [string[]] ``` | Type | Gives | | ------------------ | ----------------------------------------------------------------------------------- | | `InferFlagType` | The value a flag resolves to | | `InferArgType` | The value an arg resolves to | | `InferFlags` | An object of flag values, as `run` receives it | | `InferArgs` | A tuple of arg values, as `run` receives it | | `MergeFlags` | `A` with `B`'s keys added or replaced, which is how scope and command flags combine | ## Sharing handlers `RunContext` is the type of `run`'s context, so a handler can be written separately from its command: ```ts import { command, flag, type RunContext } from 'jinju'; const cleanFlags = { force: flag.boolean({ description: 'Force' }) }; const clean = ({ flags }: RunContext) => { if (flags.force) console.log('forcing'); }; command({ description: 'Clean', flags: cleanFlags, run: clean }); ``` ## Compile-time checks Some mistakes are reported as type errors as well as at runtime: * more than one `.multiple()` arg (`CheckArgs`) * [dynamic segment](./dynamic-segments#compile-time-checks) problems in the command tree (`CheckTree`) * Standard Schemas whose input can't be a string (`CheckSchemaInput`) These types resolve to an object with an `~error` key that describes the problem, so the message appears in the type error. ## Readable types Types are written to show resolved values in editors, e.g. `string[] | undefined` rather than an alias such as `Flag<…>`. Every type used in a public signature is exported, so you can name anything you see. See the [Types API](../api/types) for the full list. --- --- url: https://jinju.js.org/api/flag.md description: >- API reference for flag: every flag and switch creator, their options and the Flag modifiers. --- # flag Creators for flags and switches. See the [Flags guide](../guide/flags) for usage. ## `flag` ```ts const flag: { string(options: FlagOptions & StringOptions): Flag; number(options: FlagOptions & NumberOptions): Flag; url(options: FlagOptions): Flag; email(options: FlagOptions): Flag; directory(options: FlagOptions & PathOptions): Flag; file(options: FlagOptions & PathOptions): Flag; boolean(options: FlagOptions): Switch; count(options: FlagOptions): Switch; custom(parse: Parse, valueName?: string): (options: FlagOptions & O) => Flag; schema(schema: S, valueName?: string): (options: FlagOptions) => Flag | undefined, SchemaOutput>; }; ``` | Creator | Value | Value name in help | Notes | | ---------------- | ------------- | --------------------- | -------------------------------------------------------------------- | | `flag.string` | `string` | `string` | [`StringOptions`](./types#stringoptions): `choices` narrows the type | | `flag.number` | `number` | `number` | [`NumberOptions`](./types#numberoptions): `min`, `max`, `integer` | | `flag.url` | `URL` | `url` | | | `flag.email` | `string` | `email` | | | `flag.directory` | `string` | `directory` | Absolute path. [`PathOptions`](./types#pathoptions): `exists` | | `flag.file` | `string` | `file` | Absolute path. [`PathOptions`](./types#pathoptions): `exists` | | `flag.boolean` | `boolean` | | Switch, defaults to `false` | | `flag.count` | `number` | | Switch, defaults to `0` | | `flag.custom` | Your parser's | `valueName` (`value`) | See [`Parse`](./types#parse) | | `flag.schema` | Schema output | `valueName` (`value`) | The schema's input must accept a string | ## `Flag` {#flag-class} A flag that takes a value. `T` is the resolved type (e.g. `string[] | undefined`) and `V` is what one value parses to. Extends [`Param`](./types#param). ```ts class Flag extends Param { readonly kind: 'flag'; readonly options: FlagOptions; required(required?: boolean): Flag<…>; multiple(allow?: boolean): Flag<…>; default(value: DefaultValue): Flag<…>; } ``` | Method | Effect | | ----------------- | --------------------------------------------------------------------------------- | | `.required(on?)` | Must be provided. Clears any default. | | `.multiple(on?)` | Repeatable, collected into an array. Converts any existing default (`v` ↔ `[v]`). | | `.default(value)` | Used when absent. Clears `required`. Must be a non-empty array when `multiple`. | Each returns a new flag: flags are immutable. ## `Switch` A flag that never takes a value: `flag.boolean` and `flag.count`. Always resolves to a value. Extends [`Param`](./types#param). ```ts class Switch extends Param { readonly kind: 'switch'; readonly options: FlagOptions; default(value: T): Switch; resolveSwitch(occurrences: readonly boolean[]): T; } ``` `resolveSwitch` turns occurrences (`--x` → `true`, `--no-x` → `false`) into the value, or the default when there are none. ## `FlagOptions` ```ts interface FlagOptions { description: string; alias?: string; hidden?: boolean; env?: string; } ``` | Option | Description | | ------------- | -------------------------------------------------------------------------------------------------------------------- | | `description` | Shown in help. | | `alias` | Another name, without dashes: `'v'` → `-v`, `'dir'` → `--dir`. | | `hidden` | Leave out of help. | | `env` | Environment variable to read when the flag isn't given. See [Environment variables](../guide/environment-variables). | ## `FlagRecord` ```ts type FlagRecord = Record; ``` An object of flags, as passed to `flags`. ## `AnyFlag` ```ts type AnyFlag = Flag | Switch; ``` Any flag or switch, for constraints in generic code. --- --- url: https://jinju.js.org/api/arg.md description: >- API reference for arg: every positional arg creator, their options and the Arg modifiers. --- # arg Creators for positional args. See the [Args guide](../guide/args) for usage. ## `arg` ```ts const arg: { string(options: ArgOptions & StringOptions): Arg; number(options: ArgOptions & NumberOptions): Arg; url(options: ArgOptions): Arg; email(options: ArgOptions): Arg; directory(options: ArgOptions & PathOptions): Arg; file(options: ArgOptions & PathOptions): Arg; custom(parse: Parse, valueName?: string): (options: ArgOptions & O) => Arg; schema(schema: S, valueName?: string): (options: ArgOptions) => Arg | undefined, SchemaOutput, N>; }; ``` The creators match [`flag`](./flag#flag)'s value flags. There are no switches. ## `Arg` {#arg-class} A positional arg. `T` is the resolved type, `V` is what one value parses to, and `N` is its `name` when given as a literal, which is used to check [dynamic segments](../guide/dynamic-segments). Extends [`Param`](./types#param). ```ts class Arg extends Param { readonly kind: 'arg'; readonly options: ArgOptions; required(required?: boolean): Arg<…>; multiple(allow?: boolean): Arg<…>; default(value: DefaultValue): Arg<…>; } ``` The modifiers work like [`Flag`](./flag#flag-class)'s. Args are optional by default, and at most one arg in a command can be `.multiple()`. ## `ArgOptions` ```ts interface ArgOptions { description: string; name?: N; } ``` | Option | Description | | ------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `description` | Shown in help. | | `name` | Display name in usage, e.g. `'files'` → ``. Defaults to the value type's name. Also binds the arg to a `:name` dynamic segment. | ## `ArgList` ```ts type ArgList = readonly AnyArg[]; ``` A tuple of args, as passed to `args`. ## `AnyArg` ```ts type AnyArg = Arg; ``` Any arg, for constraints in generic code. --- --- url: https://jinju.js.org/api/command.md description: >- API reference for command() and scope(): their config, the run context and returned values. --- # command & scope See the [Commands](../guide/commands) and [Scopes](../guide/scopes) guides for usage. ## `command` ```ts function command( config: CommandConfig>, ): Command; ``` Creates a standalone command. The same as `scope({}).command(config)`. ## `scope` ```ts function scope( config: ScopeConfig, ): Scope; ``` Creates a [`Scope`](#scope-interface): flags and args shared by every command created from it. ## `CommandConfig` ```ts interface CommandConfig { name?: string; description: string; aliases?: readonly string[]; hidden?: boolean; flags?: F; args?: A & CheckArgs<[...Prefix, ...A]>; run: (context: Ctx) => RunResult; } ``` `Ctx` is the context `run` receives, a [`RunContext`](#runcontext). `Prefix` is the args inherited from a scope, which are checked together with the command's own. ## `RunContext` ```ts interface RunContext { flags: InferFlags; args: InferArgs; argv: readonly string[]; } ``` What `run` receives: typed flags and args, and the argv the command parsed (without the command path). ## `RunResult` ```ts type RunResult = void | number | Promise; ``` What `run` returns. A number sets the exit code. ## `Command` ```ts class Command { readonly config: CommandConfig; readonly flags: F; readonly args: A; readonly inherited: ReadonlySet; get description(): string; get params(): readonly Param[]; parse(argv: readonly string[], options?: ParseOptions): Promise>; execute(parsed: Parsed): RunResult; } ``` | Member | Description | | ----------- | ----------------------------------------------------------- | | `flags` | Own and inherited flags. | | `args` | Inherited args followed by own args. | | `inherited` | Keys of flags inherited from the scope and not redefined. | | `params` | The args as [`Param`](./types#param)s. | | `parse` | Parses argv (without the command path) into flags and args. | | `execute` | Calls `run` with parsed input. | Create commands with [`command`](#command) or `scope.command` rather than `new Command`. ## `ParseOptions` ```ts interface ParseOptions { globals?: FlagRecord; strict?: boolean; leading?: readonly string[]; env?: Readonly>; } ``` | Option | Description | | --------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `globals` | Extra flags parsed and validated alongside the command's own, such as the program's built-ins. Their values aren't included in the result. | | `strict` | Reject unknown flags. Defaults to `true`. | | `leading` | Positional values captured before the command name (dynamic segments), placed first. | | `env` | Where flags' `env` variables are read from. Defaults to `process.env`. | ## `Parsed` ```ts interface Parsed { flags: Record; args: unknown[]; argv: readonly string[]; } ``` The untyped result of `Command.parse`, passed to `Command.execute`. ## `Scope` {#scope-interface} ```ts interface Scope

{ readonly flags: P; readonly args: PA; command( config: CommandConfig, [...PA, ...A]>, PA>, ): Command, [...PA, ...A]>; scope( config: ScopeConfig, ): Scope, [...PA, ...A]>; } ``` `command` creates a command that inherits the scope's flags and args. `scope` creates a nested scope that adds more. ## `ScopeConfig` ```ts interface ScopeConfig { flags?: F; args?: A & CheckArgs<[...Prefix, ...A]>; } ``` `flags` are declared by every command in the scope, and accepted before the command name. Every command in the scope starts with `args`. ## `ScopeDefinition` ```ts interface ScopeDefinition { flags: FlagRecord; args: ArgList; } ``` The flags and args a command inherits from its scope. ## `AnyCommand` ```ts type AnyCommand = Command; ``` Any command, for constraints in generic code. --- --- url: https://jinju.js.org/api/program.md description: >- API reference for jinju() and Program: the program config, run() and help(), and packageVersion(). --- # Program See the [Program guide](../guide/program) for usage. ## `jinju` ```ts function jinju(config: ProgramConfig): Program; ``` Creates a program from a tree of commands. Throws a `ConfigError` if the tree or its commands are invalid. ## `ProgramConfig` ```ts interface ProgramConfig { name?: string; version?: string; description?: string; commands: T & CheckTree; strict?: boolean; env?: Readonly>; help?: HelpTemplate; error?: ErrorTemplate; stdout?: (text: string) => void; stderr?: (text: string) => void; exit?: (code: number) => void; } ``` | Option | Default | Description | | ------------- | -------------------------- | ------------------------------------------- | | `name` | The script's file name | Shown in usage. | | `version` | | Enables `--version`. | | `description` | | Shown on the root help page. | | `commands` | | The [command tree](#commandtree). | | `strict` | `true` | Reject unknown flags. | | `env` | `process.env` | Where flags' `env` variables are read from. | | `help` | `new HelpTemplate()` | Renders help pages. | | `error` | `new ErrorTemplate()` | Renders error output. | | `stdout` | Writes to `process.stdout` | Receives help and version output. | | `stderr` | Writes to `process.stderr` | Receives error output. | | `exit` | Sets `process.exitCode` | Receives the exit code. | ## `Program` ```ts class Program { readonly name: string; constructor(config: ProgramConfig); run(argv?: readonly string[]): Promise; help(path?: readonly string[]): string; } ``` What [`jinju`](#jinju) returns. Define it once, then call its methods: ```ts const app = jinju({ name: 'app', commands }); await app.run(); ``` Use `jinju` rather than `new Program`, because `jinju` checks the command tree's types. ### Properties | Property | Description | | -------- | ---------------------------------------------------------------------- | | `name` | The program's name: `name` from the config, or the script's file name. | ### `run(argv?)` ```ts run(argv?: readonly string[]): Promise; ``` Parses `argv`, routes it to a command and runs it. `argv` defaults to `process.argv.slice(2)`; pass it yourself to run with other input, e.g. `app.run(['build', '--watch'])`. Resolves to the exit code, which it also passes to `exit` (by default, setting `process.exitCode`). Help, `--version` and errors are written to `stdout` or `stderr`. It rejects only when a `run` handler throws an error that isn't a `JinjuError`. ### `help(path?)` ```ts help(path?: readonly string[]): string; ``` Returns the help text for a command path, e.g. `app.help(['sync', 'repeat'])`, without printing it. With no path, it returns the root page. Throws `UnknownCommandError` for an unknown name. ## `CommandTree` ```ts interface CommandTree { readonly [key: string]: AnyCommand | CommandTree; } ``` Commands keyed by name. Nest objects for subcommands; `$` is the command for that level. A `:name` key is a dynamic segment, which captures one word into the commands' arg named `name`. ## `packageVersion` ```ts function packageVersion(from: string | URL): string; ``` Reads `version` from the nearest `package.json` at or above `from`, usually the CLI's own `import.meta.url`. Pass the result as `jinju({ version })`. Throws a `ConfigError` when there's no `package.json`, or it has no `version`. --- --- url: https://jinju.js.org/api/help.md description: API reference for HelpTemplate and the help data it renders. --- # Help See the [Help guide](../guide/help) for usage. ## `HelpTemplate` ```ts class HelpTemplate { readonly options: Readonly>; constructor(options?: HelpTemplateOptions); render(data: HelpData): string; sections(data: HelpData): string[]; commands(data: HelpData): readonly HelpCommand[]; flags(data: HelpData): readonly HelpFlag[]; formatUsage(data: HelpData): string; formatCommands(data: HelpData): string; formatArgs(data: HelpData): string; formatFlags(data: HelpData): string; formatCommand(command: HelpCommand): HelpRow; formatArg(arg: HelpArg): HelpRow; formatFlag(flag: HelpFlag): HelpRow; formatTags(param: HelpArg | HelpFlag): string; formatValue(value: unknown): string; formatPath(path: readonly HelpPathPart[]): string; formatTable(rows: readonly HelpRow[]): string[]; formatSection(title: string, lines: readonly string[]): string; } ``` Renders help pages from [`HelpData`](#helpdata). This is the built-in layout: extend it and override any method to change part of it, or `render` to replace it. | Method | Default | | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- | | `render` | Joins `sections` with blank lines and adds a trailing newline. The result is printed as-is. | | `sections` | `[description, usage, commands, args, flags]`, with empty sections left out | | `commands`, `flags` | What to list: hidden entries are left out unless `showHidden`; flags are sorted unless `sortFlags: false` | | `formatUsage`, `formatCommands`, `formatArgs`, `formatFlags` | One section each, or `''` when there's nothing to show | | `formatCommand`, `formatArg`, `formatFlag` | One table row each: `[label, description + tags]` | | `formatTags` | e.g. ` (required; default: 3)`. Switch defaults aren't shown | | `formatValue` | A default value; arrays are joined with `, ` | | `formatPath` | [`formatPath`](#formatpath) | | `formatTable` | Rows with the left column padded to a common width | | `formatSection` | A `Title:` heading followed by indented lines | ## `HelpTemplateOptions` ```ts interface HelpTemplateOptions { sortFlags?: boolean; showHidden?: boolean; } ``` | Option | Default | Description | | ------------ | ------- | ---------------------------------- | | `sortFlags` | `true` | Sort flags by long name. | | `showHidden` | `false` | Include hidden commands and flags. | ## `HelpData` ```ts interface HelpData { program: HelpProgram; path: readonly HelpPathPart[]; description?: string; aliases: readonly string[]; runnable: boolean; usage: readonly string[]; commands: readonly HelpCommand[]; args: readonly HelpArg[]; flags: readonly HelpFlag[]; } ``` Everything known about the page being rendered. Nothing is filtered or sorted. | Field | Description | | ------------- | --------------------------------------------------------------------------------------- | | `program` | The program's name, version and description. | | `path` | The command path below the program. Empty at the root. | | `description` | This level's description: its command's, or the program's at the root. | | `aliases` | Aliases of this level's command. | | `runnable` | Whether this level has a command to run, as opposed to only subcommands. | | `usage` | Usage lines, e.g. `app deploy [flags] []` and `app `. | | `commands` | Subcommands, in definition order. | | `args` | Args, in order. | | `flags` | Every flag accepted here (own, inherited and built-in), in definition order. | ## `HelpProgram` ```ts interface HelpProgram { name: string; version?: string; description?: string; } ``` ## `HelpPathPart` ```ts interface HelpPathPart { name: string; dynamic: boolean; } ``` One step of a command path. `dynamic` is `true` for a dynamic segment (`:name` in the command tree). ## `HelpCommand` ```ts interface HelpCommand extends HelpPathPart { label: string; description: string; aliases: readonly string[]; hidden: boolean; path: readonly HelpPathPart[]; runnable: boolean; group: boolean; } ``` A subcommand. `label` is `deploy`, or `` for a dynamic segment. `path` is its full path from the program. `runnable` means it has a command to run, and `group` means it has subcommands. ## `HelpParam` ```ts interface HelpParam { description: string; type: string; required: boolean; multiple: boolean; hasDefault: boolean; default: unknown; choices?: readonly string[]; options: Readonly>; } ``` Details shared by args and flags. `type` is the value type's name, e.g. `string`, `url`, or `boolean`/`count` for switches. `default` is the raw default value, only meaningful when `hasDefault`. `options` is everything passed to the creator. ## `HelpArg` ```ts interface HelpArg extends HelpParam { name: string; label: string; usage: string; source: 'command' | 'scope'; segment: boolean; } ``` An arg. `name` is its `name`, or its type when unnamed. `label` is e.g. ``, and `usage` is how it appears in a usage line, e.g. `[]` or `...`. `segment` means its value comes from a dynamic segment in the command path. ## `HelpFlag` ```ts interface HelpFlag extends HelpParam { key: string; name: string; alias?: string; kind: 'flag' | 'switch'; env?: string; source: 'command' | 'scope' | 'builtin'; hidden: boolean; label: string; } ``` A flag. `key` is the key it's defined under (`dryRun`), and `name` is its long name without dashes (`dry-run`). `label` is e.g. `-n, --dry-run `. A switch defaulting to `true` shows as `--[no-]name`. ## `HelpRow` ```ts type HelpRow = readonly [string, string]; ``` A table row: the left column (e.g. a label) and the right column (e.g. a description). ## `formatPath` ```ts function formatPath(path: readonly HelpPathPart[]): string; ``` Joins a command path with spaces, with dynamic segments in angle brackets: ` deploy`. --- --- url: https://jinju.js.org/api/errors.md description: >- API reference for ErrorTemplate, JinjuError and every error class Jinju throws. --- # Errors See the [Errors guide](../guide/errors) for usage. ## `ErrorTemplate` ```ts class ErrorTemplate { readonly options: Readonly>; constructor(options?: ErrorTemplateOptions); render(data: ErrorData): string; sections(data: ErrorData): string[]; formatErrors(data: ErrorData): string; formatError(error: JinjuError): string; formatHint(data: ErrorData): string; formatCommand(data: ErrorData): string; } ``` Renders error output, which is printed to stderr, from [`ErrorData`](#errordata). Extend it and override any method to change part of it, or `render` to replace it. | Method | Default | | --------------- | ------------------------------------------------------------- | | `render` | Joins `sections` with blank lines and adds a trailing newline | | `sections` | `[errors, hint]`. The hint is left out with `hint: false` | | `formatErrors` | One line per error, from `formatError` | | `formatError` | `error: ` | | `formatHint` | `Run 'app my-site deploy --help' for usage.` | | `formatCommand` | The command as typed, e.g. `app my-site deploy` | ## `ErrorTemplateOptions` ```ts interface ErrorTemplateOptions { hint?: boolean; } ``` `hint` adds a "Run '… --help' for usage." line. Defaults to `true`. ## `ErrorData` ```ts interface ErrorData { program: HelpProgram; errors: readonly JinjuError[]; path: readonly HelpPathPart[]; words: readonly string[]; help: HelpData; } ``` | Field | Description | | --------- | ---------------------------------------------------------------------- | | `program` | The program's name, version and description. | | `errors` | The errors, in the order found. | | `path` | The matched command path, e.g. ` deploy`. Empty at the root. | | `words` | The command path as typed, e.g. `['my-site', 'deploy']`. | | `help` | The matched level's [`HelpData`](./help#helpdata), e.g. to show usage. | ## `JinjuError` ```ts class JinjuError extends Error {} ``` The base class for every error Jinju produces. Throw it (or a subclass) from `run` to report a problem with exit code `1`. ## `ConfigError` ```ts class ConfigError extends JinjuError {} ``` Thrown synchronously when flags, args or commands are defined in a conflicting way, such as two flags with the same alias, or an invalid command tree. ## `UnknownCommandError` ```ts class UnknownCommandError extends JinjuError { readonly command: string; } ``` `Unknown command ''`. ## `UnknownFlagError` ```ts class UnknownFlagError extends JinjuError { readonly flag: string; } ``` `Unknown flag ''`. Only reported in strict mode, which is the default. ## `UnexpectedArgError` ```ts class UnexpectedArgError extends JinjuError { readonly arg: string; } ``` `Unexpected argument ''`: a positional value with no arg to take it. ## `MissingRequiredError` ```ts class MissingRequiredError extends JinjuError { readonly label: string; } ``` `Missing required ; ``` The value an arg resolves to. ### `InferFlags` ```ts type InferFlags = { [K in keyof F]: InferFlagType }; ``` An object of flag values, as `run` receives it. ### `InferArgs` ```ts type InferArgs = { -readonly [K in keyof A]: InferArgType }; ``` A tuple of arg values, as `run` receives it. ### `MergeFlags` ```ts type MergeFlags; ``` `A` with `B`'s keys added or replaced. This is how scope and command flags combine. ## Checks Each check resolves to `unknown` when the input is valid, and otherwise to an object with an `~error` key that describes the problem, so the message appears in the type error. ### `CheckArgs` ```ts type CheckArgs; ``` Fails when more than one arg is `.multiple()`. ### `CheckTree` ```ts type CheckTree; ``` Fails on the first problem with dynamic segments in a command tree. See [Dynamic segments](../guide/dynamic-segments#compile-time-checks). ### `TreeErrors` ```ts type TreeErrors; ``` Every problem in tree `T` as a union of messages, or `never`. `S` is the segments above it and `At` is its path. Used by `CheckTree`. ### `NodeErrors` ```ts type NodeErrors; ``` Problems for a command or subtree `V` at `At`, or `never`. Used by `TreeErrors`. ### `SegmentError` ```ts type SegmentError; ``` The first problem binding args `A` of the command at `At` to segments `S`, or `never`. Used by `NodeErrors`. ### `CheckSchemaInput` ```ts type CheckSchemaInput; ``` Fails when a schema can't receive a string, e.g. `z.number()`. ## Params ### `Param` ```ts abstract class Param { abstract readonly kind: ParamKind; readonly value: ValueType; readonly options: ParamOptions; readonly state: ParamState; readonly defaultValue: unknown; resolve(inputs: readonly string[], label: string): Promise>; } ``` The base class of [`Flag`](./flag#flag-class), [`Switch`](./flag#switch) and [`Arg`](./arg#arg-class). `T` is the resolved type, e.g. `string[] | undefined`, and `V` is what one input parses to. `resolve` parses raw inputs into the final value, applying the modifiers. ### `ParamKind` ```ts type ParamKind = 'arg' | 'flag' | 'switch'; ``` ### `ParamOptions` ```ts interface ParamOptions { description: string; alias?: string; hidden?: boolean; name?: string; choices?: readonly string[]; env?: string; } ``` The options any param can have: the union of [`FlagOptions`](./flag#flagoptions) and [`ArgOptions`](./arg#argoptions). ### `ParamState` ```ts interface ParamState { readonly required: boolean; readonly multiple: boolean; readonly hasDefault: boolean; } ``` The modifier state at runtime. At the type level, it's derived from `T` and `V`. ### `ParamInit` ```ts interface ParamInit { value: ValueType; options: ParamOptions; state: ParamState; defaultValue?: unknown; } ``` What a param is constructed from. ### `Toggle` ```ts type Toggle = (occurrences: readonly [boolean, ...boolean[]]) => V; ``` Reduces a switch's occurrences (`--x` → `true`, `--no-x` → `false`) to its value. Only called when the switch was given at least once. ### `Result` ```ts type Result = { ok: true; value: T } | { ok: false; errors: JinjuError[] }; ``` ### `Defined` ```ts type Defined = {} | null; ``` A parsed value: anything but `undefined`, which means "not provided". ### `IsMultiple` ```ts type IsMultiple; ``` `true` when resolved type `T` collects `V` values into an array. ### `DefaultValue` ```ts type DefaultValue = IsMultiple extends true ? readonly [V, ...V[]] : V; ``` What `.default()` accepts: a non-empty array for `.multiple()` params, so `.multiple(false)` can take the first value. ## Values ### `Parse` ```ts type Parse = (input: string, options: O) => V | Promise; ``` Converts a raw command-line string into a typed value, using the options passed to the creator. Throw any `Error` to reject the input: its message is shown to the user. ### `ValueType` ```ts interface ValueType { readonly name: string; readonly parse: (input: string) => V | Promise; } ``` A parser bound to its options, and the name used in help (`--port `). ### `StringOptions` ```ts interface StringOptions { choices?: readonly C[]; } ``` `choices` restricts input to these values, and the type is their union. ### `NumberOptions` ```ts interface NumberOptions { min?: number; max?: number; integer?: boolean; } ``` ### `PathOptions` ```ts interface PathOptions { exists?: boolean; } ``` `exists` checks the path exists and is the right kind of entry (file or directory). ## Standard Schema ### `StandardSchemaV1` ```ts interface StandardSchemaV1 { readonly '~standard': { readonly version: 1; readonly vendor: string; readonly validate: ( value: unknown, ) => StandardSchemaResult | Promise>; readonly types?: { readonly input: Input; readonly output: Output } | undefined; }; } ``` The [Standard Schema](https://standardschema.dev) interface, as far as Jinju uses it. It's structurally compatible with `@standard-schema/spec`, so schemas from any compliant library work without Jinju depending on them. ### `StandardSchemaResult` ```ts type StandardSchemaResult = | { readonly value: Output; readonly issues?: undefined } | { readonly issues: readonly StandardSchemaIssue[] }; ``` ### `StandardSchemaIssue` ```ts interface StandardSchemaIssue { readonly message: string; readonly path?: readonly (PropertyKey | { readonly key: PropertyKey })[] | undefined; } ``` ### `SchemaInput` ```ts type SchemaInput; ``` A schema's declared input type, or `unknown` when it doesn't declare `types`. ### `SchemaOutput` ```ts type SchemaOutput; ``` What a schema validates to. --- --- url: https://jinju.js.org/changelog.md description: Every notable change to Jinju, by release. --- # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.1.1] - 2026-10-25 ### Changed * Tooling: updated `fallow` to 3.35 and `tsdown` to 0.23.2 ## [0.1.0] - 2026-10-24 ### Added * Flags: `flag.string` (with typed `choices`), `flag.number` (`min`, `max`, `integer`), `flag.url`, `flag.email`, `flag.directory` and `flag.file` (`exists`), and `flag.custom` for user-defined parsers, which may be async * Standard Schema support: `flag.schema(schema, name?)` and `arg.schema(schema, name?)` use any Standard Schema (zod, valibot, arktype, …) as the parser, with types from the schema's output. Schemas that can't receive a string (e.g. `z.number()`) are type errors. No new dependencies; a compatible `StandardSchemaV1` type is exported * Environment variable fallback: `env: 'API_TOKEN'` on any flag reads the variable when the flag isn't given (priority: argv, env, default), parsed and validated like a command-line value. `jinju({ env })` supplies the variables (defaults to `process.env`), and `HelpFlag.env` exposes them to help templates * Switches: `flag.boolean` (with `--no-x` negation, and `--x=` accepting `true`/`false`, `1`/`0`, `yes`/`no`, `on`/`off`) and `flag.count` (`-vvv`). Built-in `--help`/`--version` follow the same rules * Positional args: `arg.*` creators sharing the same parsers as flags, plus `arg.custom` * Immutable modifiers `.required()`, `.multiple()` and `.default()` on flags and args, with fully inferred types (`InferFlagType`, `InferArgType`, `InferFlags`, `InferArgs`). `Flag`, `Arg` and `Switch` are classes built on a shared abstract `Param` base. Every type used in a public signature is exported (e.g. `Param`, `Defined`, `ParseOptions`, `MergeFlags`, `CheckArgs`) * Positional allocation supporting one `.multiple()` arg in any position (e.g. `cp`-style `[...sources, dest]`) * `command()` with typed `run` context, optional `name`, `aliases`, `hidden` and exit codes returned from `run` * `scope()` for flags and args shared by groups of commands; scopes nest, and shared flags are accepted before the command name * Dynamic segments: a `:name` key in the command tree reads a value before the command name into the commands' arg called `name` (e.g. `app my-site deploy`), with static names taking priority and setup checked by `jinju()`. Mistakes in the command tree are compile-time errors: `$` must be a command, one segment per level, no args on `$` next to a segment, and (with literal arg names) each command's segment args. `Arg` has a third type parameter carrying its literal `name`, and `CheckTree` (with its building blocks `TreeErrors`, `NodeErrors` and `SegmentError`) is exported * `jinju()` program with nested command trees (`$` for a level's own command), built-in `--help`/`-h` and `--version`, help generation, `strict` mode and injectable `stdout`, `stderr` and `exit` * Customisable help: pass a `HelpTemplate` instance as `help` to `jinju()`. Configure it with `sortFlags` and `showHidden`, or extend it and override any method: `render`, `sections`, the per-section and per-row formatters (`formatFlag`, `formatArg`, `formatCommand`, …) and the layout primitives. It renders from `HelpData`, the building blocks of a page with no filtering or sorting applied: the program, path, usage lines, and every subcommand, arg and flag with its syntax `label`, `source` (command, scope or built-in), raw details and creator `options`. Also exported: `formatPath`, and the `HelpTemplateOptions`, `HelpRow`, `HelpData`, `HelpProgram`, `HelpPathPart`, `HelpCommand`, `HelpArg`, `HelpFlag` and `HelpParam` types * Customisable errors: pass an `ErrorTemplate` instance as `error` to `jinju()`. Configure it with `hint`, or extend it and override `render`, `sections`, `formatErrors`, `formatError`, `formatHint` or `formatCommand`. It renders from `ErrorData`: the program, the `JinjuError` instances, the matched path, the words as typed, and the matched level's `HelpData`. Also exported: the `ErrorData` and `ErrorTemplateOptions` types * `packageVersion(import.meta.url)` reads `version` from the nearest `package.json`, for `jinju({ version })` * Tokenizer support for `--x=v`, `--x v`, `-xv`, bundled shorts (`-abc`), negative numbers as values and `--` * Error classes extending `JinjuError`; errors are collected and reported together * Tooling: `fallow` (with type-aware analysis enabled, and `fallow dead-code` checked in CI), CI tests on Node 22, 24 and 26 (including loading the build with `require()`), coverage checks (100% required), CI summary support and type tests run by `vitest` * `AGENTS.md` (imported by `CLAUDE.md`) with contributor guidance for coding agents, including keeping the changelog and documentation up to date * Documentation site at https://jinju.js.org/, built with VitePress: guides and an API reference, with examples type-checked against the source. Docs are versioned: the root is the latest release, `/x.y.z/` each release and `/next/` the newest prerelease, with a version switcher. Every release redeploys it to GitHub Pages. For AI assistants, it publishes `llms.txt`, `llms-full.txt` and a Markdown copy of every page, with the types the examples show on hover written in as comments, and each page has a Copy page menu for copying it as Markdown or opening it in ChatGPT or Claude * Docs tooling: `docs:dev`, `docs:build` and `docs:build:all` scripts, a docs build in CI (catching dead links and broken examples), and a test that every export has an entry in the API reference ### Changed * `README.md` is now a short introduction. The full reference has moved to the documentation site, which is also the package's `homepage` * Build is ESM-only (`dist/index.js`, `dist/index.d.ts`), built with `tsdown` (rolldown), with `exports` fixed to match the build output * Requires Node `>=22.12`. CommonJS projects can `require('jinju')`, which loads the ESM build * Replaced `eslint` with `oxlint` and `prettier` with `oxfmt` * Updated `node` to `v24` (pinned to `24.22.0` in `.nvmrc`), `vitest` to `v5` and `typescript` to `v6` * Updated GitHub Actions `actions/checkout` and `actions/setup-node` to `v7` * Releases publish to npm with Trusted Publishing (OIDC) instead of an `NPM_TOKEN` secret, and published versions now carry provenance. Prereleases run from `release.yml` after CI passes on `main` * Bump dependencies * Releases are staged on npm (`npm stage publish` via trusted publishing) and go live once approved on npmjs.com * VSCode config updates * Updated LICENSE date ### Removed * The 0.0.1 APIs (`celebrate`, `debug`, `fail`, `log`, `spinner`, `success`, `warn`) and configuration support, replaced by the command-line toolkit above * CommonJS build, and `rollup` with its plugins (replaced by `tsdown`) ## [0.0.1] - 2025-05-06 ### Added * Initial setup * New APIs; `celebrate`, `debug`, `fail`, `log`, `spinner`, `success`, `warn` * Ability to set a configuration [Unreleased]: https://github.com/simmo/jinju/compare/0.1.1...HEAD [@simmo]: https://github.com/simmo [0.0.1]: https://github.com/simmo/jinju/compare/0.0.1-beta.16...0.0.1 [0.1.1]: https://github.com/simmo/jinju/compare/0.1.0...0.1.1 [0.1.0]: https://github.com/simmo/jinju/compare/0.1.0-beta.1...0.1.0