Command
Declarative command trees with schema-validated flags and a dispatching registry.
@ozaco/cli/command@ozaco/cli/command turns a tree of plain descriptors into a working CLI. You describe leaf subcommands with defineAction and group them with defineCommand; both are pure metadata — no plugin is built at definition time. The DefaultRegistry compiles a registered tree into a path-identified plugin tree and installs each level lazily as dispatch descends into it.
An action carries an input schema (any Standard Schema — zod, valibot, arktype), which both types the handler’s ctx (StandardSchemaV1.InferOutput) and drives parsing. The short map assigns short flags (e.g. { message: 'm' }) and args lists the fields fillable positionally. The runner tokenizes argv, validates it against the schema, and only then calls the handler — parse or validation problems surface as CommandErrors failures rather than exceptions.
Because compiled commands are keyed by their tree path (e.g. kube.config), distinct commands never collide in the shared scope even when they share a human-facing name. --help/-h and --version/-V are handled by the runner.
Defining commands
Pure descriptors for leaf actions and the command tree.
defineCommand
Describe a command and its members (nested commands + leaf actions).
function defineCommand<TContext = unknown, TArgs extends unknown[] = []>(
options: CommandDef.Options<TContext, TArgs>,
): CommandDef.Spec<TContext, TArgs>Returns a pure Spec, not a plugin. options.actions mixes leaf actions (from defineAction, collected into leaf) and nested command specs (collected into subs, keyed by the token you type). An optional setup builds a context installed when dispatch enters the command.
import { defineAction, defineCommand } from '@ozaco/cli/command'
import { z } from 'zod'
const greet = defineAction(
{ description: 'Greet someone', input: z.object({ name: z.string() }), args: ['name'] },
function* (ctx) {
console.log(`Hello, ${ctx.name}`)
},
)
const app = defineCommand({
name: 'app',
actions: { greet },
})defineAction
Define a leaf subcommand: a handler carrying its parse metadata.
function defineAction<S extends StandardSchemaV1, R>(
config: CommandDef.ActionConfig<S> & { input: S },
handler: (ctx: StandardSchemaV1.InferOutput<S>) => Operation<R>,
): CommandDef.Action<S, R>
function defineAction<R>(
config: Omit<CommandDef.ActionConfig, 'input'>,
handler: (ctx: EmptyType) => Operation<R>,
): CommandDef.Action<unknown, R>The handler is a generator operation; its ctx is the parsed, validated options+args object inferred from input. With no input, ctx is empty. config.short maps schema fields to short flags and config.args lists fields fillable positionally, in order.
import { defineAction } from '@ozaco/cli/command'
import { z } from 'zod'
const build = defineAction(
{
input: z.object({ watch: z.boolean().default(false) }),
short: { watch: 'w' },
},
function* (ctx) {
if (ctx.watch) startWatcher()
},
)Registry
Compiles and dispatches command trees (mirrors the backend `Broker`).
DefaultRegistry
The command registry: register command trees, then run argv against them.
const DefaultRegistry: Plugin<
RegistryDef.Context,
[options?: RegistryDef.Options],
RegistryDef.Actions
>Install it (optionally with { name, version, description }), register each top-level command — which compiles it and runs its own setup — then run(argv) dispatches argv[0] to the matching command tree. get(name) looks a registered command up.
import { install } from '@ozaco/std/plugin'
import { Registry } from '@ozaco/cli/core'
import { DefaultRegistry } from '@ozaco/cli/command'
yield* install(DefaultRegistry, { name: 'app', version: '1.0.0' })
yield* Registry.actions.register(app)
yield* Registry.actions.run(process.argv.slice(2))Errors
CommandErrors
The `cli:command.*` failure tags raised during parse and validation.
const CommandErrors: {
Parse: 'cli:command.parse'
Required: 'cli:command.required'
Invalid: 'cli:command.invalid'
Unknown: 'cli:command.unknown'
}The runner fails with Parse on malformed argv, Required for a missing required field, Invalid when the schema rejects a value, and Unknown for an unrecognized command or flag.
Types
CommandDef
The command descriptor types: config, action, spec and resolved option metadata.
namespace CommandDef {
type Infer<S> // parsed ctx type inferred from an action's input schema
interface ActionConfig<S extends StandardSchemaV1> {
description?: string
input?: S
short?: Record<string, string>
args?: readonly string[]
}
type Action<S, R> = ActionMeta & ((ctx: Infer<S>) => Operation<R>)
type Member = Action | Spec
interface Options<TContext, TArgs> {
name: string
version?: string
description?: string
actions: Record<string, Member>
setup?: (...args: TArgs) => Operation<TContext>
}
interface Spec<TContext, TArgs> {
name: string
leaf: Record<string, Action>
subs: Record<string, Spec>
setup?: (...args: TArgs) => Operation<TContext>
}
}The CommandDef namespace also exposes:
- •
ActionMeta— the metadatadefineActionattaches to a handler (_t,input,short,args,description). - •
Built— aSpeccompiled into a path-identified plugin (identity likekube.config). - •
OptionInfo— resolved option metadata derived from aninputschema (name,type,array,enum?,required,hasDefault) used for tokenizing and help.