The extensible CLI framework
Commands, hooks, middleware, extensions — everything snaps together.
Install · Quick Start · Extensions · Config · Context API
bun install -g kapyRun kapy directly. It ships with meta-commands — its CLI surface grows as you install extensions:
kapy install npm:@foo/kapy-deploy
kapy deploy:aws --region us-east-1Build your own extensible CLI on top of kapy:
import { kapy } from "@moikapy/kapy"
kapy()
.command("deploy", {
description: "Deploy your application",
args: [{ name: "env", description: "Environment", default: "staging" }],
flags: {
verbose: { type: "boolean", alias: "v", description: "Verbose output" },
},
}, async (ctx) => {
ctx.log(`Deploying to ${ctx.args.env}...`)
})
.run()| Command | Description |
|---|---|
kapy init <name> |
Scaffold a new kapy-powered CLI project |
kapy install <pkg> |
Install an extension (npm/git/local) |
kapy list |
Show installed extensions |
kapy update [name] |
Update all or a specific extension |
kapy remove <name> |
Uninstall an extension |
kapy upgrade [--pm <bun|npm|yarn|pnpm>] |
Upgrade kapy (auto-detects package manager) |
kapy config |
View/edit configuration |
kapy dev |
Run CLI in dev mode with hot reload |
kapy commands |
List all registered commands |
kapy inspect |
Dump full state (extensions, config, hooks) |
kapy help [command] |
Show help for a command |
Extensions are npm packages with the kapy-extension keyword. They export a register function and a meta object:
import type { KapyExtensionAPI } from "@moikapy/kapy"
export async function register(api: KapyExtensionAPI) {
api.addCommand("deploy:aws", {
description: "Deploy to AWS",
}, async (ctx) => {
ctx.log("Deploying to AWS...")
})
api.addHook("before:deploy", async (ctx) => {
// auth check, etc.
})
api.addMiddleware(async (ctx, next) => {
const start = Date.now()
await next()
ctx.log(`Completed in ${Date.now() - start}ms`)
})
api.declareConfig({
region: { type: "string", required: true, description: "AWS region" },
})
api.on("custom-event", async (data) => {
console.log("Event received:", data)
})
}
export const meta = {
name: "@foo/kapy-deploy-aws",
version: "1.0.0",
dependencies: [],
}Install from npm, git, or local paths:
kapy install npm:@foo/kapy-ext
kapy install npm:@foo/kapy-ext@1.2.3
kapy install git:github.com/user/repo
kapy install ./path/to/ext| Method | Purpose |
|---|---|
addCommand(def) |
Register a command |
addHook(event, handler) |
Register a lifecycle hook |
addMiddleware(mw) |
Register middleware |
declareConfig(schema) |
Declare config schema (auto-namespaced) |
emit(event, data?) |
Emit a custom event |
on(event, handler) |
Listen for a custom event |
Config hierarchy (later overrides earlier):
kapy defaults → kapy.config.ts → ~/.kapy/config.json → env vars → CLI flags
// kapy.config.ts
import { defineConfig } from "@moikapy/kapy"
export default defineConfig({
name: "my-cli",
extensions: ["npm:@foo/kapy-deploy"],
envPrefix: "MY_CLI",
})# Environment variables
KAPY_DEPLOY_AWS_REGION=us-west-2 kapy deploy:aws
# Custom prefix (embedded mode)
MY_CLI_DEPLOY_AWS_REGION=us-west-2 my-cli deploy:awsEvery command supports --json and --no-input for scripts and automation:
kapy commands --json
kapy deploy:aws --json --no-input
kapy inspect --jsonEvery command handler receives a ctx object:
async (ctx) => {
// Basic
ctx.args // Parsed args + flags
ctx.config // Merged config
ctx.log(msg) // Styled success output
ctx.warn(msg) // Styled warning
ctx.error(msg) // Styled error
ctx.spinner(text) // Progress spinner
ctx.prompt(msg) // Interactive input
ctx.confirm(msg) // Yes/no confirm
ctx.abort(code?) // Cancel execution
// Process-Aware (v0.2.0+)
ctx.isInteractive // True when TTY + !json + !noInput
ctx.spawn(cmd, opts?) // Spawn subprocess (TTY passthrough, abort-safe)
ctx.exitCode // Writable exit code (propagated to process.exit)
ctx.teardown(fn) // Register cleanup callback (LIFO, async-safe)
}const result = await ctx.spawn(["tmux", "new-session", "-s", "dev"], {
tty: true, // Pass through stdin/stdout/stderr for interactive processes
stream: false, // Stream output in real-time (default: collect)
env: { FOO: "bar" }, // Merge env vars with process.env
cwd: "/tmp", // Working directory
abortOnError: true, // Auto-kill process on ctx.abort()
suppressOutput: true, // Suppress stdout in --json mode
})
// result: { exitCode, stdout, stderr, aborted }TypeScript · Bun · picocolors · Biome
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General error |
| 2 | Invalid arguments / unknown command |
| 3 | Extension error |
| 4 | Config error |
| 5 | Network error |
| 10 | Aborted by hook/middleware |
MIT
