Initial commit
This commit is contained in:
188
skills/oclif-patterns/examples/basic-cli-example.md
Normal file
188
skills/oclif-patterns/examples/basic-cli-example.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# Basic CLI Example
|
||||
|
||||
Complete example of a simple oclif CLI with multiple commands.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
mycli/
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
├── src/
|
||||
│ ├── commands/
|
||||
│ │ ├── hello.ts
|
||||
│ │ ├── goodbye.ts
|
||||
│ │ └── config.ts
|
||||
│ └── index.ts
|
||||
├── test/
|
||||
│ └── commands/
|
||||
│ └── hello.test.ts
|
||||
└── bin/
|
||||
└── run.js
|
||||
```
|
||||
|
||||
## Step-by-Step Setup
|
||||
|
||||
### 1. Initialize Project
|
||||
|
||||
```bash
|
||||
mkdir mycli && cd mycli
|
||||
npm init -y
|
||||
npm install @oclif/core
|
||||
npm install --save-dev @oclif/test @types/node typescript ts-node oclif
|
||||
```
|
||||
|
||||
### 2. Create package.json Configuration
|
||||
|
||||
Add oclif configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "mycli",
|
||||
"version": "1.0.0",
|
||||
"oclif": {
|
||||
"bin": "mycli",
|
||||
"commands": "./lib/commands",
|
||||
"plugins": [
|
||||
"@oclif/plugin-help"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Create Hello Command
|
||||
|
||||
File: `src/commands/hello.ts`
|
||||
|
||||
```typescript
|
||||
import { Command, Flags, Args } from '@oclif/core'
|
||||
|
||||
export default class Hello extends Command {
|
||||
static description = 'Say hello to someone'
|
||||
|
||||
static examples = [
|
||||
'<%= config.bin %> <%= command.id %> Alice',
|
||||
'<%= config.bin %> <%= command.id %> Bob --greeting="Hi"',
|
||||
]
|
||||
|
||||
static flags = {
|
||||
greeting: Flags.string({
|
||||
char: 'g',
|
||||
description: 'Greeting to use',
|
||||
default: 'Hello',
|
||||
}),
|
||||
excited: Flags.boolean({
|
||||
char: 'e',
|
||||
description: 'Add exclamation',
|
||||
default: false,
|
||||
}),
|
||||
}
|
||||
|
||||
static args = {
|
||||
name: Args.string({
|
||||
description: 'Name to greet',
|
||||
required: true,
|
||||
}),
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
const { args, flags } = await this.parse(Hello)
|
||||
|
||||
const punctuation = flags.excited ? '!' : '.'
|
||||
this.log(`${flags.greeting}, ${args.name}${punctuation}`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Build and Test
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
./bin/run.js hello World
|
||||
# Output: Hello, World.
|
||||
|
||||
./bin/run.js hello World --greeting="Hi" --excited
|
||||
# Output: Hi, World!
|
||||
```
|
||||
|
||||
### 5. Add Help Documentation
|
||||
|
||||
```bash
|
||||
./bin/run.js hello --help
|
||||
|
||||
# Output:
|
||||
# Say hello to someone
|
||||
#
|
||||
# USAGE
|
||||
# $ mycli hello NAME [-g <value>] [-e]
|
||||
#
|
||||
# ARGUMENTS
|
||||
# NAME Name to greet
|
||||
#
|
||||
# FLAGS
|
||||
# -e, --excited Add exclamation
|
||||
# -g, --greeting=<value> [default: Hello] Greeting to use
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
File: `test/commands/hello.test.ts`
|
||||
|
||||
```typescript
|
||||
import { expect, test } from '@oclif/test'
|
||||
|
||||
describe('hello', () => {
|
||||
test
|
||||
.stdout()
|
||||
.command(['hello', 'World'])
|
||||
.it('says hello', ctx => {
|
||||
expect(ctx.stdout).to.contain('Hello, World.')
|
||||
})
|
||||
|
||||
test
|
||||
.stdout()
|
||||
.command(['hello', 'Alice', '--excited'])
|
||||
.it('says hello with excitement', ctx => {
|
||||
expect(ctx.stdout).to.contain('Hello, Alice!')
|
||||
})
|
||||
|
||||
test
|
||||
.stdout()
|
||||
.command(['hello', 'Bob', '--greeting=Hi'])
|
||||
.it('uses custom greeting', ctx => {
|
||||
expect(ctx.stdout).to.contain('Hi, Bob.')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Run Tests
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
## Distribution
|
||||
|
||||
### Package for npm
|
||||
|
||||
```bash
|
||||
npm pack
|
||||
npm publish
|
||||
```
|
||||
|
||||
### Install Globally
|
||||
|
||||
```bash
|
||||
npm install -g .
|
||||
mycli hello World
|
||||
```
|
||||
|
||||
## Key Concepts Demonstrated
|
||||
|
||||
1. **Command Structure**: Basic command with flags and args
|
||||
2. **Flag Types**: String and boolean flags with defaults
|
||||
3. **Arguments**: Required string argument
|
||||
4. **Help Documentation**: Auto-generated from metadata
|
||||
5. **Testing**: Using @oclif/test for command testing
|
||||
6. **Build Process**: TypeScript compilation to lib/
|
||||
7. **CLI Binary**: bin/run.js entry point
|
||||
427
skills/oclif-patterns/examples/enterprise-cli-example.md
Normal file
427
skills/oclif-patterns/examples/enterprise-cli-example.md
Normal file
@@ -0,0 +1,427 @@
|
||||
# Enterprise CLI Example
|
||||
|
||||
Complete production-ready oclif CLI with all best practices.
|
||||
|
||||
## Overview
|
||||
|
||||
This example demonstrates:
|
||||
- Custom base command with common functionality
|
||||
- Configuration management
|
||||
- Logging system
|
||||
- Error handling
|
||||
- Plugin support
|
||||
- Comprehensive testing
|
||||
- CI/CD integration
|
||||
- Auto-update capability
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
enterprise-cli/
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
├── .eslintrc.json
|
||||
├── .github/
|
||||
│ └── workflows/
|
||||
│ ├── test.yml
|
||||
│ └── release.yml
|
||||
├── src/
|
||||
│ ├── base-command.ts
|
||||
│ ├── config/
|
||||
│ │ ├── manager.ts
|
||||
│ │ └── schema.ts
|
||||
│ ├── commands/
|
||||
│ │ ├── deploy.ts
|
||||
│ │ ├── status.ts
|
||||
│ │ └── config/
|
||||
│ │ ├── get.ts
|
||||
│ │ └── set.ts
|
||||
│ ├── hooks/
|
||||
│ │ ├── init.ts
|
||||
│ │ └── prerun.ts
|
||||
│ ├── utils/
|
||||
│ │ ├── logger.ts
|
||||
│ │ └── error-handler.ts
|
||||
│ └── index.ts
|
||||
├── test/
|
||||
│ ├── commands/
|
||||
│ ├── helpers/
|
||||
│ └── fixtures/
|
||||
└── docs/
|
||||
└── commands/
|
||||
```
|
||||
|
||||
## Base Command Implementation
|
||||
|
||||
File: `src/base-command.ts`
|
||||
|
||||
```typescript
|
||||
import { Command, Flags } from '@oclif/core'
|
||||
import { ConfigManager } from './config/manager'
|
||||
import { Logger } from './utils/logger'
|
||||
import { ErrorHandler } from './utils/error-handler'
|
||||
|
||||
export default abstract class BaseCommand extends Command {
|
||||
protected configManager!: ConfigManager
|
||||
protected logger!: Logger
|
||||
protected errorHandler!: ErrorHandler
|
||||
|
||||
static baseFlags = {
|
||||
config: Flags.string({
|
||||
char: 'c',
|
||||
description: 'Path to config file',
|
||||
env: 'CLI_CONFIG',
|
||||
}),
|
||||
'log-level': Flags.string({
|
||||
description: 'Set log level',
|
||||
options: ['error', 'warn', 'info', 'debug'],
|
||||
default: 'info',
|
||||
env: 'LOG_LEVEL',
|
||||
}),
|
||||
json: Flags.boolean({
|
||||
description: 'Output as JSON',
|
||||
default: false,
|
||||
}),
|
||||
'no-color': Flags.boolean({
|
||||
description: 'Disable colors',
|
||||
default: false,
|
||||
}),
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
await super.init()
|
||||
|
||||
// Initialize logger
|
||||
const { flags } = await this.parse(this.constructor as typeof BaseCommand)
|
||||
this.logger = new Logger(flags['log-level'], !flags['no-color'])
|
||||
|
||||
// Initialize config manager
|
||||
this.configManager = new ConfigManager(flags.config)
|
||||
await this.configManager.load()
|
||||
|
||||
// Initialize error handler
|
||||
this.errorHandler = new ErrorHandler(this.logger)
|
||||
|
||||
// Log initialization
|
||||
this.logger.debug(`Initialized ${this.id}`)
|
||||
}
|
||||
|
||||
protected async catch(err: Error & { exitCode?: number }): Promise<any> {
|
||||
return this.errorHandler.handle(err)
|
||||
}
|
||||
|
||||
protected output(data: any, humanMessage?: string): void {
|
||||
const { flags } = this.parse(this.constructor as typeof BaseCommand)
|
||||
|
||||
if (flags.json) {
|
||||
this.log(JSON.stringify(data, null, 2))
|
||||
} else if (humanMessage) {
|
||||
this.log(humanMessage)
|
||||
} else {
|
||||
this.log(JSON.stringify(data, null, 2))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Manager
|
||||
|
||||
File: `src/config/manager.ts`
|
||||
|
||||
```typescript
|
||||
import * as fs from 'fs-extra'
|
||||
import * as path from 'path'
|
||||
import * as os from 'os'
|
||||
|
||||
export class ConfigManager {
|
||||
private config: any = {}
|
||||
private configPath: string
|
||||
|
||||
constructor(customPath?: string) {
|
||||
this.configPath = customPath || this.getDefaultConfigPath()
|
||||
}
|
||||
|
||||
async load(): Promise<void> {
|
||||
if (await fs.pathExists(this.configPath)) {
|
||||
this.config = await fs.readJson(this.configPath)
|
||||
} else {
|
||||
// Create default config
|
||||
this.config = this.getDefaultConfig()
|
||||
await this.save()
|
||||
}
|
||||
}
|
||||
|
||||
async save(): Promise<void> {
|
||||
await fs.ensureDir(path.dirname(this.configPath))
|
||||
await fs.writeJson(this.configPath, this.config, { spaces: 2 })
|
||||
}
|
||||
|
||||
get(key: string, defaultValue?: any): any {
|
||||
const keys = key.split('.')
|
||||
let value = this.config
|
||||
|
||||
for (const k of keys) {
|
||||
if (value && typeof value === 'object' && k in value) {
|
||||
value = value[k]
|
||||
} else {
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
set(key: string, value: any): void {
|
||||
const keys = key.split('.')
|
||||
const lastKey = keys.pop()!
|
||||
let current = this.config
|
||||
|
||||
for (const k of keys) {
|
||||
if (!(k in current) || typeof current[k] !== 'object') {
|
||||
current[k] = {}
|
||||
}
|
||||
current = current[k]
|
||||
}
|
||||
|
||||
current[lastKey] = value
|
||||
}
|
||||
|
||||
private getDefaultConfigPath(): string {
|
||||
return path.join(os.homedir(), '.config', 'mycli', 'config.json')
|
||||
}
|
||||
|
||||
private getDefaultConfig(): any {
|
||||
return {
|
||||
version: '1.0.0',
|
||||
defaults: {
|
||||
region: 'us-east-1',
|
||||
timeout: 30000,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Logger Implementation
|
||||
|
||||
File: `src/utils/logger.ts`
|
||||
|
||||
```typescript
|
||||
import chalk from 'chalk'
|
||||
|
||||
export class Logger {
|
||||
constructor(
|
||||
private level: string = 'info',
|
||||
private color: boolean = true
|
||||
) {}
|
||||
|
||||
debug(message: string): void {
|
||||
if (this.shouldLog('debug')) {
|
||||
this.output('DEBUG', message, chalk.gray)
|
||||
}
|
||||
}
|
||||
|
||||
info(message: string): void {
|
||||
if (this.shouldLog('info')) {
|
||||
this.output('INFO', message, chalk.blue)
|
||||
}
|
||||
}
|
||||
|
||||
warn(message: string): void {
|
||||
if (this.shouldLog('warn')) {
|
||||
this.output('WARN', message, chalk.yellow)
|
||||
}
|
||||
}
|
||||
|
||||
error(message: string, error?: Error): void {
|
||||
if (this.shouldLog('error')) {
|
||||
this.output('ERROR', message, chalk.red)
|
||||
if (error && error.stack) {
|
||||
console.error(chalk.red(error.stack))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
success(message: string): void {
|
||||
if (this.shouldLog('info')) {
|
||||
this.output('SUCCESS', message, chalk.green)
|
||||
}
|
||||
}
|
||||
|
||||
private shouldLog(level: string): boolean {
|
||||
const levels = ['error', 'warn', 'info', 'debug']
|
||||
const currentIndex = levels.indexOf(this.level)
|
||||
const messageIndex = levels.indexOf(level)
|
||||
return messageIndex <= currentIndex
|
||||
}
|
||||
|
||||
private output(level: string, message: string, colorFn: any): void {
|
||||
const timestamp = new Date().toISOString()
|
||||
const prefix = this.color ? colorFn(`[${level}]`) : `[${level}]`
|
||||
console.log(`${timestamp} ${prefix} ${message}`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Enterprise Deploy Command
|
||||
|
||||
File: `src/commands/deploy.ts`
|
||||
|
||||
```typescript
|
||||
import BaseCommand from '../base-command'
|
||||
import { Flags, Args, ux } from '@oclif/core'
|
||||
|
||||
export default class Deploy extends BaseCommand {
|
||||
static description = 'Deploy application to environment'
|
||||
|
||||
static examples = [
|
||||
'<%= config.bin %> deploy myapp --env production',
|
||||
'<%= config.bin %> deploy myapp --env staging --auto-approve',
|
||||
]
|
||||
|
||||
static flags = {
|
||||
...BaseCommand.baseFlags,
|
||||
env: Flags.string({
|
||||
char: 'e',
|
||||
description: 'Environment to deploy to',
|
||||
options: ['development', 'staging', 'production'],
|
||||
required: true,
|
||||
}),
|
||||
'auto-approve': Flags.boolean({
|
||||
description: 'Skip confirmation prompt',
|
||||
default: false,
|
||||
}),
|
||||
'rollback-on-failure': Flags.boolean({
|
||||
description: 'Automatically rollback on failure',
|
||||
default: true,
|
||||
}),
|
||||
}
|
||||
|
||||
static args = {
|
||||
app: Args.string({
|
||||
description: 'Application name',
|
||||
required: true,
|
||||
}),
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
const { args, flags } = await this.parse(Deploy)
|
||||
|
||||
this.logger.info(`Starting deployment of ${args.app} to ${flags.env}`)
|
||||
|
||||
// Confirmation prompt (skip in CI or with auto-approve)
|
||||
if (!flags['auto-approve'] && !process.env.CI) {
|
||||
const confirmed = await ux.confirm(
|
||||
`Deploy ${args.app} to ${flags.env}? (y/n)`
|
||||
)
|
||||
|
||||
if (!confirmed) {
|
||||
this.logger.info('Deployment cancelled')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-deployment checks
|
||||
ux.action.start('Running pre-deployment checks')
|
||||
await this.runPreDeploymentChecks(args.app, flags.env)
|
||||
ux.action.stop('passed')
|
||||
|
||||
// Deploy
|
||||
ux.action.start(`Deploying ${args.app}`)
|
||||
|
||||
try {
|
||||
const result = await this.deploy(args.app, flags.env)
|
||||
ux.action.stop('done')
|
||||
|
||||
this.logger.success(`Deployed ${args.app} to ${flags.env}`)
|
||||
this.output(result, `Deployment URL: ${result.url}`)
|
||||
} catch (error) {
|
||||
ux.action.stop('failed')
|
||||
|
||||
if (flags['rollback-on-failure']) {
|
||||
this.logger.warn('Deployment failed, rolling back...')
|
||||
await this.rollback(args.app, flags.env)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private async runPreDeploymentChecks(
|
||||
app: string,
|
||||
env: string
|
||||
): Promise<void> {
|
||||
// Check credentials
|
||||
// Validate app exists
|
||||
// Check environment health
|
||||
// Verify dependencies
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
}
|
||||
|
||||
private async deploy(app: string, env: string): Promise<any> {
|
||||
// Actual deployment logic
|
||||
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||
|
||||
return {
|
||||
app,
|
||||
env,
|
||||
version: '1.2.3',
|
||||
url: `https://${app}.${env}.example.com`,
|
||||
deployedAt: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
private async rollback(app: string, env: string): Promise<void> {
|
||||
// Rollback logic
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
this.logger.info('Rollback complete')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Setup
|
||||
|
||||
File: `test/helpers/test-context.ts`
|
||||
|
||||
```typescript
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs-extra'
|
||||
import { Config } from '@oclif/core'
|
||||
|
||||
export class TestContext {
|
||||
testDir: string
|
||||
config: Config
|
||||
|
||||
constructor() {
|
||||
this.testDir = path.join(__dirname, '../fixtures/test-run')
|
||||
}
|
||||
|
||||
async setup(): Promise<void> {
|
||||
await fs.ensureDir(this.testDir)
|
||||
this.config = await Config.load()
|
||||
}
|
||||
|
||||
async teardown(): Promise<void> {
|
||||
await fs.remove(this.testDir)
|
||||
}
|
||||
|
||||
async createConfigFile(config: any): Promise<string> {
|
||||
const configPath = path.join(this.testDir, 'config.json')
|
||||
await fs.writeJson(configPath, config)
|
||||
return configPath
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Key Enterprise Features
|
||||
|
||||
1. **Base Command**: Shared functionality across all commands
|
||||
2. **Configuration**: Centralized config management
|
||||
3. **Logging**: Structured logging with levels
|
||||
4. **Error Handling**: Consistent error handling
|
||||
5. **Confirmation Prompts**: Interactive confirmations
|
||||
6. **Rollback**: Automatic rollback on failure
|
||||
7. **Pre-deployment Checks**: Validation before operations
|
||||
8. **CI Detection**: Different behavior in CI environments
|
||||
9. **JSON Output**: Machine-readable output option
|
||||
10. **Environment Variables**: Config via env vars
|
||||
307
skills/oclif-patterns/examples/plugin-cli-example.md
Normal file
307
skills/oclif-patterns/examples/plugin-cli-example.md
Normal file
@@ -0,0 +1,307 @@
|
||||
# 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
|
||||
|
||||
```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`
|
||||
|
||||
```typescript
|
||||
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`
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```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`
|
||||
|
||||
```typescript
|
||||
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')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 3: Build and Link Plugin
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```bash
|
||||
cd plugins/plugin-deploy
|
||||
npm publish
|
||||
```
|
||||
|
||||
### Install Plugin
|
||||
|
||||
```bash
|
||||
mycli plugins:install @mycli/plugin-deploy
|
||||
```
|
||||
|
||||
## Plugin Management Commands
|
||||
|
||||
```bash
|
||||
# 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`
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```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`
|
||||
|
||||
```typescript
|
||||
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')
|
||||
})
|
||||
```
|
||||
393
skills/oclif-patterns/examples/quick-reference.md
Normal file
393
skills/oclif-patterns/examples/quick-reference.md
Normal file
@@ -0,0 +1,393 @@
|
||||
# oclif Patterns Quick Reference
|
||||
|
||||
Fast lookup for common oclif patterns and commands.
|
||||
|
||||
## Command Creation
|
||||
|
||||
### Basic Command
|
||||
```typescript
|
||||
import { Command, Flags, Args } from '@oclif/core'
|
||||
|
||||
export default class MyCommand extends Command {
|
||||
static description = 'Description'
|
||||
|
||||
static flags = {
|
||||
name: Flags.string({ char: 'n', required: true }),
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
const { flags } = await this.parse(MyCommand)
|
||||
this.log(`Hello ${flags.name}`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using Scripts
|
||||
```bash
|
||||
# Create command from template
|
||||
./scripts/create-command.sh my-command basic
|
||||
|
||||
# Create advanced command
|
||||
./scripts/create-command.sh deploy advanced
|
||||
|
||||
# Create async command
|
||||
./scripts/create-command.sh fetch async
|
||||
```
|
||||
|
||||
## Flag Patterns
|
||||
|
||||
### String Flag
|
||||
```typescript
|
||||
name: Flags.string({
|
||||
char: 'n',
|
||||
description: 'Name',
|
||||
required: true,
|
||||
default: 'World',
|
||||
})
|
||||
```
|
||||
|
||||
### Boolean Flag
|
||||
```typescript
|
||||
verbose: Flags.boolean({
|
||||
char: 'v',
|
||||
description: 'Verbose output',
|
||||
default: false,
|
||||
allowNo: true, // Enables --no-verbose
|
||||
})
|
||||
```
|
||||
|
||||
### Integer Flag
|
||||
```typescript
|
||||
port: Flags.integer({
|
||||
char: 'p',
|
||||
description: 'Port number',
|
||||
min: 1024,
|
||||
max: 65535,
|
||||
default: 3000,
|
||||
})
|
||||
```
|
||||
|
||||
### Option Flag (Enum)
|
||||
```typescript
|
||||
env: Flags.string({
|
||||
char: 'e',
|
||||
description: 'Environment',
|
||||
options: ['dev', 'staging', 'prod'],
|
||||
required: true,
|
||||
})
|
||||
```
|
||||
|
||||
### Multiple Values
|
||||
```typescript
|
||||
tags: Flags.string({
|
||||
char: 't',
|
||||
description: 'Tags',
|
||||
multiple: true,
|
||||
})
|
||||
// Usage: --tags=foo --tags=bar
|
||||
```
|
||||
|
||||
### Custom Flag
|
||||
```typescript
|
||||
date: Flags.custom<Date>({
|
||||
parse: async (input) => new Date(input),
|
||||
})
|
||||
```
|
||||
|
||||
## Argument Patterns
|
||||
|
||||
### Required Argument
|
||||
```typescript
|
||||
static args = {
|
||||
file: Args.string({
|
||||
description: 'File path',
|
||||
required: true,
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
### File Argument
|
||||
```typescript
|
||||
static args = {
|
||||
file: Args.file({
|
||||
description: 'Input file',
|
||||
exists: true, // Validates file exists
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
### Directory Argument
|
||||
```typescript
|
||||
static args = {
|
||||
dir: Args.directory({
|
||||
description: 'Target directory',
|
||||
exists: true,
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
## Output Patterns
|
||||
|
||||
### Simple Log
|
||||
```typescript
|
||||
this.log('Message')
|
||||
```
|
||||
|
||||
### Error with Exit
|
||||
```typescript
|
||||
this.error('Error message', { exit: 1 })
|
||||
```
|
||||
|
||||
### Warning
|
||||
```typescript
|
||||
this.warn('Warning message')
|
||||
```
|
||||
|
||||
### Spinner
|
||||
```typescript
|
||||
import { ux } from '@oclif/core'
|
||||
|
||||
ux.action.start('Processing')
|
||||
// ... work
|
||||
ux.action.stop('done')
|
||||
```
|
||||
|
||||
### Progress Bar
|
||||
```typescript
|
||||
import { ux } from '@oclif/core'
|
||||
|
||||
const total = 100
|
||||
ux.progress.start(total)
|
||||
for (let i = 0; i < total; i++) {
|
||||
ux.progress.update(i)
|
||||
}
|
||||
ux.progress.stop()
|
||||
```
|
||||
|
||||
### Table Output
|
||||
```typescript
|
||||
import { ux } from '@oclif/core'
|
||||
|
||||
ux.table(data, {
|
||||
id: {},
|
||||
name: {},
|
||||
status: { extended: true },
|
||||
})
|
||||
```
|
||||
|
||||
### Prompt
|
||||
```typescript
|
||||
import { ux } from '@oclif/core'
|
||||
|
||||
const name = await ux.prompt('What is your name?')
|
||||
const password = await ux.prompt('Password', { type: 'hide' })
|
||||
const confirmed = await ux.confirm('Continue? (y/n)')
|
||||
```
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
### Basic Test
|
||||
```typescript
|
||||
import { expect, test } from '@oclif/test'
|
||||
|
||||
test
|
||||
.stdout()
|
||||
.command(['mycommand', '--name', 'Test'])
|
||||
.it('runs command', ctx => {
|
||||
expect(ctx.stdout).to.contain('Test')
|
||||
})
|
||||
```
|
||||
|
||||
### Test with Error
|
||||
```typescript
|
||||
test
|
||||
.command(['mycommand'])
|
||||
.catch(error => {
|
||||
expect(error.message).to.contain('Missing')
|
||||
})
|
||||
.it('fails without flags')
|
||||
```
|
||||
|
||||
### Test with Mock
|
||||
```typescript
|
||||
test
|
||||
.nock('https://api.example.com', api =>
|
||||
api.get('/data').reply(200, { result: 'success' })
|
||||
)
|
||||
.stdout()
|
||||
.command(['mycommand'])
|
||||
.it('handles API call', ctx => {
|
||||
expect(ctx.stdout).to.contain('success')
|
||||
})
|
||||
```
|
||||
|
||||
### Test with Environment
|
||||
```typescript
|
||||
test
|
||||
.env({ API_KEY: 'test-key' })
|
||||
.stdout()
|
||||
.command(['mycommand'])
|
||||
.it('reads from env')
|
||||
```
|
||||
|
||||
## Plugin Patterns
|
||||
|
||||
### Create Plugin
|
||||
```bash
|
||||
./scripts/create-plugin.sh my-plugin
|
||||
```
|
||||
|
||||
### Link Plugin
|
||||
```bash
|
||||
mycli plugins:link ./plugin-my-plugin
|
||||
```
|
||||
|
||||
### Install Plugin
|
||||
```bash
|
||||
mycli plugins:install @mycli/plugin-name
|
||||
```
|
||||
|
||||
## Hook Patterns
|
||||
|
||||
### Init Hook
|
||||
```typescript
|
||||
import { Hook } from '@oclif/core'
|
||||
|
||||
const hook: Hook<'init'> = async function (opts) {
|
||||
// Runs before any command
|
||||
}
|
||||
|
||||
export default hook
|
||||
```
|
||||
|
||||
### Prerun Hook
|
||||
```typescript
|
||||
const hook: Hook<'prerun'> = async function (opts) {
|
||||
const { Command, argv } = opts
|
||||
// Runs before each command
|
||||
}
|
||||
```
|
||||
|
||||
## Common Commands
|
||||
|
||||
### Generate Documentation
|
||||
```bash
|
||||
npm run prepack
|
||||
# Generates oclif.manifest.json and updates README.md
|
||||
```
|
||||
|
||||
### Build
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Test
|
||||
```bash
|
||||
npm test
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
### Lint
|
||||
```bash
|
||||
npm run lint
|
||||
npm run lint:fix
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
### Validate Command
|
||||
```bash
|
||||
./scripts/validate-command.sh src/commands/mycommand.ts
|
||||
```
|
||||
|
||||
### Validate Plugin
|
||||
```bash
|
||||
./scripts/validate-plugin.sh ./my-plugin
|
||||
```
|
||||
|
||||
### Validate Tests
|
||||
```bash
|
||||
./scripts/validate-tests.sh
|
||||
```
|
||||
|
||||
## Configuration Patterns
|
||||
|
||||
### Read Config
|
||||
```typescript
|
||||
const configPath = path.join(this.config.home, '.myclirc')
|
||||
const config = await fs.readJson(configPath)
|
||||
```
|
||||
|
||||
### Write Config
|
||||
```typescript
|
||||
await fs.writeJson(configPath, config, { spaces: 2 })
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
```typescript
|
||||
const apiKey = process.env.API_KEY || this.error('API_KEY required')
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Try-Catch
|
||||
```typescript
|
||||
try {
|
||||
await riskyOperation()
|
||||
} catch (error) {
|
||||
this.error(`Operation failed: ${error.message}`, { exit: 1 })
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Error
|
||||
```typescript
|
||||
if (!valid) {
|
||||
this.error('Invalid input', {
|
||||
exit: 1,
|
||||
suggestions: ['Try --help for usage']
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Async Patterns
|
||||
|
||||
### Concurrent Operations
|
||||
```typescript
|
||||
const results = await Promise.all([
|
||||
operation1(),
|
||||
operation2(),
|
||||
operation3(),
|
||||
])
|
||||
```
|
||||
|
||||
### Sequential Operations
|
||||
```typescript
|
||||
for (const item of items) {
|
||||
await processItem(item)
|
||||
}
|
||||
```
|
||||
|
||||
### With Timeout
|
||||
```typescript
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), 5000)
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { signal: controller.signal })
|
||||
} finally {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Always provide clear descriptions for flags and commands
|
||||
2. Use char flags for common options (e.g., -v for verbose)
|
||||
3. Validate inputs early in the run() method
|
||||
4. Use ux.action.start/stop for long operations
|
||||
5. Handle errors gracefully with helpful messages
|
||||
6. Test both success and failure cases
|
||||
7. Generate documentation with oclif manifest
|
||||
8. Use TypeScript strict mode
|
||||
9. Follow naming conventions (kebab-case for commands)
|
||||
10. Keep commands focused and single-purpose
|
||||
Reference in New Issue
Block a user