CLI for parsed commands and generated help,
or a script CLI when the program should parse its own arguments.
Shell builtins and CLIs keep separate catalogs and dispatch. Both use
CommandSpec for argument parsing.
Define a typed command
Each handler receives oneCLIInvocation. The example below shows the
registration and parsing contract; the runnable pager example
adds a mutable incident service and shared cross-language coverage.
type: 'path'. Read flags through new FlagView(inv.flags, inv.spec)
so a read of an undeclared option fails rather than looking like an omitted flag.
Choose the command shape
A typed node is a leaf or a group. Names and aliases are single words;
child names and aliases share one namespace. Options cannot collide across
ancestors and descendants. Construction validates the tree and compiles the
grammar; do not repeat parser validation in every handler.
CommandSpec.arguments holds options and positional arguments: a dashed name
such as --by declares an option; a bare name declares a positional.
CLI.handlers maps canonical paths below the root to CLIHandler bindings,
such as "ack", "incident ack", or "" for a single-command program.
Declare configModel on the account CLI. Registration validates
each installation, and inv.config holds that account’s settings. Without a
config model, a typed CLI accepts no config and receives null.
Mark mutating CLIHandler bindings write: true. This classifies the command for policy and
cache invalidation; it does not grant permission or perform the mutation.
Descriptions, options and operands supply --help and man at every level.
Register the program
Installing a CLI directly is the shortest path:./file.mjs:ATTR and package references
such as my-clis/specs:PAGER. Relative files resolve next to the YAML file. A
.js file follows its nearest package.json type; .mjs is unambiguous. A
.ts file uses Node’s type stripping, not compilation, so syntax requiring
transformation must be built first.
Registered names and builtins win over direct references. A name collision
fails rather than silently replacing another program. TypeScript has no Python
entry-point discovery equivalent; use a package reference or call
registerCliSpec from the host.
Registration and installation are host-side lifecycle operations. There is no
shell install or uninstall command, so an agent can use a CLI it was given
but cannot remove it. A deployment that must pin the head word should enforce
that with policy; shell functions can otherwise shadow it like they do in bash.
Use workspace files
Build the service client frominv.config. A CLI should not find a matching
VFS to obtain credentials or a backend accessor. Reuse core service functions
between CLI and VFS implementations where their behavior is the same.
For file arguments such as uploads or attachments, use inv.view:
Declare file options and operands with
type: 'path'; preserve the resulting
PathSpec values when dispatching. Use PathSpec.fromStrPath(value, undefined, inv.cwd) for a path string from
config, and derive paths with join and parent. Do not convert them to plain
strings before filesystem access. Direct handler callers may omit the view;
a handler requiring files must refuse when the needed entry point is absent.
Account CLI writes, including failed writes that may have reached the service,
expire mounted caches. Config-free filesystem CLIs rely on the dispatcher’s
per-path invalidation. A script marked as a write also expires caches because
its config and service access are opaque.
Snapshots and credentials
A named typed CLI is saved by spec name. A Zod objectconfigModel doubles as
the redaction schema; mark string credentials with secretStr(). Loading a
snapshot with redacted fields must provide fresh install config through clis;
Mirage refuses to restore redacted credentials as if they were usable values.
configModel, but its result is
opaque to snapshot redaction and is serialized as-is. Prefer a Zod object for
credential-bearing config. The loading process must also make the spec name
resolvable, or pass [cli, config] as that install’s clis override.
Workspace.copy() carries the live CLI and config together, including a tree
installed directly in code.
A script CLI is different: the script source is embedded in the snapshot and
its opaque config is serialized verbatim because there is no config model to
identify secrets. Keep credentials in the environment, or provide fresh script
config through clis when loading.
Script CLIs
Usescript: when the program should own its grammar or should have no Mirage
imports:
MIRAGE_CLI_CONFIG. The
program’s stdout, stderr, and exit status become the shell result. A script CLI
cannot declare configModel, subcommands, or function handlers; it parses and
documents its own options, including --help.
Slot 0 is the installed name on pyodide, monty, and quickjs, so two installs can
identify themselves and user-facing errors can name the invoked program. The
Node-only local runtime runs CPython with -c, which owns sys.argv[0];
arguments still begin at index 1. A YAML script declares no Mirage grammar, so
every option reaches the program unchanged. A script CLI constructed in code
may supply a CommandSpec to opt into Mirage parsing and generated help.
Runtime argument spellings differ:
The sandboxed runtimes provide files plus compute, not arbitrary networking or
Node builtins. The Node-only
local runtime selects the host interpreter when
that broader authority is intentional.
Authoring reference
The public constructors document exact types and defaults:CLI and CLIHandler,
and CommandSpec and Argument.
CLI and CLIHandler
CommandSpec
Argument
This follows argparse’s naming conventions and supports a subset of its
features. Positional defaults and variadic options are unsupported;
append
collects single option values, while extend flattens fixed groups.