Oclif Framework
Build production-grade, extensible CLI tools using the oclif framework with TypeScript
You are an expert in building command-line tools with oclif, Salesforce's open-source framework for building production CLIs.
## Key Points
- Use the `static description`, `static examples`, and `static flags` properties on every command — oclif auto-generates high-quality help text from them.
- Organize commands into topic directories (e.g., `commands/deploy/`, `commands/config/`) to keep large CLIs navigable, since the directory structure maps directly to the command hierarchy.
- Use `this.error('message', { exit: 1 })` and `this.warn('message')` instead of throwing or using `console.error` — oclif formats these consistently and handles exit codes.
- **Not using static examples** — omitting the `static examples` array means the auto-generated help provides no usage guidance, forcing users to read documentation or guess the correct invocation.
## Quick Example
```bash
npx oclif generate my-cli
cd my-cli
npm install
```
```bash
my-cli plugins install my-cli-plugin-auth
```skilldb get cli-development-skills/Oclif FrameworkFull skill: 213 linesoclif — CLI Development
You are an expert in building command-line tools with oclif, Salesforce's open-source framework for building production CLIs.
Overview
oclif is a framework for building CLIs in Node.js and TypeScript. It powers the Heroku CLI and Salesforce CLI, and is designed for large, plugin-extensible command-line tools. It provides a class-based command structure, automatic help generation, flag/argument parsing, plugin support, and built-in testing utilities.
Setup & Configuration
Scaffold a new oclif project:
npx oclif generate my-cli
cd my-cli
npm install
Project structure:
my-cli/
src/
commands/
index.ts # default command
deploy/
index.ts # `my-cli deploy`
status.ts # `my-cli deploy status`
hooks/
init.ts
bin/
run.js
dev.js
package.json
Minimal command:
import { Command, Flags, Args } from '@oclif/core';
export default class Greet extends Command {
static description = 'Greet a user';
static examples = [
'<%= config.bin %> greet world',
'<%= config.bin %> greet world --shout',
];
static flags = {
shout: Flags.boolean({ char: 's', description: 'uppercase the output' }),
times: Flags.integer({ char: 'n', description: 'repeat count', default: 1 }),
};
static args = {
name: Args.string({ description: 'who to greet', required: true }),
};
async run(): Promise<void> {
const { args, flags } = await this.parse(Greet);
let msg = `Hello, ${args.name}!`;
if (flags.shout) msg = msg.toUpperCase();
for (let i = 0; i < flags.times; i++) {
this.log(msg);
}
}
}
Core Patterns
Multi-command with topics
// src/commands/deploy/index.ts
import { Command, Flags } from '@oclif/core';
export default class Deploy extends Command {
static description = 'Deploy application to a target environment';
static flags = {
environment: Flags.string({ char: 'e', required: true, options: ['staging', 'production'] }),
force: Flags.boolean({ description: 'skip confirmation' }),
};
async run(): Promise<void> {
const { flags } = await this.parse(Deploy);
this.log(`Deploying to ${flags.environment}...`);
}
}
// src/commands/deploy/status.ts — accessed as `my-cli deploy status`
export default class DeployStatus extends Command {
static description = 'Check deployment status';
async run(): Promise<void> {
this.log('Deployment status: running');
}
}
Hooks for lifecycle events
// src/hooks/init.ts
import { Hook } from '@oclif/core';
const hook: Hook<'init'> = async function (options) {
// runs before every command
const configPath = `${options.config.configDir}/config.json`;
// load config, check auth, etc.
};
export default hook;
Register in package.json:
{
"oclif": {
"hooks": {
"init": "./dist/hooks/init"
}
}
}
Structured output with JSON flag
import { Command, Flags, ux } from '@oclif/core';
export default class List extends Command {
static flags = {
json: Flags.boolean({ description: 'output as JSON' }),
};
async run(): Promise<void> {
const { flags } = await this.parse(List);
const items = await fetchItems();
if (flags.json) {
this.log(JSON.stringify(items, null, 2));
} else {
ux.table(items, {
name: { header: 'Name' },
status: { header: 'Status' },
updated: { header: 'Updated', get: (row) => row.updatedAt.toLocaleDateString() },
});
}
}
}
Plugin architecture
// package.json of a plugin
{
"name": "my-cli-plugin-auth",
"oclif": {
"commands": "./dist/commands",
"hooks": {
"init": "./dist/hooks/check-auth"
}
}
}
Users install plugins:
my-cli plugins install my-cli-plugin-auth
Best Practices
- Use the
static description,static examples, andstatic flagsproperties on every command — oclif auto-generates high-quality help text from them. - Organize commands into topic directories (e.g.,
commands/deploy/,commands/config/) to keep large CLIs navigable, since the directory structure maps directly to the command hierarchy. - Use
this.error('message', { exit: 1 })andthis.warn('message')instead of throwing or usingconsole.error— oclif formats these consistently and handles exit codes.
Core Philosophy
oclif is built for CLIs that grow. Its class-based command structure, automatic help generation, and plugin architecture are designed for tools with dozens or hundreds of commands maintained by multiple teams. If you are building a small utility, oclif may be overkill — but if you are building a CLI that will be extended, distributed, and maintained for years, oclif's conventions pay dividends in consistency and developer experience.
Convention over configuration is oclif's guiding principle. The directory structure maps to the command hierarchy. Static class properties generate help text. Flags and arguments have built-in parsing and validation. Follow these conventions rather than fighting them, and you get a production-grade CLI with minimal boilerplate.
Plugins are oclif's killer feature. They allow users and third parties to extend your CLI with new commands without modifying the core codebase. Design your CLI with plugin extensibility in mind from the start: keep the core lean, define clear extension points, and use hooks to allow plugins to participate in lifecycle events like initialization and error handling.
Anti-Patterns
-
Using
console.logandconsole.errordirectly — oclif providesthis.log(),this.warn(), andthis.error()with consistent formatting and exit code handling; bypassing them produces inconsistent output. -
Forgetting to compile TypeScript before running — oclif loads commands from
dist/, so changes insrc/are invisible until compiled; usebin/dev.jsduring development to avoid this friction. -
Putting business logic in command classes — command classes should be thin wrappers that parse input and delegate to service modules; embedding logic directly in
run()makes it untestable without subprocess execution. -
Not using static examples — omitting the
static examplesarray means the auto-generated help provides no usage guidance, forcing users to read documentation or guess the correct invocation. -
Overloading a single command instead of using topics — cramming many behaviors into one command with a dozen flags creates a confusing UX; split into topic directories (e.g.,
commands/deploy/,commands/config/) for clarity.
Common Pitfalls
- Forgetting to compile TypeScript before testing — oclif loads commands from
dist/, so changes insrc/are invisible until you runnpm run build(usebin/dev.jsduring development to avoid this). - Placing a command file in the wrong directory depth —
src/commands/deploy.tscreatesmy-cli deploy, butsrc/commands/deploy/index.tsdoes the same thing while also allowing subcommands likedeploy status.
Install this skill directly: skilldb add cli-development-skills
Related Skills
Chalk Picocolors
Style terminal output with chalk and picocolors for colored, formatted CLI text
Clack Prompts
Build beautiful interactive CLI prompts using @clack/prompts with minimal boilerplate
CLI Distribution
Package and distribute CLI tools via npm/npx, standalone binaries with pkg, and Homebrew taps
CLI Testing
Test CLI applications using subprocess execution, mock filesystems, and snapshot testing
Commander Js
Build structured CLI applications with Commander.js including commands, options, and argument parsing
Ink React CLI
Build rich interactive terminal UIs using Ink, a React renderer for the command line