Skip to main content
Wire Type: CLI

CLI tools,
same functions.

wireCLI turns your Pikku functions into type-safe CLI commands with automatic argument parsing, subcommands, and custom renderers.

The Basics

Function to command

Wire your functions to CLI commands with typed parameters, options, and auto-generated help.

cli.wiring.tswiring.ts
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,
}
}
}),
}
})
Terminaloutput
$ 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.

cli.wiring.ts
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.

renderers.ts
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.

$ npm create pikku@latest

MIT Licensed  ·  Local & remote execution modes