Skip to content
zodline
Esc
navigateopen⌘Jpreview
On this page

Commands

Define commands with defineCommand, register them in defineConfig, and set a default command.

A command is the unit zodline dispatches on. defineCommand describes one; defineConfig registers it under the name users type.

Defining a command

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

export const build = defineCommand({
  description: 'Build the project',
  options: defineOptions(
    z.object({
      watch: z.boolean().default(false).describe('Rebuild on change'),
    }),
    { w: 'watch' },
  ),
  action: async (options) => {
    console.log(options.watch ? 'Watching...' : 'Building once');
  },
});
PropType
description?string

Shown next to the command in the help screen.

Typestring
options?OptionsDefinition

The result of defineOptions(). Omit for a command with no flags.

TypeOptionsDefinition
args?z.ZodType

Schema for the positional arguments array. Omit to receive them unvalidated.

Typez.ZodType
action(options, args) => void | Promise<void>

What the command does. Never called by zodline itself.

Type(options, args) => void | Promise<void>

defineCommand is a typing helper — it returns its input unchanged. Its job is to carry the Zod generics so options and args inside action are inferred instead of any.

Registering commands

The keys of commands are the names users type. They can contain any characters a shell will pass through, which makes namespaced names practical:

import { defineConfig } from 'zodline';

export const config = defineConfig({
  meta: {
    name: 'my-cli',
    version: '1.0.0',
    description: 'Does useful things',
  },
  commands: {
    build,
    'apps:bundles:create': createBundle,
  },
});

Commands without options

options is optional. Leave it out and action receives an empty object:

const version = defineCommand({
  description: 'Print the resolved toolchain version',
  action: async () => {
    console.log(process.version);
  },
});

Default command

defaultCommand runs when the user passes no command name at all:

export const config = defineConfig({
  meta: { name: 'my-cli', version: '1.0.0' },
  commands: { build, serve },
  defaultCommand: build,
});
my-cli --watch    # runs build
my-cli build      # also runs build
my-cli nope       # throws: Unknown command: nope

Two behaviours to keep in mind:

  • --help and --version are handled before the default command, so my-cli --help still prints the command list rather than running build.
  • The default command is only reached when there are no positional arguments, so it always receives an empty args array. A command that needs positional arguments has to be invoked by name.

Without a defaultCommand, running the CLI bare prints the help screen and throws ZodlineError: No command specified.

Next: Options

Schemas, aliases, kebab-case, arrays and defaults.

Last updated on August 6, 2026

Was this page helpful?