Pikku CLI Wiring
This skill helps you wire Pikku functions to command-line interfaces with nested commands, positional parameters, options, and custom output renderers.
Overview​
CLI wiring transforms Pikku functions into command-line programs with:
- Nested commands and subcommands
- Positional parameters (
<required>
and[optional]
) - Named options/flags with short forms
- Custom output renderers for formatted console output
- Option inheritance from global → command → subcommand
- Smart type plucking (functions receive only declared options)
**Domain logic stays in packages/functions/src/functions/**/\*.function.ts
.**
File Naming Rules​
- CLI wiring files:
*.cli.ts
- Render files:
*.render.ts
(optional, can be inline) - One
wireCLI
per program (can have multiple programs)
Examples:
packages/functions/src/my-tool.cli.ts
packages/functions/src/utils.render.ts
Imports​
From wiring files:
âś… Allowed:
wireCLI
,pikkuCLICommand
from./pikku-types.gen.js
pikkuCLIRender
from@pikku/core
- Exported Pikku functions from
./functions/**/*.function.ts
middleware
from./middleware.ts
permissions
from./permissions.ts
❌ Never:
- Import from
./services/**
- Implement business logic in wiring files
Basic CLI Wiring​
import { wireCLI, pikkuCLICommand } from './pikku-types.gen.js'
import { pikkuCLIRender } from '@pikku/core'
import { greetUser } from './functions/greet.function.js'
wireCLI({
program: 'my-tool',
commands: {
greet: pikkuCLICommand({
parameters: '<name>',
func: greetUser,
description: 'Greet a user',
options: {
loud: {
description: 'Use uppercase',
short: 'l',
default: false,
},
},
}),
},
})
Usage:
my-tool greet Alice
my-tool greet Alice --loud
my-tool greet Alice -l
Positional Parameters​
Use <required>
and [optional]
syntax in the parameters
string:
pikkuCLICommand({
parameters: '<package> [version]', // package is required, version is optional
func: installPackage,
})
Positionals are mapped to function input:
type InstallInput = {
package: string // from <package>
version?: string // from [version]
}
Options​
Options are CLI flags/options with optional short forms:
options: {
force: {
description: 'Force overwrite',
short: 'f',
default: false,
},
registry: {
description: 'NPM registry URL',
short: 'r',
default: 'https://registry.npmjs.org',
},
}
Usage:
my-tool install express --force
my-tool install express -f
my-tool install express --registry https://custom.registry
Nested Commands (Subcommands)​
Commands can have subcommands for hierarchical CLI structures:
wireCLI({
program: 'my-tool',
commands: {
user: {
description: 'User management',
subcommands: {
create: pikkuCLICommand({
parameters: '<username> <email>',
func: createUser,
description: 'Create a user',
}),
list: pikkuCLICommand({
func: listUsers,
description: 'List users',
}),
},
},
},
})
Usage:
my-tool user create alice alice@example.com
my-tool user list
Renderers​
Renderers transform function output for console display. Use pikkuCLIRender
to create CLI-specific renderers.
See examples/greet.render.ts
for renderer examples.
Renderer Levels​
- Command-specific renderer - Defined on command
- Global renderer - Defined on program (fallback)
- Default JSON renderer - Built-in fallback
// Global renderer (applies to all commands without specific renderer)
wireCLI({
program: 'my-tool',
render: pikkuCLIRender((services, output) => {
console.log(JSON.stringify(output, null, 2))
}),
commands: {
greet: pikkuCLICommand({
func: greetUser,
// Command-specific renderer (overrides global)
render: pikkuCLIRender<{ message: string }>((_services, output) => {
console.log(output.message)
}),
}),
},
})
Renderer Signature​
pikkuCLIRender<OutputType>(
(services, output, session?) => {
// Format and output to console
console.log(...)
}
)
services
- Singleton services (logger, config, etc.)output
- Function outputsession
- Optional user session (ifauth: true
)
Option Inheritance​
Options cascade from global → command → subcommand, with CLI arguments taking highest priority:
wireCLI({
program: 'my-tool',
// Global options (inherited by all commands)
options: {
verbose: {
description: 'Verbose output',
short: 'v',
default: false,
},
},
commands: {
install: pikkuCLICommand({
func: installPackage,
// Command options (inherited by subcommands)
options: {
force: {
description: 'Force overwrite',
short: 'f',
default: false,
},
},
}),
},
})
Resolution priority (highest to lowest):
- CLI arguments (
--verbose
,-v
) - Command/subcommand defaults
- Global defaults
Smart Type Plucking​
Functions only receive options they declare in their type signature:
// Function declares what it needs
const installPackage = pikkuFunc<
{
package: string // positional
version?: string // positional
force?: boolean // option (plucked if available)
},
InstallResult
>({ ... })
// Available options through inheritance
// { verbose: boolean, force: boolean, registry: string }
// Function receives ONLY: { package, version?, force? }
// verbose and registry are NOT passed even though available
Middleware and Permissions​
Apply middleware and permissions to CLI commands:
wireCLI({
program: 'my-tool',
// Global middleware (applies to all commands)
middleware: [loggingMiddleware],
commands: {
deploy: pikkuCLICommand({
func: deployApp,
auth: true, // Requires authentication
permissions: {
admin: requireAdmin, // Requires admin permission
},
// Command-specific middleware
middleware: [auditMiddleware],
}),
},
})
Middleware must guard for CLI interaction:
const cliMetrics = pikkuMiddleware(async ({ logger }, interaction, next) => {
if (!interaction.cli) {
throw new InvalidMiddlewareInteractionError(
'cliMetrics middleware can only be used with CLI interactions'
)
}
logger.info('cli.command', {
program: interaction.cli.program,
command: interaction.cli.command.join(' '),
})
await next()
})
Complete Example​
See examples/complete.cli.ts
for a full CLI program with:
- Nested commands
- Positional parameters
- Options with inheritance
- Custom renderers
- Middleware
Channel Integration​
CLI commands can use channels for real-time output (progress bars, streaming, etc.):
const buildApp = pikkuFunc<BuildInput, BuildResult>({
func: async ({ logger, channel }, data) => {
// Stream progress updates via channel
if (channel) {
await channel.send({ type: 'progress', percent: 25 })
await channel.send({ type: 'progress', percent: 50 })
await channel.send({ type: 'progress', percent: 100 })
}
return { success: true }
},
})
wireCLI({
program: 'my-tool',
commands: {
build: pikkuCLICommand({
func: buildApp,
render: buildRenderer, // Can listen to channel messages
}),
},
})
The interaction.cli
object contains:
program
- Program namecommand
- Command path array (e.g.,['user', 'create']
)data
- All positionals and options mergedchannel
- PikkuChannel for real-time output
Best Practices​
- Keep functions transport-agnostic - Functions should work via HTTP, CLI, or any transport
- Use renderers for formatting - Don't console.log in functions, use renderers
- Leverage type inference - Let TypeScript infer option types from function signatures
- Group related commands - Use subcommands for logical grouping
- Provide descriptions - Help text is generated from descriptions
- Use short forms - Single-letter shortcuts for common options
Examples​
See the examples/
directory for:
- Basic command wiring
- Nested commands with subcommands
- Custom renderers for different output formats
- Option inheritance and type plucking
- Middleware and permissions for CLI