oclif
An open CLI framework for Node.js/TypeScript developed by Heroku/Salesforce. Features a plugin system.
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!')
}
}