GitHub

Modern Zod-based CLI builder, fully type-safe, super lightweight and flexible.

Highlights

  • ðŸŽŊ Full TypeScript support with robust type inference
  • ðŸ›Ąïļ Runtime validation using Zod
  • 🔄 Supports command aliases and nested commands
  • 🌟 Global and command-specific options
  • ðŸ“Ķ Shareable command presets
  • ðŸŽĻ Flexible configuration API
  • ðŸŠķ Lightweight with zero runtime dependencies (except Zod)

Installation

npm install zodest zod
# or
yarn add zodest zod
# or
pnpm add zodest zod
# or
bun add zodest zod

Quick Example

import { z } from 'zod';
import { defineCommand, defineConfig, defineOptions } from 'zodest/config';
import { processConfig } from 'zodest';
const globalOptions = defineOptions(
  z.object({
    verbose: z.boolean().default(false),
  }),
);
const config = defineConfig({
  globalOptions,
  commands: {
    serve: defineCommand({
      description: 'Start development server',
      options: defineOptions(
        z.object({
          port: z.coerce.number().min(1024).default(3000),
        }),
        { p: 'port' }, // aliases, type-safe with the schema
      ),
      args: z.array(z.string()),
      action(options, args) {
        console.log('Server starting on port', options.port);
      },
    }),
  },
});
// Process CLI arguments, returns the matching command
const result = processConfig(config, process.argv.slice(2));
console.log('result:', result._kind, result);
// => result._kind == 'serve', the matching command's name
// in `result` there's also `result.globalOptions`, and so on, all type-safe
// call the command
result.command.action(result.options, result.args);

Core Concepts

Commands

Commands are the basic building blocks. Each command can have:

  • Description
  • Options (with Zod validation)
  • Arguments (with Zod validation)
  • Aliases
  • Action function
const myCommand = defineCommand({
  description: 'My command description',
  options: defineOptions(
    z.object({
      force: z.boolean().default(false),
    }),
  ),
  args: z.string().min(3),
  aliases: ['mc', 'mycmd'],
  action(options, args) {
    // Fully typed options and args
  },
});

Global Options

Shared options across all commands:

const globalOptions = defineOptions(
  z.object({
    debug: z.boolean().default(false),
    config: z.string().optional(),
  }),
  {
    d: 'debug', // aliases
    c: 'config',
  },
);

Nested Commands

Support for namespaced commands:

const config = defineConfig({
  commands: {
    'user add': defineCommand({...}),
    'user remove': defineCommand({...}),
    'user list': defineCommand({...}),
  }
});

Shareable Command Presets

Create reusable command sets:

import { withGlobalOptions } from 'zodest/config';
const userCommands = withGlobalOptions(globalOptions, (gopts) => ({
  userAdd: defineCommand({...}),
  userRemove: defineCommand({...}),
}));
// Use in any CLI
const config = defineConfig({
  globalOptions,
  commands: (gopts) => ({
    ...userCommands(gopts),
    // Add more commands
  })
});

API Reference

defineConfig(config)

Creates a type-safe CLI configuration.

defineCommand(command)

Defines a new command with options, arguments, and an action.

defineOptions(schema, aliases?)

Creates a type-safe options definition with optional aliases.

processConfig(config, argv)

Processes command line arguments against the configuration.

withGlobalOptions(globalOptions, commands)

Creates a command preset with access to global options.

getCommands(config)

Returns all available commands from the configuration.

Advanced Examples

Command with Complex Arguments

const command = defineCommand({
  description: 'Complex command',
  options: defineOptions(
    z.object({
      mode: z.enum(['development', 'production']),
      port: z.coerce.number().min(1024),
    }),
  ),
  args: z.tuple([z.string(), z.coerce.number()]),
  action(options, [path, num]) {
    // path is string, num is number
  },
});

Default Commands

NOTE: mixing grouped commands with non-grouped ones breaks the defaultCommand inference of the grouped commands, so you must use as any to mute TypeScript, or use a command from non-grouped commands

const config = defineConfig({
  defaultCommand: 'serve',
  commands: {
    serve: defineCommand({...})
    dev: defineCommand({...})
  }
});

Command Groups

const devCommands = withGlobalOptions(globalOptions, (gopts) => ({
  serve: defineCommand({...}),
  build: defineCommand({...}),
}));
const dbCommands = withGlobalOptions(globalOptions, (gopts) => ({
  'db migrate': defineCommand({...}),
  'db seed': defineCommand({...}),
}));
const config = defineConfig({
  globalOptions,
  commands: (gopts) => ({
    ...devCommands(gopts),
    ...dbCommands(gopts),
  })
});

License

Apache-2.0

Read the original on github.com ↗