Files
gh-vanman2024-cli-builder-p…/skills/oclif-patterns/examples/plugin-cli-example.md
2025-11-30 09:04:14 +08:00

5.7 KiB

Plugin CLI Example

Complete example of an oclif CLI with plugin support.

Overview

This example shows:

  • Main CLI with core commands
  • Plugin system for extensibility
  • Plugin installation and management
  • Shared hooks between CLI and plugins

Main CLI Structure

mycli/
├── package.json
├── src/
│   ├── commands/
│   │   └── core.ts
│   └── hooks/
│       └── init.ts
└── plugins/
    └── plugin-deploy/
        ├── package.json
        └── src/
            └── commands/
                └── deploy.ts

Step 1: Create Main CLI

Main CLI package.json

{
  "name": "mycli",
  "version": "1.0.0",
  "oclif": {
    "bin": "mycli",
    "commands": "./lib/commands",
    "plugins": [
      "@oclif/plugin-help",
      "@oclif/plugin-plugins"
    ],
    "hooks": {
      "init": "./lib/hooks/init"
    }
  },
  "dependencies": {
    "@oclif/core": "^3.0.0",
    "@oclif/plugin-help": "^6.0.0",
    "@oclif/plugin-plugins": "^4.0.0"
  }
}

Core Command

File: src/commands/core.ts

import { Command, Flags } from '@oclif/core'

export default class Core extends Command {
  static description = 'Core CLI functionality'

  static flags = {
    version: Flags.boolean({
      char: 'v',
      description: 'Show CLI version',
    }),
  }

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

    if (flags.version) {
      this.log(`Version: ${this.config.version}`)
      return
    }

    this.log('Core CLI is running')

    // List installed plugins
    const plugins = this.config.plugins
    this.log(`\nInstalled plugins: ${plugins.length}`)
    plugins.forEach(p => {
      this.log(`  - ${p.name} (${p.version})`)
    })
  }
}

Init Hook

File: src/hooks/init.ts

import { Hook } from '@oclif/core'

const hook: Hook<'init'> = async function (opts) {
  // Initialize CLI
  this.debug('Initializing mycli...')

  // Check for updates, load config, etc.
}

export default hook

Step 2: Create Plugin

Plugin package.json

{
  "name": "@mycli/plugin-deploy",
  "version": "1.0.0",
  "description": "Deployment plugin for mycli",
  "oclif": {
    "bin": "mycli",
    "commands": "./lib/commands",
    "topics": {
      "deploy": {
        "description": "Deployment commands"
      }
    }
  },
  "dependencies": {
    "@oclif/core": "^3.0.0"
  }
}

Deploy Command

File: plugins/plugin-deploy/src/commands/deploy.ts

import { Command, Flags } from '@oclif/core'

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

  static examples = [
    '<%= config.bin %> deploy --env production',
  ]

  static flags = {
    env: Flags.string({
      char: 'e',
      description: 'Environment to deploy to',
      options: ['development', 'staging', 'production'],
      required: true,
    }),
    force: Flags.boolean({
      char: 'f',
      description: 'Force deployment',
      default: false,
    }),
  }

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

    this.log(`Deploying to ${flags.env}...`)

    if (flags.force) {
      this.log('Force deployment enabled')
    }

    // Deployment logic here
    this.log('✓ Deployment successful')
  }
}
# Build main CLI
cd mycli
npm run build

# Build plugin
cd plugins/plugin-deploy
npm run build

# Link plugin to main CLI
cd ../../
mycli plugins:link ./plugins/plugin-deploy

Step 4: Use Plugin Commands

# List plugins
mycli plugins

# Use plugin command
mycli deploy --env production

# Get help for plugin command
mycli deploy --help

Step 5: Install Plugin from npm

Publish Plugin

cd plugins/plugin-deploy
npm publish

Install Plugin

mycli plugins:install @mycli/plugin-deploy

Plugin Management Commands

# List installed plugins
mycli plugins

# Install plugin
mycli plugins:install @mycli/plugin-name

# Update plugin
mycli plugins:update @mycli/plugin-name

# Uninstall plugin
mycli plugins:uninstall @mycli/plugin-name

# Link local plugin (development)
mycli plugins:link /path/to/plugin

Advanced: Plugin with Hooks

File: plugins/plugin-deploy/src/hooks/prerun.ts

import { Hook } from '@oclif/core'

const hook: Hook<'prerun'> = async function (opts) {
  // Check deployment prerequisites
  if (opts.Command.id === 'deploy') {
    this.log('Checking deployment prerequisites...')

    // Check environment, credentials, etc.
  }
}

export default hook

Register in plugin package.json:

{
  "oclif": {
    "hooks": {
      "prerun": "./lib/hooks/prerun"
    }
  }
}

Key Concepts Demonstrated

  1. Plugin System: @oclif/plugin-plugins integration
  2. Plugin Discovery: Automatic command loading from plugins
  3. Plugin Management: Install, update, uninstall commands
  4. Local Development: plugins:link for local plugin development
  5. Hooks: Shared hooks between main CLI and plugins
  6. Topic Commands: Organized plugin commands (deploy:*)
  7. Plugin Metadata: Package.json oclif configuration
  8. Plugin Distribution: Publishing to npm

Testing Plugins

File: plugins/plugin-deploy/test/commands/deploy.test.ts

import { expect, test } from '@oclif/test'

describe('deploy', () => {
  test
    .stdout()
    .command(['deploy', '--env', 'production'])
    .it('deploys to production', ctx => {
      expect(ctx.stdout).to.contain('Deploying to production')
      expect(ctx.stdout).to.contain('successful')
    })

  test
    .command(['deploy'])
    .catch(error => {
      expect(error.message).to.contain('Missing required flag')
    })
    .it('requires env flag')
})