@arki/cli

next

for @arki/[email protected] · Tier 1: the stack · MIT

Typed command-line programs from one defineCommand({...}).

Version 0.1.0 next

Install

0.1.0 is not on npm yet. No version is on npm yet.

Release health for @arki/[email protected]

Doc examples

7 compiled2 run1 skipped

Why 1 example was skipped
  • README.md:120
    a bin entry: it calls process.exit

Open gaps

no open gaps

Every release check passes and no demo has reported a gap.

Used by demos

Runtimes

No engines field, so no runtime requirement is declared.

Benchmarks

No benchmarks are published for this version.

README

Typed command-line programs from one defineCommand({...}). The input schema drives the flags, their coercion and their help. A run writes a JSON envelope when stdout is a pipe and text on a terminal. Exit codes are stable. Help and --version never run a command, and import at most one lazy level of the tree.

It runs on Node, Bun and Deno. It is not a prompt or color library, and it does not configure logging: the bin file does that.

 argv ─▶ parse flags (from the input schema) ─▶ validate ─▶ run(input)
                                                              │
        terminal: text  ◀── render ◀── data or failure ───────┤
        pipe:     JSON envelope (arki.cli/1) + exit code ◀────┘

Install

bun add @arki/cli

Bring any Standard Schema validator that can produce JSON Schema (Zod 4, Valibot with its JSON Schema bridge, ArkType). The examples use Zod.

Quick start

One command, one program, one run. main takes the argument list and the output functions, so the whole run happens in-process and returns the exit code:

import { z } from 'zod';
 
import { defineCli, defineCommand, group } from '@arki/cli';
 
const greet = defineCommand({
  describe: 'Greet someone',
  input: z.object({ name: z.string().default('world').describe('Who to greet') }),
  run: ({ input }) => ({ greeting: `Hello, ${input.name}` }),
});
 
const program = defineCli({
  name: 'app',
  version: '1.0.0',
  commands: { greet, admin: group('Administration', { greet }) },
});
 
let stdout = '';
const exitCode = await program.main(['greet', '--name', 'Ada'], {
  stdout: text => {
    stdout += text;
  },
  tty: false,
});
const envelope: unknown = JSON.parse(stdout);
if (exitCode !== 0 || !JSON.stringify(envelope).includes('Hello, Ada')) throw new Error(stdout);

main(argv, io) never exits the process. run(program) is the only process boundary (see Programs).

When not to use this

  • You need prompts, colors or spinners. This package parses, validates, runs and prints. Pair it with a prompt library for interactive input.
  • You are building a full-screen terminal UI. Use a TUI library; a command here returns data, it does not own the screen.
  • The script takes one flag. parseArgs from node:util is shorter.
  • Your validator cannot produce JSON Schema. Flags, coercion and help come from the input schema's JSON Schema. Zod 4, Valibot with its JSON Schema bridge and ArkType can produce it.

Runtime support

It runs on Node, Bun and Deno. main(argv, io) writes only to the io functions you pass and never exits the process. run(program) reads process.argv, wires stdout, stderr, the terminal and SIGINT/SIGTERM, and exits with the code. bun run test:runtimes checks the built dist/ on each runtime.

Public imports

One entry, @arki/cli:

ExportWhat it is
defineCommandOne command: input schema, run, optional preview and render.
groupA named branch of commands.
defineCliThe program: name, version and the command tree.
runThe process boundary: argv in, exit code out.
ExitCodeThe stable exit codes, by name.
UnknownCommand, InvalidInput, PreviewUnsupported, InputRequired, ConfirmationRequired, CancelledThe framework failures (cli.*).
types Command, RunContext, InputOf, OutputOf, Program, Io, Ui, Envelope, …Inferred from your definitions; write no type arguments.

Commands

One file per command. input is an object schema; each key becomes a --kebab-case flag. run gets the validated input (defaults filled, transforms applied) and returns the data, or a failure from @arki/failure.

import { z } from 'zod';
 
import { defineCommand } from '@arki/cli';
 
export default defineCommand({
  describe: 'Seed the embeddings',
  input: z.object({
    maxBatches: z.int().min(1).optional().describe('Stop after this many batches'),
    yes: z.boolean().default(false).describe('Skip the confirmation'),
  }),
  args: ['maxBatches'],
  run: ({ input }) => ({ embedded: (input.maxBatches ?? 1) * 100 }),
});

args names the keys taken as positionals, in order. Booleans take --yes and --no-yes. Numbers are coerced, and 2junk fails. An array key takes a repeated flag or a comma list. Unknown flags fail, and so does a scalar given twice.

Programs

Register each command in the tree with one line. A lazy entry (() => import(...)) is loaded only when its branch is taken.

At the process boundary, run(program) reads process.argv, wires stdout, stderr, the terminal and SIGINT/SIGTERM, and exits with the code:

import { defineCli, run } from '@arki/cli';
 
await run(
  defineCli({
    name: 'app',
    version: '1.0.0',
    commands: { seed: () => import('./commands/seed.js') },
  }),
);

Output

Where stdout goesDefault modeOverride
A terminaltext-o json, --json, ARKI_OUTPUT=json
A pipe, file or agentjson-o text, ARKI_OUTPUT=text

-o ndjson writes one line per element of data.items, then the envelope without data. --fields a,b.c keeps only those paths of data, in every mode.

The envelope (arki.cli/1) always has every key:

