oclif

An open CLI framework for Node.js/TypeScript developed by Heroku/Salesforce. Features a plugin system.

javascriptclitypescriptframeworkplugin

Framework

oclif

Overview

oclif is an open CLI framework for Node.js/TypeScript developed by Heroku/Salesforce. It features a plugin system that enables building extensible and full-fledged CLI applications. It's popular for enterprise-grade CLI tool development and is adopted in large projects like Heroku CLI and Salesforce CLI.

Details

oclif stands for "Open CLI Framework" and was developed by Salesforce (formerly Heroku). Rather than just a command-line argument parser, it's a comprehensive framework for building full-fledged CLI applications. It provides integrated features necessary for enterprise-level CLI tools, including plugin architecture, automatic test generation, and distribution systems. With a TypeScript-first design, it emphasizes both type safety and developer experience.

Key Features

  • Plugin Architecture: Extensible plugin system
  • TypeScript Support: Benefits of type safety and IntelliSense
  • Automatic Help Generation: Beautiful and structured help messages
  • Testing Support: Automatic testing functionality for CLI commands
  • Distribution System: Generation of npm packages, tarballs, and installers
  • Hooks: Extensibility through lifecycle hooks
  • Configuration Management: Integrated management of config files and flags

Pros and Cons

Pros

  • Enterprise-ready: Design suitable for large-scale CLI applications
  • Plugin Support: Flexible extension system
  • Type Safety: Robust development experience with TypeScript
  • Rich Features: Comprehensive support including testing, distribution, and configuration management
  • Track Record: Proven track record at Salesforce and Heroku with community support

Cons

  • Complexity: Features may be excessive for simple CLIs
  • Learning Cost: Understanding of framework-specific concepts required
  • Dependencies: Large bundle size due to many dependencies
  • Overhead: Can be too heavy for small-scale tools

Key Links

Example Usage

// package.json
{
  "name": "mycli",
  "version": "1.0.0",
  "oclif": {
    "bin": "mycli",
    "dirname": "mycli",
    "commands": "./lib/commands",
    "plugins": [
      "@oclif/plugin-help",
      "@oclif/plugin-plugins"
    ],
    "topicSeparator": " "
  }
}

// src/commands/hello.ts
import { Args, Command, Flags } from '@oclif/core'

export default class Hello extends Command {
  static override args = {
    person: Args.string({ description: 'person to say hello to' }),
  }

  static override description = 'describe the command here'

  static override examples = [
    '<%= config.bin %> <%= command.id %> friend',
    '<%= config.bin %> <%= command.id %> --name=myname',
  ]

  static override flags = {
    name: Flags.string({ char: 'n', description: 'name to print' }),
    from: Flags.string({ char: 'f', description: 'who is saying hello', required: true }),
    uppercase: Flags.boolean({ char: 'u', description: 'output in uppercase' }),
  }

  public async run(): Promise<void> {
    const { args, flags } = await this.parse(Hello)

    const name = flags.name ?? args.person ?? 'World'
    let message = `Hello, ${name}! From ${flags.from}`

    if (flags.uppercase) {
      message = message.toUpperCase()
    }

    this.log(message)
  }
}

// src/commands/config/set.ts
import { Args, Command, Flags } from '@oclif/core'

export default class ConfigSet extends Command {
  static override args = {
    key: Args.string({ description: 'config key', required: true }),
    value: Args.string({ description: 'config value', required: true }),
  }

  static override description = 'set a configuration value'

  static override flags = {
    global: Flags.boolean({ char: 'g', description: 'save to global config' }),
  }

  public async run(): Promise<void> {
    const { args, flags } = await this.parse(ConfigSet)
    
    // Configuration saving logic
    this.log(`Setting config: ${args.key} = ${args.value}`)
    if (flags.global) {
      this.log('Saved to global configuration')
    }
  }
}

// src/commands/deploy.ts
import { Command, Flags } from '@oclif/core'
import { ux } from '@oclif/core'

export default class Deploy extends Command {
  static override description = 'deploy application'

  static override flags = {
    environment: Flags.string({
      char: 'e',
      description: 'deployment environment',
      options: ['development', 'staging', 'production'],
      default: 'staging',
    }),
    'dry-run': Flags.boolean({ description: 'simulate deployment' }),
    verbose: Flags.boolean({ char: 'v', description: 'verbose logging' }),
  }

  public async run(): Promise<void> {
    const { flags } = await this.parse(Deploy)

    if (flags['dry-run']) {
      this.log('🔍 Dry run mode')
    }

    this.log(`🚀 Starting deployment to ${flags.environment}`)

    // Display progress bar
    const bar = ux.progress({
      format: 'Deploying... [{bar}] {percentage}% | {value}/{total}',
    })
    bar.start(100, 0)

    // Simulate deployment process
    for (let i = 0; i <= 100; i += 10) {
      bar.update(i)
      await new Promise(resolve => setTimeout(resolve, 200))
    }

    bar.stop()
    this.log('✅ Deployment completed')
  }
}

// src/hooks/init.ts
import { Hook } from '@oclif/core'

const hook: Hook<'init'> = async function (opts) {
  // Initialization logic
  this.log('CLI initialized')
}

export default hook

// Plugin example - src/commands/plugins/install.ts
import { Args, Command } from '@oclif/core'

export default class PluginsInstall extends Command {
  static override args = {
    plugin: Args.string({ description: 'plugin name to install', required: true }),
  }

  static override description = 'install plugin'

  public async run(): Promise<void> {
    const { args } = await this.parse(PluginsInstall)
    
    this.log(`Installing plugin: ${args.plugin}`)
    // Plugin installation logic
    this.log('Installation complete!')
  }
}