The Basics
Function to command
Wire your functions to CLI commands with typed parameters, options, and auto-generated help.
wireCLI({
program: 'todos',
commands: {
add: pikkuCLICommand({
parameters: '<text>',
func: createTodo,
description: 'Add a new todo',
render: todoRenderer,
options: {
priority: {
description: 'Set priority',
short: 'p',
default: 'normal',
choices: ['low', 'normal', 'high'],
}
}
}),
list: pikkuCLICommand({
func: listTodos,
description: 'List all todos',
render: todosRenderer,
options: {
completed: {
description: 'Show completed only',
short: 'c',
default: false,
}
}
}),
}
})
$ todos add "Buy milk" --priority high
✓ Created: Buy milk (priority: high)
$ todos list --completed
1. Write docs ✓
2. Ship feature ✓
$ todos --help
Usage: todos <command> [options]
Commands:
add <text> Add a new todo
list List all todos
Typed parameters
<required> [optional] [variadic...] — parsed and validated automatically
Options & flags
Long flags, short aliases, defaults, choices — all from a plain config object
Auto help generation
--help is generated from your descriptions — no manual maintenance
Subcommands
Nested command trees
Group related commands under namespaces. Global options cascade down to every subcommand.
wireCLI({
program: 'app',
options: {
verbose: { description: 'Verbose output', short: 'v', default: false },
},
commands: {
// Simple top-level command
greet: pikkuCLICommand({
parameters: '<name>',
func: greetUser,
render: greetRenderer,
}),
// Nested subcommands
user: {
description: 'User management',
subcommands: {
create: pikkuCLICommand({
parameters: '<username> <email>',
func: createUser,
render: userRenderer,
options: {
admin: { description: 'Admin role', short: 'a', default: false }
}
}),
list: pikkuCLICommand({
func: listUsers,
render: usersRenderer,
options: {
limit: { description: 'Max results', short: 'l' },
}
}),
}
},
}
})
Compose across files. Use defineCLICommands() to define command groups in separate files, then import and compose them in one wireCLI call.
Renderers
Custom output formatting
Separate your display logic from your business logic. Each command gets a typed renderer that formats the function's output.
const todoRenderer = pikkuCLIRender<{ todo: Todo }>(
(_services, { todo }) => {
console.log(`✓ Created: ${todo.text} (priority: ${todo.priority})`)
}
)
const todosRenderer = pikkuCLIRender<{ todos: Todo[] }>(
(_services, { todos }) => {
todos.forEach((t, i) => {
const check = t.completed ? '✓' : ' '
console.log(` ${i + 1}. ${t.text} ${check}`)
})
}
)
// Fallback: JSON renderer for commands without custom render
wireCLI({
program: 'todos',
render: jsonRenderer, // Default for all commands
commands: {
add: pikkuCLICommand({
func: createTodo,
render: todoRenderer, // Override per command
}),
}
})
Typed from output
pikkuCLIRender<T> is typed from your function's return type — the data parameter is fully autocompleted.
Cascading fallback
Set a default renderer on the program. Override per-command. Commands without a custom renderer use the fallback.
Execution Modes
Local or remote
Same wireCLI config generates both a local executable and a remote client that connects over WebSocket.
Local CLI
Runs in-process. Direct access to all services — databases, caches, file system. Best for dev tools and admin scripts.
Remote CLI
Connects over WebSocket to your server. Same commands, but execution happens server-side. Best for production admin tools.
Same commands, different execution. Local CLI runs functions directly. Remote CLI sends the parsed command over WebSocket and streams back the rendered output.
Start wiring CLI tools in 5 minutes
One command to scaffold a project with CLI wiring already configured.
MIT Licensed · Local & remote execution modes