Skip to content
zodline
Esc
navigateopen⌘Jpreview
Zero runtime dependencies · ESM only

Type-safe CLIs.
Declared as schemas.

Declare your commands, options, and positional arguments as Zod schemas. zodline parsesprocess.argv, validates it against them, and returns a fully typed result.

$ npm install zodline zod
import { z } from 'zod';
import { defineCommand, defineOptions } from 'zodline';

export const greet = defineCommand({
  description: 'Greet someone',
  options: defineOptions(
    z.object({
      name: z.string().describe('Name to greet'),
      loud: z.boolean().default(false).describe('Use uppercase'),
    }),
    { n: 'name', l: 'loud' },
  ),
  action: async (options) => {
    const greeting = `Hello, ${options.name}!`;
    console.log(options.loud ? greeting.toUpperCase() : greeting);
  },
});

Requirements

Node.js ≥ 16Zod ^4.0.0ESM onlyZero dependencies
Types includedStandard library APIs onlyMIT licensed

Type-safe

Option and argument types come straight from your Zod schemas. No manual generics.

Zero runtime dependencies

Zod is the only peer dependency. Nothing else is installed alongside it.

Flexible flag parsing

--flag, -f, --flag=value, --flag value, and short-flag clustering like -abc.

Kebab to camel

Pass --android-max and the schema key androidMax receives the value.

Aliases

Map any short flag to a schema key with the alias record on defineOptions.

Array normalization

A single value for an array field is wrapped in an array before validation.

Generated help

--help and --version are built from your schemas and their .describe() text.

Strict by default

An unknown option raises a ZodlineError instead of being silently dropped.

Parsing and execution stay separate

processConfig returns { command, options, args }and stops there. Running the action is your call, which leaves every action a plain function over typed values: a test invokes it with an object literal, no argv and no process involved.Positional arguments arrive as the second parameter, validated by the command's args schema.

Why parsing and execution are separate
// processConfig parses and validates. It never runs your action.
const result = processConfig(config, process.argv.slice(2));
await result.command.action(result.options, result.args);

// Which leaves the action a plain function a test can call directly.
test('greet shouts when loud is set', async () => {
  await greet.action({ name: 'World', loud: true }, undefined);
  expect(stdout).toContain('HELLO, WORLD!');
});

Every flag form, normalized

zodline accepts the forms people actually type and resolves them to your schema keys before validation runs.

You typeSchema fieldoptions receives
--name Worldname: z.string(){ name: 'World' }
--name=Worldname: z.string(){ name: 'World' }
-n Worldalias { n: 'name' }{ name: 'World' }
--loudloud: z.boolean(){ loud: true }
-abcthree boolean fields{ a: true, b: true, c: true }
--tag xtag: z.array(z.string()){ tag: ['x'] }
--tag x --tag ytag: z.array(z.string()){ tag: ['x', 'y'] }
--android-max 34androidMax: z.string(){ androidMax: '34' }
See the full flag syntax reference

Start here