{
  "ok": false,
  "command": ["seed"],
  "data": null,
  "error": {
    "code": "cli.invalid_input",
    "message": "--max-batches expected an integer, received \"big\"",
    "remediation": "Fix the flags named in the issues; run with --help to see them.",
    "issues": [
      {
        "path": ["maxBatches"],
        "flag": "--max-batches",
        "message": "--max-batches expected an integer, received \"big\""
      }
    ],
    "data": null,
    "retryable": false,
    "outcome": "unchanged",
    "exitCode": 2,
    "docsUrl": null
  },
  "meta": { "schema": "arki.cli/1", "version": "1.0.0", "durationMs": 0, "dryRun": false }
}

In text mode a command can draw its own output with render(data, ui). Without it, an object prints as key: value lines and an array of objects as a table. A failure goes to stderr.

Exit codes

CodeExitCodeWhen
0okrun succeeded; help; --version
1bugsomething threw: cli.bug, message only, the stack goes to the log
2usagecli.unknown_command, cli.invalid_input, cli.preview_unsupported
3failurerun returned a failure
4retryrun returned a failure with status 429 or 503
5inputRequiredcli.input_required, cli.confirmation_required
130interruptedcli.cancelled: the run ended after SIGINT/SIGTERM

Global flags

Every command takes -o, --output <auto|text|json|ndjson>, --json, --fields, --input-json <object>, --input-file <path|->, -q, --quiet, --no-color, --dry-run, -h, --help and --version. An input key may not use one of these names; defineCli throws and names the key.

--input-json and --input-file replace the flags: the whole input as one JSON object. Mixing them with flags is invalid input. --dry-run runs the command's preview instead of run and sets meta.dryRun; a command without preview fails with exit 2.

Failure behaviour

A command never throws on purpose. run returns data or a failure value, and the program turns it into the envelope and an exit code. Anything that does throw is a bug: exit 1, code cli.bug, the message only, and the stack goes to the log.

run returns failures as values, with @arki/failure:

import { z } from 'zod';
 
import { defineCommand } from '@arki/cli';
import { defineFailure } from '@arki/failure';
 
const ProviderDown = defineFailure('embed.provider_down', {
  status: 503,
  message: 'provider {provider} is not answering',
  remediation: 'Start the provider, then run again.',
  data: z.object({ provider: z.string() }),
});
 
export default defineCommand({
  describe: 'Embed one text',
  input: z.object({ text: z.string() }),
  run: async ({ input }) => {
    const answered = await Promise.resolve(input.text.length > 0);
    return answered ? { vectors: 1 } : ProviderDown({ provider: 'local' });
  },
});

The output type is what run succeeds with: the failure is removed from it. Status 429 or 503 makes the failure retryable (exit 4); any other returned failure exits 3.

The framework's own failures:

CodeExitWhenFix
cli.unknown_command2The command name is not one at this level.Pick a name from .error.data.suggestions.
cli.invalid_input2The command line does not fit the input schema.Fix the flags named in .error.issues.
cli.preview_unsupported2--dry-run on a command without preview.Run it without --dry-run.
cli.input_required5A required value is missing and there is no terminal.Pass the flag in .error.data.flag.
cli.confirmation_required5The command needs confirmation and there is no terminal.Pass --yes.
cli.cancelled130SIGINT or SIGTERM ended the run.Check what it changed, then run it again.

Limits

  • Strict parsing. Unknown flags fail, and so does a scalar flag given twice. Numbers are coerced strictly: --max-batches 2junk is cli.invalid_input with exit 2, not the number 2.
  • Reserved names. An input key may not use a global flag name (output, json, fields, quiet, dryRun, help, version, …). defineCli throws and names the key.
  • Flags or JSON, not both. --input-json and --input-file replace the flags. Mixing them with flags is cli.invalid_input.
  • Schema defaults apply in validation. The parser never fills them. A schema without a default leaves the key undefined.
  • No terminal, no questions. Without a terminal, a missing value or a confirmation fails with exit 5 instead of waiting for input.

Documentation

  • Agent guide: add a command, group commands, return a failure, offer a dry run, test a command.
  • llms.txt: the map for agents.
  • AGENTS.md: the laws for changing this package.
  • CHANGELOG.md: what changed in each version.

Support

Report bugs and ask questions at github.com/arki-labs/arki/issues. Include the command line, the JSON envelope (--json) and the exit code.

License

MIT — see LICENSE.

Docs 3 pages in 0.1.0

Public imports 1 path · 40 exports

Import pathExportsFirst names
@arki/cli40 exportsAnyCommand, AnyRunContext, Cancelled, Cli, …
All 40 names
  • AnyCommand type
  • AnyRunContext type
  • Cancelled variable
  • Cli type
  • Command type
  • CommandTree type
  • ConfirmationRequired variable
  • defineCli function
  • defineCommand function
  • Effects type
  • Envelope type
  • EnvelopeError type
  • EnvelopeMeta type
  • ExitCode variable
  • ExitCodeValue type
  • FailureEnvelope type
  • group function
  • Group type
  • InputIssue type
  • InputKeys type
  • InputOf type
  • InputRequired variable
  • InvalidInput variable
  • Io type
  • Mode type
  • NoInput type
  • NoServices type
  • OkEnvelope type
  • Outcome type
  • OutputOf type
  • PreviewUnsupported variable
  • Program type
  • run function
  • RunContext type
  • ServicesOf type
  • Success type
  • TreeEntry type
  • TreeNode type
  • Ui type
  • UnknownCommand variable

Every export with its signature, as JSON