.claude/skills/pikku-cli/SKILL.md
Use when building CLI commands with Pikku. Covers wireCLI, pikkuCLICommand, subcommands, options, parameters, custom renderers, and nested command groups. TRIGGER when: code uses wireCLI/pikkuCLICommand, user asks about CLI commands, terminal tools, command-line interface, or adding subcommands. DO NOT TRIGGER when: user asks about the pikku CLI tool itself (use pikku-info) or HTTP endpoints (use pikku-http).
npx skillsauth add pikkujs/pikku pikku-cliInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Wire Pikku functions as CLI commands with parameters, options, subcommands, and custom terminal renderers.
pikku info functions --verbose # See existing functions and their types
pikku info tags --verbose # Understand project organization
See pikku-concepts for the core mental model.
wireCLI(config)import { wireCLI } from '@pikku/core/cli'
wireCLI({
program: string, // Program name (e.g. 'todos')
options?: { // Global options
[key: string]: {
description: string,
short?: string, // Single char alias (e.g. 'v')
default?: any,
}
},
render?: PikkuCLIRender, // Default renderer for all commands
commands: {
[name: string]: PikkuCLICommand | {
description: string,
subcommands: { [name: string]: PikkuCLICommand }
}
},
})
pikkuCLICommand(config)import { pikkuCLICommand } from '#pikku'
pikkuCLICommand({
parameters?: string, // Positional args (e.g. '<text>', '<username> <email>')
func: PikkuFunc, // Business logic function
description?: string,
render?: PikkuCLIRender, // Custom output renderer
options?: {
[key: string]: {
description: string,
short?: string,
default?: any,
choices?: string[], // Restrict to values
}
},
})
pikkuCLIRender(fn)import { pikkuCLIRender } from '@pikku/core/cli'
const renderer = pikkuCLIRender<OutputType>((services, data) => {
// Format and print output to terminal
console.log(data)
})
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,
},
},
}),
},
})
// Usage: todos add "Buy milk" -p high
// Usage: todos list -c
wireCLI({
program: 'app',
options: {
verbose: { description: 'Verbose output', short: 'v', default: false },
},
commands: {
greet: pikkuCLICommand({
parameters: '<name>',
func: greetUser,
render: greetRenderer,
}),
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' },
},
}),
},
},
},
})
// Usage: app greet Alice
// Usage: app user create bob [email protected] -a
// Usage: app user list -l 10
// Usage: app -v user list
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}`)
})
}
)
// Default renderer for program, override per-command
wireCLI({
program: 'todos',
render: jsonRenderer,
commands: {
add: pikkuCLICommand({
func: createTodo,
render: todoRenderer, // Overrides jsonRenderer
}),
},
})
// functions/admin.functions.ts
export const createUser = pikkuFunc({
title: 'Create User',
func: async ({ db }, { username, email, admin }) => {
const user = await db.createUser({
username,
email,
role: admin ? 'admin' : 'user',
})
return { user }
},
})
export const listUsers = pikkuSessionlessFunc({
title: 'List Users',
func: async ({ db }, { limit }) => {
return { users: await db.listUsers(limit || 50) }
},
})
export const deleteUser = pikkuFunc({
title: 'Delete User',
func: async ({ db }, { username }) => {
await db.deleteUser(username)
return { deleted: username }
},
})
// wirings/cli.wiring.ts
const userRenderer = pikkuCLIRender<{ user: User }>((_services, { user }) => {
console.log(`Created user: ${user.username} (${user.email}) [${user.role}]`)
})
const usersRenderer = pikkuCLIRender<{ users: User[] }>(
(_services, { users }) => {
console.log(`Users (${users.length}):`)
users.forEach((u) =>
console.log(` ${u.username} <${u.email}> [${u.role}]`)
)
}
)
wireCLI({
program: 'admin',
commands: {
user: {
description: 'User management',
subcommands: {
create: pikkuCLICommand({
parameters: '<username> <email>',
func: createUser,
render: userRenderer,
options: {
admin: {
description: 'Create as admin',
short: 'a',
default: false,
},
},
}),
list: pikkuCLICommand({
func: listUsers,
render: usersRenderer,
options: {
limit: { description: 'Max results', short: 'l' },
},
}),
delete: pikkuCLICommand({
parameters: '<username>',
func: deleteUser,
description: 'Delete a user',
}),
},
},
},
})
documentation
Standard cleanup to run right after a Pikku template is cloned or scaffolded into a new project. TRIGGER when: a Pikku template was just cloned/scaffolded (via `pikku create`, `git clone <template>`, or the user says "I cloned the kanban template / starter / template"), or the working tree still looks like an untouched template (template README, placeholder `@project/*` name in package.json). DO NOT TRIGGER when: working in an established project mid-feature, or editing the template repo itself.
development
Make a Pikku frontend work in both English (LTR) and Arabic / right-to-left languages. Direction is derived from the active locale, applied once at the document root, and the layout mirrors itself — but only if styling is written flow-relative (margin-inline-start, text-align: start, Mantine ms/me) instead of left/right. TRIGGER when: adding Arabic (or Hebrew/Farsi/Urdu), asked to "support RTL / right-to-left / bidi / mirror the layout", or writing layout styles in an app that may run RTL. Builds on pikku-i18n (an RTL language is just another locale file). DO NOT TRIGGER for backend functions or for LTR-only copy changes.
development
Wire i18n into a Pikku frontend (Vite SPA, Vite SSR, or Next.js app-router) with react-i18next + i18next. English by default, every user-facing string goes through a `t()` token, and additional languages are served under `/de` `/es` URL prefixes. TRIGGER when: scaffolding or editing a frontend and writing user-facing text, adding a second language, or asked to "make this translatable / use tokens / add i18n". DO NOT TRIGGER for backend functions, error messages thrown from functions, or log output.
development
Use when translating an n8n Code node body into a real Pikku function body. Triggered when the user opens or points at a stub generated by @pikku/n8n-import (look for `STUB — generated from n8n Code node` in the file's JSDoc), or when the user says 'translate this n8n code', 'port this n8n code node', 'finish the codeStub__... function', etc. The stub file is a `pikkuSessionlessFunc` with a Zod input/output, a JSDoc preserving the original n8n JavaScript verbatim, and a `throw new Error('… — implement me')` body.