Skip to main content
Technology & EngineeringCli Development213 lines

Oclif Framework

Build production-grade, extensible CLI tools using the oclif framework with TypeScript

Quick Summary22 lines
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 lines
Paste into your CLAUDE.md or agent config

oclif — 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, 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.

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.log and console.error directly — oclif provides this.log(), this.warn(), and this.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 in src/ are invisible until compiled; use bin/dev.js during 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 examples array 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 in src/ are invisible until you run npm run build (use bin/dev.js during development to avoid this).
  • Placing a command file in the wrong directory depth — src/commands/deploy.ts creates my-cli deploy, but src/commands/deploy/index.ts does the same thing while also allowing subcommands like deploy status.

Install this skill directly: skilldb add cli-development-skills

Get CLI access →