Initial commit
This commit is contained in:
259
skills/oclif-patterns/SKILL.md
Normal file
259
skills/oclif-patterns/SKILL.md
Normal file
@@ -0,0 +1,259 @@
|
||||
---
|
||||
name: oclif-patterns
|
||||
description: Enterprise CLI patterns using oclif framework with TypeScript. Use when building oclif CLIs, creating plugins, implementing commands with flags/args, adding auto-documentation, testing CLI commands, or when user mentions oclif, enterprise CLI, TypeScript CLI, plugin system, or CLI testing.
|
||||
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
|
||||
---
|
||||
|
||||
# oclif Enterprise CLI Patterns
|
||||
|
||||
Provides comprehensive patterns for building production-grade CLIs with oclif framework.
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
### 1. Command Structure
|
||||
- Single and multi-command CLIs
|
||||
- Flag definitions (string, boolean, integer, custom)
|
||||
- Argument parsing with validation
|
||||
- Command inheritance and base classes
|
||||
- Async command execution
|
||||
|
||||
### 2. Plugin System
|
||||
- Installable plugins
|
||||
- Plugin discovery and loading
|
||||
- Hook system for extensibility
|
||||
- Plugin commands and lifecycle
|
||||
|
||||
### 3. Auto-Documentation
|
||||
- Auto-generated help text
|
||||
- README generation
|
||||
- Command reference docs
|
||||
- Flag and argument documentation
|
||||
|
||||
### 4. Testing Patterns
|
||||
- Command unit tests
|
||||
- Integration testing
|
||||
- Mock stdin/stdout
|
||||
- Fixture management
|
||||
- Test helpers
|
||||
|
||||
## Implementation Guide
|
||||
|
||||
### Command Creation
|
||||
|
||||
**Use templates**:
|
||||
- `templates/command-basic.ts` - Simple command with flags
|
||||
- `templates/command-advanced.ts` - Complex command with validation
|
||||
- `templates/command-async.ts` - Async operations
|
||||
- `templates/base-command.ts` - Custom base class
|
||||
|
||||
**Key patterns**:
|
||||
1. Import Command from '@oclif/core'
|
||||
2. Define flags using `Flags` object
|
||||
3. Define args using `Args` object
|
||||
4. Implement async run() method
|
||||
5. Use this.log() for output
|
||||
6. Use this.error() for errors
|
||||
|
||||
### Flag Patterns
|
||||
|
||||
**Common flags**:
|
||||
- String: `Flags.string({ description, required, default })`
|
||||
- Boolean: `Flags.boolean({ description, allowNo })`
|
||||
- Integer: `Flags.integer({ description, min, max })`
|
||||
- Custom: `Flags.custom<T>({ parse: async (input) => T })`
|
||||
- Multiple: `Flags.string({ multiple: true })`
|
||||
|
||||
**Best practices**:
|
||||
- Always provide clear descriptions
|
||||
- Use char for common short flags
|
||||
- Set required vs optional explicitly
|
||||
- Provide sensible defaults
|
||||
- Validate in parse function for custom flags
|
||||
|
||||
### Argument Patterns
|
||||
|
||||
**Definition**:
|
||||
```typescript
|
||||
static args = {
|
||||
name: Args.string({ description: 'Name', required: true }),
|
||||
file: Args.file({ description: 'File path', exists: true })
|
||||
}
|
||||
```
|
||||
|
||||
**Access in run()**:
|
||||
```typescript
|
||||
const { args } = await this.parse(MyCommand)
|
||||
```
|
||||
|
||||
### Plugin Development
|
||||
|
||||
**Use templates**:
|
||||
- `templates/plugin-package.json` - Plugin package.json
|
||||
- `templates/plugin-command.ts` - Plugin command structure
|
||||
- `templates/plugin-hooks.ts` - Hook implementations
|
||||
|
||||
**Plugin structure**:
|
||||
```
|
||||
my-plugin/
|
||||
├── package.json (oclif configuration)
|
||||
├── src/
|
||||
│ ├── commands/ (plugin commands)
|
||||
│ └── hooks/ (lifecycle hooks)
|
||||
├── test/ (plugin tests)
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### Testing Setup
|
||||
|
||||
**Use templates**:
|
||||
- `templates/test-command.ts` - Command test template
|
||||
- `templates/test-helpers.ts` - Test utilities
|
||||
- `templates/test-setup.ts` - Test configuration
|
||||
|
||||
**Testing approach**:
|
||||
1. Use @oclif/test for test helpers
|
||||
2. Mock stdin/stdout with fancy-test
|
||||
3. Test flag parsing separately
|
||||
4. Test command execution
|
||||
5. Test error handling
|
||||
6. Use fixtures for file operations
|
||||
|
||||
### Auto-Documentation
|
||||
|
||||
**Generated automatically**:
|
||||
- Command help via `--help` flag
|
||||
- README.md with command reference
|
||||
- Usage examples
|
||||
- Flag and argument tables
|
||||
|
||||
**Use scripts**:
|
||||
- `scripts/generate-docs.sh` - Generate all documentation
|
||||
- `scripts/update-readme.sh` - Update README with commands
|
||||
|
||||
## Quick Start Examples
|
||||
|
||||
### Create Basic Command
|
||||
```bash
|
||||
# Use template
|
||||
./scripts/create-command.sh my-command basic
|
||||
|
||||
# Results in: src/commands/my-command.ts
|
||||
```
|
||||
|
||||
### Create Plugin
|
||||
```bash
|
||||
# Use template
|
||||
./scripts/create-plugin.sh my-plugin
|
||||
|
||||
# Results in: plugin directory structure
|
||||
```
|
||||
|
||||
### Run Tests
|
||||
```bash
|
||||
# Use test helpers
|
||||
npm test
|
||||
# or with coverage
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
## Validation Scripts
|
||||
|
||||
**Available validators**:
|
||||
- `scripts/validate-command.sh` - Check command structure
|
||||
- `scripts/validate-plugin.sh` - Verify plugin structure
|
||||
- `scripts/validate-tests.sh` - Ensure test coverage
|
||||
|
||||
## Templates Reference
|
||||
|
||||
### TypeScript Commands
|
||||
1. `command-basic.ts` - Simple command pattern
|
||||
2. `command-advanced.ts` - Full-featured command
|
||||
3. `command-async.ts` - Async/await patterns
|
||||
4. `base-command.ts` - Custom base class
|
||||
5. `command-with-config.ts` - Configuration management
|
||||
|
||||
### Plugin System
|
||||
6. `plugin-package.json` - Plugin package.json
|
||||
7. `plugin-command.ts` - Plugin command
|
||||
8. `plugin-hooks.ts` - Hook implementations
|
||||
9. `plugin-manifest.json` - Plugin manifest
|
||||
|
||||
### Testing
|
||||
10. `test-command.ts` - Command unit test
|
||||
11. `test-helpers.ts` - Test utilities
|
||||
12. `test-setup.ts` - Test configuration
|
||||
13. `test-integration.ts` - Integration test
|
||||
|
||||
### Configuration
|
||||
14. `tsconfig.json` - TypeScript config
|
||||
15. `package.json` - oclif package.json
|
||||
16. `.eslintrc.json` - ESLint config
|
||||
|
||||
## Examples Directory
|
||||
|
||||
See `examples/` for complete working examples:
|
||||
- `examples/basic-cli/` - Simple CLI with commands
|
||||
- `examples/plugin-cli/` - CLI with plugin support
|
||||
- `examples/enterprise-cli/` - Full enterprise setup
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Error Handling
|
||||
```typescript
|
||||
if (!valid) {
|
||||
this.error('Invalid input', { exit: 1 })
|
||||
}
|
||||
```
|
||||
|
||||
### Spinner/Progress
|
||||
```typescript
|
||||
const spinner = ux.action.start('Processing')
|
||||
// ... work
|
||||
ux.action.stop()
|
||||
```
|
||||
|
||||
### Prompts
|
||||
```typescript
|
||||
const answer = await ux.prompt('Continue?')
|
||||
```
|
||||
|
||||
### Table Output
|
||||
```typescript
|
||||
ux.table(data, { columns: [...] })
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Node.js 18+
|
||||
- TypeScript 5+
|
||||
- @oclif/core ^3.0.0
|
||||
- @oclif/test for testing
|
||||
- Knowledge of TypeScript decorators (optional but helpful)
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Command Design**: Keep commands focused, single responsibility
|
||||
2. **Flags**: Use descriptive names, provide help text
|
||||
3. **Testing**: Test command parsing and execution separately
|
||||
4. **Documentation**: Let oclif generate docs, keep them updated
|
||||
5. **Plugins**: Design for extensibility from the start
|
||||
6. **Error Messages**: Provide actionable error messages
|
||||
7. **TypeScript**: Use strict mode, define proper types
|
||||
8. **Async**: Use async/await, handle promises properly
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Custom Flag Types
|
||||
Create reusable custom flag parsers for complex validation.
|
||||
|
||||
### Hook System
|
||||
Implement hooks for: init, prerun, postrun, command_not_found.
|
||||
|
||||
### Topic Commands
|
||||
Organize commands into topics (e.g., `mycli topic:command`).
|
||||
|
||||
### Auto-Update
|
||||
Use @oclif/plugin-update for automatic CLI updates.
|
||||
|
||||
### Analytics
|
||||
Integrate analytics to track command usage.
|
||||
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
|
||||
78
skills/oclif-patterns/scripts/create-command.sh
Executable file
78
skills/oclif-patterns/scripts/create-command.sh
Executable file
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Create oclif command from template
|
||||
# Usage: ./create-command.sh <command-name> <template-type>
|
||||
# Template types: basic, advanced, async
|
||||
|
||||
set -e
|
||||
|
||||
COMMAND_NAME="$1"
|
||||
TEMPLATE_TYPE="${2:-basic}"
|
||||
|
||||
if [ -z "$COMMAND_NAME" ]; then
|
||||
echo "Error: Command name is required"
|
||||
echo "Usage: ./create-command.sh <command-name> <template-type>"
|
||||
echo "Template types: basic, advanced, async"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate template type
|
||||
if [[ ! "$TEMPLATE_TYPE" =~ ^(basic|advanced|async)$ ]]; then
|
||||
echo "Error: Invalid template type. Must be: basic, advanced, or async"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get script directory
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TEMPLATE_DIR="$(dirname "$SCRIPT_DIR")/templates"
|
||||
|
||||
# Determine output directory
|
||||
if [ -d "src/commands" ]; then
|
||||
OUTPUT_DIR="src/commands"
|
||||
elif [ -d "commands" ]; then
|
||||
OUTPUT_DIR="commands"
|
||||
else
|
||||
echo "Error: Cannot find commands directory. Are you in the CLI project root?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Convert command name to proper format
|
||||
# e.g., "my-command" -> "MyCommand"
|
||||
COMMAND_CLASS=$(echo "$COMMAND_NAME" | sed -r 's/(^|-)([a-z])/\U\2/g')
|
||||
|
||||
# Determine output file path
|
||||
OUTPUT_FILE="$OUTPUT_DIR/${COMMAND_NAME}.ts"
|
||||
|
||||
# Check if file already exists
|
||||
if [ -f "$OUTPUT_FILE" ]; then
|
||||
echo "Error: Command file already exists: $OUTPUT_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Select template file
|
||||
TEMPLATE_FILE="$TEMPLATE_DIR/command-${TEMPLATE_TYPE}.ts"
|
||||
|
||||
if [ ! -f "$TEMPLATE_FILE" ]; then
|
||||
echo "Error: Template file not found: $TEMPLATE_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create command file from template
|
||||
echo "Creating command from template: $TEMPLATE_TYPE"
|
||||
cp "$TEMPLATE_FILE" "$OUTPUT_FILE"
|
||||
|
||||
# Replace placeholders
|
||||
sed -i "s/{{COMMAND_NAME}}/$COMMAND_CLASS/g" "$OUTPUT_FILE"
|
||||
sed -i "s/{{DESCRIPTION}}/Command description for $COMMAND_NAME/g" "$OUTPUT_FILE"
|
||||
|
||||
echo "✓ Created command: $OUTPUT_FILE"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Edit the command: $OUTPUT_FILE"
|
||||
echo " 2. Update the description and flags"
|
||||
echo " 3. Implement the run() method"
|
||||
echo " 4. Build: npm run build"
|
||||
echo " 5. Test: npm test"
|
||||
echo ""
|
||||
echo "Run the command:"
|
||||
echo " ./bin/run.js $COMMAND_NAME --help"
|
||||
127
skills/oclif-patterns/scripts/create-plugin.sh
Executable file
127
skills/oclif-patterns/scripts/create-plugin.sh
Executable file
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Create oclif plugin structure
|
||||
# Usage: ./create-plugin.sh <plugin-name>
|
||||
|
||||
set -e
|
||||
|
||||
PLUGIN_NAME="$1"
|
||||
|
||||
if [ -z "$PLUGIN_NAME" ]; then
|
||||
echo "Error: Plugin name is required"
|
||||
echo "Usage: ./create-plugin.sh <plugin-name>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get script directory
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TEMPLATE_DIR="$(dirname "$SCRIPT_DIR")/templates"
|
||||
|
||||
# Determine output directory
|
||||
PLUGIN_DIR="plugin-$PLUGIN_NAME"
|
||||
|
||||
if [ -d "$PLUGIN_DIR" ]; then
|
||||
echo "Error: Plugin directory already exists: $PLUGIN_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Creating plugin: $PLUGIN_NAME"
|
||||
|
||||
# Create directory structure
|
||||
mkdir -p "$PLUGIN_DIR/src/commands"
|
||||
mkdir -p "$PLUGIN_DIR/src/hooks"
|
||||
mkdir -p "$PLUGIN_DIR/test"
|
||||
|
||||
# Copy package.json template
|
||||
cp "$TEMPLATE_DIR/plugin-package.json" "$PLUGIN_DIR/package.json"
|
||||
|
||||
# Replace placeholders in package.json
|
||||
sed -i "s/{{PLUGIN_NAME}}/$PLUGIN_NAME/g" "$PLUGIN_DIR/package.json"
|
||||
sed -i "s/{{DESCRIPTION}}/Plugin for $PLUGIN_NAME/g" "$PLUGIN_DIR/package.json"
|
||||
sed -i "s/{{AUTHOR}}/Your Name/g" "$PLUGIN_DIR/package.json"
|
||||
sed -i "s/{{GITHUB_USER}}/yourusername/g" "$PLUGIN_DIR/package.json"
|
||||
|
||||
# Copy TypeScript config
|
||||
cp "$TEMPLATE_DIR/tsconfig.json" "$PLUGIN_DIR/tsconfig.json"
|
||||
|
||||
# Copy ESLint config
|
||||
cp "$TEMPLATE_DIR/.eslintrc.json" "$PLUGIN_DIR/.eslintrc.json"
|
||||
|
||||
# Create plugin command
|
||||
cp "$TEMPLATE_DIR/plugin-command.ts" "$PLUGIN_DIR/src/commands/$PLUGIN_NAME.ts"
|
||||
sed -i "s/{{PLUGIN_NAME}}/$PLUGIN_NAME/g" "$PLUGIN_DIR/src/commands/$PLUGIN_NAME.ts"
|
||||
sed -i "s/{{COMMAND_NAME}}/main/g" "$PLUGIN_DIR/src/commands/$PLUGIN_NAME.ts"
|
||||
sed -i "s/{{COMMAND_CLASS}}/Main/g" "$PLUGIN_DIR/src/commands/$PLUGIN_NAME.ts"
|
||||
sed -i "s/{{DESCRIPTION}}/Main plugin command/g" "$PLUGIN_DIR/src/commands/$PLUGIN_NAME.ts"
|
||||
|
||||
# Create hooks
|
||||
cp "$TEMPLATE_DIR/plugin-hooks.ts" "$PLUGIN_DIR/src/hooks/init.ts"
|
||||
sed -i "s/{{PLUGIN_NAME}}/$PLUGIN_NAME/g" "$PLUGIN_DIR/src/hooks/init.ts"
|
||||
|
||||
# Create index.ts
|
||||
cat > "$PLUGIN_DIR/src/index.ts" << EOF
|
||||
export { default as init } from './hooks/init'
|
||||
EOF
|
||||
|
||||
# Create README
|
||||
cat > "$PLUGIN_DIR/README.md" << EOF
|
||||
# @mycli/plugin-$PLUGIN_NAME
|
||||
|
||||
Plugin for mycli: $PLUGIN_NAME
|
||||
|
||||
## Installation
|
||||
|
||||
\`\`\`bash
|
||||
mycli plugins:install @mycli/plugin-$PLUGIN_NAME
|
||||
\`\`\`
|
||||
|
||||
## Usage
|
||||
|
||||
\`\`\`bash
|
||||
mycli $PLUGIN_NAME:main --help
|
||||
\`\`\`
|
||||
|
||||
## Commands
|
||||
|
||||
<!-- commands -->
|
||||
<!-- commandsstop -->
|
||||
|
||||
## Development
|
||||
|
||||
\`\`\`bash
|
||||
npm install
|
||||
npm run build
|
||||
npm test
|
||||
\`\`\`
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
EOF
|
||||
|
||||
# Create .gitignore
|
||||
cat > "$PLUGIN_DIR/.gitignore" << EOF
|
||||
*-debug.log
|
||||
*-error.log
|
||||
*.tgz
|
||||
.DS_Store
|
||||
/.nyc_output
|
||||
/dist
|
||||
/lib
|
||||
/package-lock.json
|
||||
/tmp
|
||||
node_modules
|
||||
oclif.manifest.json
|
||||
tsconfig.tsbuildinfo
|
||||
EOF
|
||||
|
||||
echo "✓ Created plugin structure: $PLUGIN_DIR"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " cd $PLUGIN_DIR"
|
||||
echo " npm install"
|
||||
echo " npm run build"
|
||||
echo " npm test"
|
||||
echo ""
|
||||
echo "To install plugin locally:"
|
||||
echo " mycli plugins:link $PWD/$PLUGIN_DIR"
|
||||
28
skills/oclif-patterns/scripts/generate-docs.sh
Executable file
28
skills/oclif-patterns/scripts/generate-docs.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Generate oclif documentation
|
||||
# Usage: ./generate-docs.sh
|
||||
|
||||
set -e
|
||||
|
||||
echo "Generating oclif documentation..."
|
||||
|
||||
# Check if oclif is installed
|
||||
if ! command -v oclif &> /dev/null; then
|
||||
echo "Error: oclif CLI not found. Install with: npm install -g oclif"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Generate manifest
|
||||
echo "→ Generating command manifest..."
|
||||
oclif manifest
|
||||
|
||||
# Generate README
|
||||
echo "→ Generating README..."
|
||||
oclif readme
|
||||
|
||||
echo "✓ Documentation generated successfully"
|
||||
echo ""
|
||||
echo "Generated files:"
|
||||
echo " - oclif.manifest.json (command metadata)"
|
||||
echo " - README.md (updated with command reference)"
|
||||
76
skills/oclif-patterns/scripts/validate-command.sh
Executable file
76
skills/oclif-patterns/scripts/validate-command.sh
Executable file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Validate oclif command structure
|
||||
# Usage: ./validate-command.sh <command-file>
|
||||
|
||||
set -e
|
||||
|
||||
COMMAND_FILE="$1"
|
||||
|
||||
if [ -z "$COMMAND_FILE" ]; then
|
||||
echo "Error: Command file path is required"
|
||||
echo "Usage: ./validate-command.sh <command-file>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$COMMAND_FILE" ]; then
|
||||
echo "Error: Command file not found: $COMMAND_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Validating command: $COMMAND_FILE"
|
||||
|
||||
# Check for required imports
|
||||
if ! grep -q "from '@oclif/core'" "$COMMAND_FILE"; then
|
||||
echo "✗ Missing import from @oclif/core"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Has @oclif/core import"
|
||||
|
||||
# Check for Command class
|
||||
if ! grep -q "extends Command" "$COMMAND_FILE"; then
|
||||
echo "✗ Missing 'extends Command'"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Extends Command class"
|
||||
|
||||
# Check for description
|
||||
if ! grep -q "static description" "$COMMAND_FILE"; then
|
||||
echo "⚠ Warning: Missing static description"
|
||||
else
|
||||
echo "✓ Has static description"
|
||||
fi
|
||||
|
||||
# Check for examples
|
||||
if ! grep -q "static examples" "$COMMAND_FILE"; then
|
||||
echo "⚠ Warning: Missing static examples"
|
||||
else
|
||||
echo "✓ Has static examples"
|
||||
fi
|
||||
|
||||
# Check for run method
|
||||
if ! grep -q "async run()" "$COMMAND_FILE"; then
|
||||
echo "✗ Missing async run() method"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Has async run() method"
|
||||
|
||||
# Check for proper flag access
|
||||
if grep -q "this.parse(" "$COMMAND_FILE"; then
|
||||
echo "✓ Properly parses flags"
|
||||
else
|
||||
echo "⚠ Warning: May not be parsing flags correctly"
|
||||
fi
|
||||
|
||||
# Check TypeScript
|
||||
if command -v tsc &> /dev/null; then
|
||||
echo "→ Checking TypeScript compilation..."
|
||||
if tsc --noEmit "$COMMAND_FILE" 2>/dev/null; then
|
||||
echo "✓ TypeScript compilation successful"
|
||||
else
|
||||
echo "⚠ Warning: TypeScript compilation has issues"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✓ Command validation complete"
|
||||
99
skills/oclif-patterns/scripts/validate-plugin.sh
Executable file
99
skills/oclif-patterns/scripts/validate-plugin.sh
Executable file
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Validate oclif plugin structure
|
||||
# Usage: ./validate-plugin.sh <plugin-directory>
|
||||
|
||||
set -e
|
||||
|
||||
PLUGIN_DIR="${1:-.}"
|
||||
|
||||
if [ ! -d "$PLUGIN_DIR" ]; then
|
||||
echo "Error: Plugin directory not found: $PLUGIN_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Validating plugin: $PLUGIN_DIR"
|
||||
|
||||
# Check for package.json
|
||||
if [ ! -f "$PLUGIN_DIR/package.json" ]; then
|
||||
echo "✗ Missing package.json"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Has package.json"
|
||||
|
||||
# Check for oclif configuration in package.json
|
||||
if ! grep -q '"oclif"' "$PLUGIN_DIR/package.json"; then
|
||||
echo "✗ Missing oclif configuration in package.json"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Has oclif configuration"
|
||||
|
||||
# Check for commands directory
|
||||
if [ ! -d "$PLUGIN_DIR/src/commands" ]; then
|
||||
echo "⚠ Warning: Missing src/commands directory"
|
||||
else
|
||||
echo "✓ Has src/commands directory"
|
||||
|
||||
# Check for at least one command
|
||||
COMMAND_COUNT=$(find "$PLUGIN_DIR/src/commands" -name "*.ts" | wc -l)
|
||||
if [ "$COMMAND_COUNT" -eq 0 ]; then
|
||||
echo "⚠ Warning: No commands found"
|
||||
else
|
||||
echo "✓ Has $COMMAND_COUNT command(s)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for hooks directory (optional)
|
||||
if [ -d "$PLUGIN_DIR/src/hooks" ]; then
|
||||
echo "✓ Has src/hooks directory"
|
||||
HOOK_COUNT=$(find "$PLUGIN_DIR/src/hooks" -name "*.ts" | wc -l)
|
||||
echo " ($HOOK_COUNT hook(s))"
|
||||
fi
|
||||
|
||||
# Check for test directory
|
||||
if [ ! -d "$PLUGIN_DIR/test" ]; then
|
||||
echo "⚠ Warning: Missing test directory"
|
||||
else
|
||||
echo "✓ Has test directory"
|
||||
|
||||
# Check for test files
|
||||
TEST_COUNT=$(find "$PLUGIN_DIR/test" -name "*.test.ts" | wc -l)
|
||||
if [ "$TEST_COUNT" -eq 0 ]; then
|
||||
echo "⚠ Warning: No test files found"
|
||||
else
|
||||
echo "✓ Has $TEST_COUNT test file(s)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for TypeScript config
|
||||
if [ ! -f "$PLUGIN_DIR/tsconfig.json" ]; then
|
||||
echo "⚠ Warning: Missing tsconfig.json"
|
||||
else
|
||||
echo "✓ Has tsconfig.json"
|
||||
fi
|
||||
|
||||
# Check for README
|
||||
if [ ! -f "$PLUGIN_DIR/README.md" ]; then
|
||||
echo "⚠ Warning: Missing README.md"
|
||||
else
|
||||
echo "✓ Has README.md"
|
||||
fi
|
||||
|
||||
# Check dependencies in package.json
|
||||
if grep -q '"@oclif/core"' "$PLUGIN_DIR/package.json"; then
|
||||
echo "✓ Has @oclif/core dependency"
|
||||
else
|
||||
echo "✗ Missing @oclif/core dependency"
|
||||
fi
|
||||
|
||||
# Check if plugin can be built
|
||||
if [ -f "$PLUGIN_DIR/package.json" ]; then
|
||||
if grep -q '"build"' "$PLUGIN_DIR/package.json"; then
|
||||
echo "✓ Has build script"
|
||||
else
|
||||
echo "⚠ Warning: Missing build script"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✓ Plugin validation complete"
|
||||
59
skills/oclif-patterns/scripts/validate-tests.sh
Executable file
59
skills/oclif-patterns/scripts/validate-tests.sh
Executable file
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Validate test coverage for oclif CLI
|
||||
# Usage: ./validate-tests.sh
|
||||
|
||||
set -e
|
||||
|
||||
echo "Validating test coverage..."
|
||||
|
||||
# Check if test directory exists
|
||||
if [ ! -d "test" ]; then
|
||||
echo "✗ Missing test directory"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Has test directory"
|
||||
|
||||
# Count test files
|
||||
TEST_COUNT=$(find test -name "*.test.ts" | wc -l)
|
||||
if [ "$TEST_COUNT" -eq 0 ]; then
|
||||
echo "✗ No test files found"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ Has $TEST_COUNT test file(s)"
|
||||
|
||||
# Count command files
|
||||
if [ -d "src/commands" ]; then
|
||||
COMMAND_COUNT=$(find src/commands -name "*.ts" | wc -l)
|
||||
echo " $COMMAND_COUNT command file(s) in src/commands"
|
||||
|
||||
# Check if each command has tests
|
||||
for cmd in src/commands/*.ts; do
|
||||
CMD_NAME=$(basename "$cmd" .ts)
|
||||
if [ ! -f "test/commands/$CMD_NAME.test.ts" ]; then
|
||||
echo "⚠ Warning: No test for command: $CMD_NAME"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Run tests if available
|
||||
if command -v npm &> /dev/null && grep -q '"test"' package.json; then
|
||||
echo ""
|
||||
echo "→ Running tests..."
|
||||
if npm test; then
|
||||
echo "✓ All tests passed"
|
||||
else
|
||||
echo "✗ Some tests failed"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for test coverage script
|
||||
if grep -q '"test:coverage"' package.json; then
|
||||
echo ""
|
||||
echo "→ Checking test coverage..."
|
||||
npm run test:coverage
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✓ Test validation complete"
|
||||
18
skills/oclif-patterns/templates/.eslintrc.json
Normal file
18
skills/oclif-patterns/templates/.eslintrc.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": [
|
||||
"oclif",
|
||||
"oclif-typescript"
|
||||
],
|
||||
"rules": {
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"unicorn/no-abusive-eslint-disable": "off",
|
||||
"unicorn/prefer-module": "off",
|
||||
"unicorn/prefer-top-level-await": "off",
|
||||
"valid-jsdoc": "off",
|
||||
"no-console": "warn",
|
||||
"no-warning-comments": "warn",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"perfectionist/sort-objects": "off"
|
||||
}
|
||||
}
|
||||
192
skills/oclif-patterns/templates/base-command.ts
Normal file
192
skills/oclif-patterns/templates/base-command.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { Command, Flags } from '@oclif/core'
|
||||
import * as fs from 'fs-extra'
|
||||
import * as path from 'path'
|
||||
|
||||
/**
|
||||
* Base command class with common functionality for all commands
|
||||
* Extend this class instead of Command for consistent behavior
|
||||
*/
|
||||
export default abstract class BaseCommand extends Command {
|
||||
// Global flags available to all commands
|
||||
static baseFlags = {
|
||||
config: Flags.string({
|
||||
char: 'c',
|
||||
description: 'Path to config file',
|
||||
env: 'CLI_CONFIG',
|
||||
}),
|
||||
'log-level': Flags.string({
|
||||
description: 'Log level',
|
||||
options: ['error', 'warn', 'info', 'debug'],
|
||||
default: 'info',
|
||||
}),
|
||||
json: Flags.boolean({
|
||||
description: 'Output as JSON',
|
||||
default: false,
|
||||
}),
|
||||
}
|
||||
|
||||
protected config_: any = null
|
||||
|
||||
/**
|
||||
* Initialize command - load config, setup logging
|
||||
*/
|
||||
async init(): Promise<void> {
|
||||
await super.init()
|
||||
await this.loadConfig()
|
||||
this.setupLogging()
|
||||
}
|
||||
|
||||
/**
|
||||
* Load configuration file
|
||||
*/
|
||||
private async loadConfig(): Promise<void> {
|
||||
const { flags } = await this.parse(this.constructor as typeof BaseCommand)
|
||||
|
||||
if (flags.config) {
|
||||
if (!await fs.pathExists(flags.config)) {
|
||||
this.error(`Config file not found: ${flags.config}`, { exit: 1 })
|
||||
}
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(flags.config, 'utf-8')
|
||||
this.config_ = JSON.parse(content)
|
||||
this.debug(`Loaded config from ${flags.config}`)
|
||||
} catch (error) {
|
||||
this.error(`Failed to parse config file: ${error instanceof Error ? error.message : 'Unknown error'}`, {
|
||||
exit: 1,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Try to load default config locations
|
||||
const defaultLocations = [
|
||||
path.join(process.cwd(), '.clirc'),
|
||||
path.join(process.cwd(), '.cli.json'),
|
||||
path.join(this.config.home, '.config', 'cli', 'config.json'),
|
||||
]
|
||||
|
||||
for (const location of defaultLocations) {
|
||||
if (await fs.pathExists(location)) {
|
||||
try {
|
||||
const content = await fs.readFile(location, 'utf-8')
|
||||
this.config_ = JSON.parse(content)
|
||||
this.debug(`Loaded config from ${location}`)
|
||||
break
|
||||
} catch {
|
||||
// Ignore parse errors for default configs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup logging based on log-level flag
|
||||
*/
|
||||
private setupLogging(): void {
|
||||
// Implementation would setup actual logging library
|
||||
this.debug('Logging initialized')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get config value with dot notation support
|
||||
*/
|
||||
protected getConfig(key: string, defaultValue?: any): any {
|
||||
if (!this.config_) return defaultValue
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* Output data respecting --json flag
|
||||
*/
|
||||
protected output(data: any, message?: string): void {
|
||||
const { flags } = this.parse(this.constructor as typeof BaseCommand)
|
||||
|
||||
if (flags.json) {
|
||||
this.log(JSON.stringify(data, null, 2))
|
||||
} else if (message) {
|
||||
this.log(message)
|
||||
} else if (typeof data === 'string') {
|
||||
this.log(data)
|
||||
} else {
|
||||
this.log(JSON.stringify(data, null, 2))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced error handling
|
||||
*/
|
||||
protected handleError(error: Error, context?: string): never {
|
||||
const message = context ? `${context}: ${error.message}` : error.message
|
||||
|
||||
if (this.config.debug) {
|
||||
this.error(error.stack || message, { exit: 1 })
|
||||
} else {
|
||||
this.error(message, { exit: 1 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log only if verbose/debug mode
|
||||
*/
|
||||
protected debug(message: string): void {
|
||||
const { flags } = this.parse(this.constructor as typeof BaseCommand)
|
||||
|
||||
if (flags['log-level'] === 'debug') {
|
||||
this.log(`[DEBUG] ${message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate required environment variables
|
||||
*/
|
||||
protected requireEnv(vars: string[]): void {
|
||||
const missing = vars.filter(v => !process.env[v])
|
||||
|
||||
if (missing.length > 0) {
|
||||
this.error(
|
||||
`Missing required environment variables: ${missing.join(', ')}`,
|
||||
{ exit: 1 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if running in CI environment
|
||||
*/
|
||||
protected isCI(): boolean {
|
||||
return Boolean(process.env.CI)
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt user unless in CI or non-interactive mode
|
||||
*/
|
||||
protected async prompt(message: string, defaultValue?: string): Promise<string> {
|
||||
if (this.isCI()) {
|
||||
return defaultValue || ''
|
||||
}
|
||||
|
||||
const { default: inquirer } = await import('inquirer')
|
||||
const { value } = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'value',
|
||||
message,
|
||||
default: defaultValue,
|
||||
},
|
||||
])
|
||||
|
||||
return value
|
||||
}
|
||||
}
|
||||
146
skills/oclif-patterns/templates/command-advanced.ts
Normal file
146
skills/oclif-patterns/templates/command-advanced.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { Command, Flags, Args, ux } from '@oclif/core'
|
||||
import * as fs from 'fs-extra'
|
||||
import * as path from 'path'
|
||||
|
||||
export default class {{COMMAND_NAME}} extends Command {
|
||||
static description = '{{DESCRIPTION}}'
|
||||
|
||||
static examples = [
|
||||
'<%= config.bin %> <%= command.id %> myfile.txt --output result.json',
|
||||
'<%= config.bin %> <%= command.id %> data.csv --format json --validate',
|
||||
]
|
||||
|
||||
static flags = {
|
||||
output: Flags.string({
|
||||
char: 'o',
|
||||
description: 'Output file path',
|
||||
required: true,
|
||||
}),
|
||||
format: Flags.string({
|
||||
char: 'f',
|
||||
description: 'Output format',
|
||||
options: ['json', 'yaml', 'csv'],
|
||||
default: 'json',
|
||||
}),
|
||||
validate: Flags.boolean({
|
||||
description: 'Validate input before processing',
|
||||
default: false,
|
||||
}),
|
||||
force: Flags.boolean({
|
||||
description: 'Overwrite existing output file',
|
||||
default: false,
|
||||
}),
|
||||
verbose: Flags.boolean({
|
||||
char: 'v',
|
||||
description: 'Verbose output',
|
||||
default: false,
|
||||
}),
|
||||
}
|
||||
|
||||
static args = {
|
||||
file: Args.string({
|
||||
description: 'Input file to process',
|
||||
required: true,
|
||||
}),
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
const { args, flags } = await this.parse({{COMMAND_NAME}})
|
||||
|
||||
// Validation
|
||||
await this.validateInput(args.file, flags)
|
||||
|
||||
// Processing with spinner
|
||||
ux.action.start('Processing file')
|
||||
|
||||
try {
|
||||
const result = await this.processFile(args.file, flags)
|
||||
ux.action.stop('done')
|
||||
|
||||
// Output
|
||||
await this.writeOutput(result, flags.output, flags.format, flags.force)
|
||||
|
||||
this.log(`✓ Successfully processed ${args.file}`)
|
||||
this.log(`✓ Output written to ${flags.output}`)
|
||||
} catch (error) {
|
||||
ux.action.stop('failed')
|
||||
this.error(`Processing failed: ${error instanceof Error ? error.message : 'Unknown error'}`, {
|
||||
exit: 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private async validateInput(file: string, flags: any): Promise<void> {
|
||||
// Check file exists
|
||||
if (!await fs.pathExists(file)) {
|
||||
this.error(`File not found: ${file}`, { exit: 1 })
|
||||
}
|
||||
|
||||
// Check output directory exists
|
||||
const outputDir = path.dirname(flags.output)
|
||||
if (!await fs.pathExists(outputDir)) {
|
||||
this.error(`Output directory not found: ${outputDir}`, { exit: 1 })
|
||||
}
|
||||
|
||||
// Check output file doesn't exist (unless force)
|
||||
if (!flags.force && await fs.pathExists(flags.output)) {
|
||||
const overwrite = await ux.confirm(`Output file ${flags.output} exists. Overwrite? (y/n)`)
|
||||
if (!overwrite) {
|
||||
this.error('Operation cancelled', { exit: 0 })
|
||||
}
|
||||
}
|
||||
|
||||
if (flags.validate) {
|
||||
if (flags.verbose) {
|
||||
this.log('Running validation...')
|
||||
}
|
||||
// Perform custom validation here
|
||||
}
|
||||
}
|
||||
|
||||
private async processFile(file: string, flags: any): Promise<any> {
|
||||
// Read input file
|
||||
const content = await fs.readFile(file, 'utf-8')
|
||||
|
||||
if (flags.verbose) {
|
||||
this.log(`Read ${content.length} bytes from ${file}`)
|
||||
}
|
||||
|
||||
// Process content (placeholder - implement your logic)
|
||||
const result = {
|
||||
source: file,
|
||||
format: flags.format,
|
||||
timestamp: new Date().toISOString(),
|
||||
data: content,
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private async writeOutput(
|
||||
result: any,
|
||||
output: string,
|
||||
format: string,
|
||||
force: boolean
|
||||
): Promise<void> {
|
||||
let content: string
|
||||
|
||||
switch (format) {
|
||||
case 'json':
|
||||
content = JSON.stringify(result, null, 2)
|
||||
break
|
||||
case 'yaml':
|
||||
// Implement YAML formatting
|
||||
content = JSON.stringify(result, null, 2) // Placeholder
|
||||
break
|
||||
case 'csv':
|
||||
// Implement CSV formatting
|
||||
content = JSON.stringify(result, null, 2) // Placeholder
|
||||
break
|
||||
default:
|
||||
content = JSON.stringify(result, null, 2)
|
||||
}
|
||||
|
||||
await fs.writeFile(output, content, 'utf-8')
|
||||
}
|
||||
}
|
||||
180
skills/oclif-patterns/templates/command-async.ts
Normal file
180
skills/oclif-patterns/templates/command-async.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { Command, Flags, ux } from '@oclif/core'
|
||||
|
||||
export default class {{COMMAND_NAME}} extends Command {
|
||||
static description = 'Async command with parallel operations and error handling'
|
||||
|
||||
static examples = [
|
||||
'<%= config.bin %> <%= command.id %> --urls https://api1.com,https://api2.com',
|
||||
'<%= config.bin %> <%= command.id %> --urls https://api.com --retry 3 --timeout 5000',
|
||||
]
|
||||
|
||||
static flags = {
|
||||
urls: Flags.string({
|
||||
char: 'u',
|
||||
description: 'Comma-separated list of URLs to fetch',
|
||||
required: true,
|
||||
}),
|
||||
parallel: Flags.integer({
|
||||
char: 'p',
|
||||
description: 'Maximum parallel operations',
|
||||
default: 5,
|
||||
}),
|
||||
timeout: Flags.integer({
|
||||
char: 't',
|
||||
description: 'Request timeout in milliseconds',
|
||||
default: 10000,
|
||||
}),
|
||||
retry: Flags.integer({
|
||||
char: 'r',
|
||||
description: 'Number of retry attempts',
|
||||
default: 0,
|
||||
}),
|
||||
verbose: Flags.boolean({
|
||||
char: 'v',
|
||||
description: 'Verbose output',
|
||||
default: false,
|
||||
}),
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
const { flags } = await this.parse({{COMMAND_NAME}})
|
||||
|
||||
const urls = flags.urls.split(',').map(u => u.trim())
|
||||
|
||||
if (flags.verbose) {
|
||||
this.log(`Processing ${urls.length} URLs with max ${flags.parallel} parallel operations`)
|
||||
}
|
||||
|
||||
// Process with progress bar
|
||||
ux.action.start(`Fetching ${urls.length} URLs`)
|
||||
|
||||
try {
|
||||
const results = await this.fetchWithConcurrency(urls, flags)
|
||||
ux.action.stop('done')
|
||||
|
||||
// Display results table
|
||||
this.displayResults(results, flags.verbose)
|
||||
} catch (error) {
|
||||
ux.action.stop('failed')
|
||||
this.error(`Operation failed: ${error instanceof Error ? error.message : 'Unknown error'}`, {
|
||||
exit: 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchWithConcurrency(
|
||||
urls: string[],
|
||||
flags: any
|
||||
): Promise<Array<{ url: string; status: string; data?: any; error?: string }>> {
|
||||
const results: Array<{ url: string; status: string; data?: any; error?: string }> = []
|
||||
const chunks = this.chunkArray(urls, flags.parallel)
|
||||
|
||||
for (const chunk of chunks) {
|
||||
const promises = chunk.map(url => this.fetchWithRetry(url, flags))
|
||||
const chunkResults = await Promise.allSettled(promises)
|
||||
|
||||
chunkResults.forEach((result, index) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
results.push({
|
||||
url: chunk[index],
|
||||
status: 'success',
|
||||
data: result.value,
|
||||
})
|
||||
} else {
|
||||
results.push({
|
||||
url: chunk[index],
|
||||
status: 'failed',
|
||||
error: result.reason.message,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
private async fetchWithRetry(url: string, flags: any): Promise<any> {
|
||||
let lastError: Error | null = null
|
||||
|
||||
for (let attempt = 0; attempt <= flags.retry; attempt++) {
|
||||
try {
|
||||
if (flags.verbose && attempt > 0) {
|
||||
this.log(`Retry ${attempt}/${flags.retry} for ${url}`)
|
||||
}
|
||||
|
||||
const response = await this.fetchWithTimeout(url, flags.timeout)
|
||||
return response
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error : new Error('Unknown error')
|
||||
if (attempt < flags.retry) {
|
||||
// Exponential backoff
|
||||
await this.sleep(Math.pow(2, attempt) * 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError
|
||||
}
|
||||
|
||||
private async fetchWithTimeout(url: string, timeout: number): Promise<any> {
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { signal: controller.signal })
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data
|
||||
} finally {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
|
||||
private displayResults(
|
||||
results: Array<{ url: string; status: string; data?: any; error?: string }>,
|
||||
verbose: boolean
|
||||
): void {
|
||||
const successCount = results.filter(r => r.status === 'success').length
|
||||
const failCount = results.filter(r => r.status === 'failed').length
|
||||
|
||||
this.log(`\nResults: ${successCount} successful, ${failCount} failed\n`)
|
||||
|
||||
if (verbose) {
|
||||
ux.table(results, {
|
||||
url: {
|
||||
header: 'URL',
|
||||
},
|
||||
status: {
|
||||
header: 'Status',
|
||||
},
|
||||
error: {
|
||||
header: 'Error',
|
||||
get: row => row.error || '-',
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// Show only failures
|
||||
const failures = results.filter(r => r.status === 'failed')
|
||||
if (failures.length > 0) {
|
||||
this.log('Failed URLs:')
|
||||
failures.forEach(f => this.log(` × ${f.url}: ${f.error}`))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private chunkArray<T>(array: T[], size: number): T[][] {
|
||||
const chunks: T[][] = []
|
||||
for (let i = 0; i < array.length; i += size) {
|
||||
chunks.push(array.slice(i, i + size))
|
||||
}
|
||||
return chunks
|
||||
}
|
||||
|
||||
private sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
}
|
||||
44
skills/oclif-patterns/templates/command-basic.ts
Normal file
44
skills/oclif-patterns/templates/command-basic.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Command, Flags } from '@oclif/core'
|
||||
|
||||
export default class {{COMMAND_NAME}} extends Command {
|
||||
static description = '{{DESCRIPTION}}'
|
||||
|
||||
static examples = [
|
||||
'<%= config.bin %> <%= command.id %> --name World',
|
||||
'<%= config.bin %> <%= command.id %> --name "John Doe" --verbose',
|
||||
]
|
||||
|
||||
static flags = {
|
||||
name: Flags.string({
|
||||
char: 'n',
|
||||
description: 'Name to use',
|
||||
required: true,
|
||||
}),
|
||||
verbose: Flags.boolean({
|
||||
char: 'v',
|
||||
description: 'Show verbose output',
|
||||
default: false,
|
||||
}),
|
||||
force: Flags.boolean({
|
||||
char: 'f',
|
||||
description: 'Force operation',
|
||||
default: false,
|
||||
}),
|
||||
}
|
||||
|
||||
static args = {}
|
||||
|
||||
async run(): Promise<void> {
|
||||
const { flags } = await this.parse({{COMMAND_NAME}})
|
||||
|
||||
if (flags.verbose) {
|
||||
this.log('Verbose mode enabled')
|
||||
}
|
||||
|
||||
this.log(`Hello, ${flags.name}!`)
|
||||
|
||||
if (flags.force) {
|
||||
this.log('Force mode: proceeding without confirmation')
|
||||
}
|
||||
}
|
||||
}
|
||||
198
skills/oclif-patterns/templates/command-with-config.ts
Normal file
198
skills/oclif-patterns/templates/command-with-config.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import { Command, Flags } from '@oclif/core'
|
||||
import * as fs from 'fs-extra'
|
||||
import * as path from 'path'
|
||||
import * as os from 'os'
|
||||
|
||||
export default class {{COMMAND_NAME}} extends Command {
|
||||
static description = 'Command with configuration file management'
|
||||
|
||||
static examples = [
|
||||
'<%= config.bin %> <%= command.id %> --init',
|
||||
'<%= config.bin %> <%= command.id %> --set key=value',
|
||||
'<%= config.bin %> <%= command.id %> --get key',
|
||||
'<%= config.bin %> <%= command.id %> --list',
|
||||
]
|
||||
|
||||
static flags = {
|
||||
init: Flags.boolean({
|
||||
description: 'Initialize configuration file',
|
||||
exclusive: ['set', 'get', 'list'],
|
||||
}),
|
||||
set: Flags.string({
|
||||
description: 'Set configuration value (key=value)',
|
||||
multiple: true,
|
||||
exclusive: ['init', 'get', 'list'],
|
||||
}),
|
||||
get: Flags.string({
|
||||
description: 'Get configuration value by key',
|
||||
exclusive: ['init', 'set', 'list'],
|
||||
}),
|
||||
list: Flags.boolean({
|
||||
description: 'List all configuration values',
|
||||
exclusive: ['init', 'set', 'get'],
|
||||
}),
|
||||
global: Flags.boolean({
|
||||
char: 'g',
|
||||
description: 'Use global config instead of local',
|
||||
default: false,
|
||||
}),
|
||||
}
|
||||
|
||||
private readonly DEFAULT_CONFIG = {
|
||||
version: '1.0.0',
|
||||
settings: {
|
||||
theme: 'default',
|
||||
verbose: false,
|
||||
timeout: 30000,
|
||||
},
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
const { flags } = await this.parse({{COMMAND_NAME}})
|
||||
const configPath = this.getConfigPath(flags.global)
|
||||
|
||||
if (flags.init) {
|
||||
await this.initConfig(configPath)
|
||||
} else if (flags.set && flags.set.length > 0) {
|
||||
await this.setConfig(configPath, flags.set)
|
||||
} else if (flags.get) {
|
||||
await this.getConfigValue(configPath, flags.get)
|
||||
} else if (flags.list) {
|
||||
await this.listConfig(configPath)
|
||||
} else {
|
||||
// Default: show current config location
|
||||
this.log(`Config location: ${configPath}`)
|
||||
if (await fs.pathExists(configPath)) {
|
||||
this.log('Config file exists')
|
||||
await this.listConfig(configPath)
|
||||
} else {
|
||||
this.log('Config file does not exist. Run with --init to create.')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getConfigPath(global: boolean): string {
|
||||
if (global) {
|
||||
return path.join(os.homedir(), '.config', 'mycli', 'config.json')
|
||||
} else {
|
||||
return path.join(process.cwd(), '.myclirc')
|
||||
}
|
||||
}
|
||||
|
||||
private async initConfig(configPath: string): Promise<void> {
|
||||
if (await fs.pathExists(configPath)) {
|
||||
this.error('Config file already exists. Remove it first or use --set to update.', {
|
||||
exit: 1,
|
||||
})
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
await fs.ensureDir(path.dirname(configPath))
|
||||
|
||||
// Write default config
|
||||
await fs.writeJson(configPath, this.DEFAULT_CONFIG, { spaces: 2 })
|
||||
|
||||
this.log(`✓ Initialized config file at: ${configPath}`)
|
||||
}
|
||||
|
||||
private async setConfig(configPath: string, settings: string[]): Promise<void> {
|
||||
// Load existing config or use default
|
||||
let config = this.DEFAULT_CONFIG
|
||||
if (await fs.pathExists(configPath)) {
|
||||
config = await fs.readJson(configPath)
|
||||
} else {
|
||||
await fs.ensureDir(path.dirname(configPath))
|
||||
}
|
||||
|
||||
// Parse and set values
|
||||
for (const setting of settings) {
|
||||
const [key, ...valueParts] = setting.split('=')
|
||||
const value = valueParts.join('=')
|
||||
|
||||
if (!value) {
|
||||
this.warn(`Skipping invalid setting: ${setting}`)
|
||||
continue
|
||||
}
|
||||
|
||||
// Support dot notation
|
||||
this.setNestedValue(config, key, this.parseValue(value))
|
||||
this.log(`✓ Set ${key} = ${value}`)
|
||||
}
|
||||
|
||||
// Save config
|
||||
await fs.writeJson(configPath, config, { spaces: 2 })
|
||||
this.log(`\n✓ Updated config file: ${configPath}`)
|
||||
}
|
||||
|
||||
private async getConfigValue(configPath: string, key: string): Promise<void> {
|
||||
if (!await fs.pathExists(configPath)) {
|
||||
this.error('Config file does not exist. Run with --init first.', { exit: 1 })
|
||||
}
|
||||
|
||||
const config = await fs.readJson(configPath)
|
||||
const value = this.getNestedValue(config, key)
|
||||
|
||||
if (value === undefined) {
|
||||
this.error(`Key not found: ${key}`, { exit: 1 })
|
||||
}
|
||||
|
||||
this.log(typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value))
|
||||
}
|
||||
|
||||
private async listConfig(configPath: string): Promise<void> {
|
||||
if (!await fs.pathExists(configPath)) {
|
||||
this.error('Config file does not exist. Run with --init first.', { exit: 1 })
|
||||
}
|
||||
|
||||
const config = await fs.readJson(configPath)
|
||||
this.log('\nCurrent configuration:')
|
||||
this.log(JSON.stringify(config, null, 2))
|
||||
}
|
||||
|
||||
private setNestedValue(obj: any, path: string, value: any): void {
|
||||
const keys = path.split('.')
|
||||
const lastKey = keys.pop()!
|
||||
let current = obj
|
||||
|
||||
for (const key of keys) {
|
||||
if (!(key in current) || typeof current[key] !== 'object') {
|
||||
current[key] = {}
|
||||
}
|
||||
current = current[key]
|
||||
}
|
||||
|
||||
current[lastKey] = value
|
||||
}
|
||||
|
||||
private getNestedValue(obj: any, path: string): any {
|
||||
const keys = path.split('.')
|
||||
let current = obj
|
||||
|
||||
for (const key of keys) {
|
||||
if (current && typeof current === 'object' && key in current) {
|
||||
current = current[key]
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
return current
|
||||
}
|
||||
|
||||
private parseValue(value: string): any {
|
||||
// Try to parse as JSON
|
||||
if (value === 'true') return true
|
||||
if (value === 'false') return false
|
||||
if (value === 'null') return null
|
||||
if (/^-?\d+$/.test(value)) return parseInt(value, 10)
|
||||
if (/^-?\d+\.\d+$/.test(value)) return parseFloat(value)
|
||||
if (value.startsWith('{') || value.startsWith('[')) {
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
92
skills/oclif-patterns/templates/package.json
Normal file
92
skills/oclif-patterns/templates/package.json
Normal file
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"name": "{{CLI_NAME}}",
|
||||
"version": "1.0.0",
|
||||
"description": "{{DESCRIPTION}}",
|
||||
"author": "{{AUTHOR}}",
|
||||
"license": "MIT",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"bin": {
|
||||
"{{CLI_BIN}}": "./bin/run.js"
|
||||
},
|
||||
"files": [
|
||||
"/bin",
|
||||
"/lib",
|
||||
"/oclif.manifest.json"
|
||||
],
|
||||
"keywords": [
|
||||
"oclif",
|
||||
"cli",
|
||||
"{{CLI_NAME}}"
|
||||
],
|
||||
"oclif": {
|
||||
"bin": "{{CLI_BIN}}",
|
||||
"dirname": "{{CLI_BIN}}",
|
||||
"commands": "./lib/commands",
|
||||
"plugins": [
|
||||
"@oclif/plugin-help",
|
||||
"@oclif/plugin-plugins"
|
||||
],
|
||||
"topicSeparator": ":",
|
||||
"topics": {
|
||||
"config": {
|
||||
"description": "Manage CLI configuration"
|
||||
}
|
||||
},
|
||||
"hooks": {
|
||||
"init": "./lib/hooks/init",
|
||||
"prerun": "./lib/hooks/prerun"
|
||||
},
|
||||
"additionalHelpFlags": ["-h"],
|
||||
"additionalVersionFlags": ["-v"]
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -b",
|
||||
"clean": "rm -rf lib && rm -f tsconfig.tsbuildinfo",
|
||||
"dev": "ts-node src/index.ts",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"lint:fix": "eslint . --ext .ts --fix",
|
||||
"postpack": "rm -f oclif.manifest.json",
|
||||
"prepack": "npm run build && oclif manifest && oclif readme",
|
||||
"test": "mocha --forbid-only \"test/**/*.test.ts\"",
|
||||
"test:watch": "mocha --watch \"test/**/*.test.ts\"",
|
||||
"test:coverage": "nyc npm test",
|
||||
"version": "oclif readme && git add README.md",
|
||||
"posttest": "npm run lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@oclif/core": "^3.0.0",
|
||||
"@oclif/plugin-help": "^6.0.0",
|
||||
"@oclif/plugin-plugins": "^4.0.0",
|
||||
"fs-extra": "^11.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@oclif/test": "^3.0.0",
|
||||
"@types/chai": "^4",
|
||||
"@types/fs-extra": "^11",
|
||||
"@types/mocha": "^10",
|
||||
"@types/node": "^18",
|
||||
"@typescript-eslint/eslint-plugin": "^6",
|
||||
"@typescript-eslint/parser": "^6",
|
||||
"chai": "^4",
|
||||
"eslint": "^8",
|
||||
"eslint-config-oclif": "^5",
|
||||
"eslint-config-oclif-typescript": "^3",
|
||||
"mocha": "^10",
|
||||
"nyc": "^15",
|
||||
"oclif": "^4.0.0",
|
||||
"ts-node": "^10",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/{{GITHUB_USER}}/{{CLI_NAME}}.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/{{GITHUB_USER}}/{{CLI_NAME}}/issues"
|
||||
},
|
||||
"homepage": "https://github.com/{{GITHUB_USER}}/{{CLI_NAME}}#readme"
|
||||
}
|
||||
54
skills/oclif-patterns/templates/plugin-command.ts
Normal file
54
skills/oclif-patterns/templates/plugin-command.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Command, Flags } from '@oclif/core'
|
||||
|
||||
/**
|
||||
* Plugin command that extends the main CLI
|
||||
*
|
||||
* This command will be available as:
|
||||
* mycli {{PLUGIN_NAME}}:{{COMMAND_NAME}}
|
||||
*/
|
||||
export default class {{COMMAND_CLASS}} extends Command {
|
||||
static description = 'Plugin command: {{DESCRIPTION}}'
|
||||
|
||||
static examples = [
|
||||
'<%= config.bin %> {{PLUGIN_NAME}}:{{COMMAND_NAME}} --option value',
|
||||
]
|
||||
|
||||
static flags = {
|
||||
option: Flags.string({
|
||||
char: 'o',
|
||||
description: 'Plugin-specific option',
|
||||
}),
|
||||
verbose: Flags.boolean({
|
||||
char: 'v',
|
||||
description: 'Verbose output',
|
||||
default: false,
|
||||
}),
|
||||
}
|
||||
|
||||
static args = {}
|
||||
|
||||
async run(): Promise<void> {
|
||||
const { flags } = await this.parse({{COMMAND_CLASS}})
|
||||
|
||||
if (flags.verbose) {
|
||||
this.log('Running plugin command...')
|
||||
}
|
||||
|
||||
// Plugin-specific logic here
|
||||
this.log(`Plugin {{PLUGIN_NAME}} executing: ${this.id}`)
|
||||
|
||||
if (flags.option) {
|
||||
this.log(`Option value: ${flags.option}`)
|
||||
}
|
||||
|
||||
// Access main CLI config if needed
|
||||
const cliConfig = this.config
|
||||
this.log(`CLI version: ${cliConfig.version}`)
|
||||
|
||||
// You can also access other plugins
|
||||
const plugins = cliConfig.plugins
|
||||
if (flags.verbose) {
|
||||
this.log(`Loaded plugins: ${plugins.map(p => p.name).join(', ')}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
90
skills/oclif-patterns/templates/plugin-hooks.ts
Normal file
90
skills/oclif-patterns/templates/plugin-hooks.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { Hook } from '@oclif/core'
|
||||
|
||||
/**
|
||||
* oclif Hook Types:
|
||||
* - init: Runs before any command
|
||||
* - prerun: Runs before a command's run method
|
||||
* - postrun: Runs after a command's run method
|
||||
* - command_not_found: Runs when command not found
|
||||
*/
|
||||
|
||||
/**
|
||||
* Init hook - runs before any command
|
||||
*/
|
||||
export const init: Hook<'init'> = async function (opts) {
|
||||
// Access configuration
|
||||
const { config } = opts
|
||||
|
||||
// Plugin initialization logic
|
||||
this.log('Plugin {{PLUGIN_NAME}} initialized')
|
||||
|
||||
// Example: Check for required environment variables
|
||||
if (!process.env.PLUGIN_API_KEY) {
|
||||
this.warn('PLUGIN_API_KEY not set - some features may not work')
|
||||
}
|
||||
|
||||
// Example: Load plugin configuration
|
||||
try {
|
||||
// Load config from default locations
|
||||
} catch (error) {
|
||||
this.debug('Failed to load plugin config')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prerun hook - runs before each command
|
||||
*/
|
||||
export const prerun: Hook<'prerun'> = async function (opts) {
|
||||
const { Command, argv } = opts
|
||||
|
||||
// Log command execution (in debug mode)
|
||||
this.debug(`Executing command: ${Command.id}`)
|
||||
|
||||
// Example: Validate environment before running commands
|
||||
// Example: Log analytics
|
||||
// Example: Check for updates
|
||||
}
|
||||
|
||||
/**
|
||||
* Postrun hook - runs after each command
|
||||
*/
|
||||
export const postrun: Hook<'postrun'> = async function (opts) {
|
||||
const { Command } = opts
|
||||
|
||||
this.debug(`Command completed: ${Command.id}`)
|
||||
|
||||
// Example: Cleanup operations
|
||||
// Example: Log analytics
|
||||
// Example: Cache results
|
||||
}
|
||||
|
||||
/**
|
||||
* Command not found hook - runs when command doesn't exist
|
||||
*/
|
||||
export const command_not_found: Hook<'command_not_found'> = async function (opts) {
|
||||
const { id } = opts
|
||||
|
||||
this.log(`Command "${id}" not found`)
|
||||
|
||||
// Example: Suggest similar commands
|
||||
const suggestions = this.config.commands
|
||||
.filter(c => c.id.includes(id) || id.includes(c.id))
|
||||
.map(c => c.id)
|
||||
.slice(0, 5)
|
||||
|
||||
if (suggestions.length > 0) {
|
||||
this.log('\nDid you mean one of these?')
|
||||
suggestions.forEach(s => this.log(` - ${s}`))
|
||||
}
|
||||
|
||||
// Example: Check if command is in a plugin that's not installed
|
||||
// Example: Suggest installing missing plugin
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom plugin-specific hooks
|
||||
* Export them and register in package.json oclif.hooks
|
||||
*/
|
||||
export const customHook = async function (opts: any) {
|
||||
// Custom hook logic
|
||||
}
|
||||
41
skills/oclif-patterns/templates/plugin-manifest.json
Normal file
41
skills/oclif-patterns/templates/plugin-manifest.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "@mycli/plugin-{{PLUGIN_NAME}}",
|
||||
"version": "1.0.0",
|
||||
"description": "{{DESCRIPTION}}",
|
||||
"commands": {
|
||||
"{{PLUGIN_NAME}}:{{COMMAND_NAME}}": {
|
||||
"id": "{{PLUGIN_NAME}}:{{COMMAND_NAME}}",
|
||||
"description": "{{COMMAND_DESCRIPTION}}",
|
||||
"pluginName": "@mycli/plugin-{{PLUGIN_NAME}}",
|
||||
"pluginType": "core",
|
||||
"aliases": [],
|
||||
"examples": [
|
||||
"<%= config.bin %> {{PLUGIN_NAME}}:{{COMMAND_NAME}} --option value"
|
||||
],
|
||||
"flags": {
|
||||
"option": {
|
||||
"name": "option",
|
||||
"type": "option",
|
||||
"char": "o",
|
||||
"description": "Plugin-specific option",
|
||||
"multiple": false
|
||||
},
|
||||
"verbose": {
|
||||
"name": "verbose",
|
||||
"type": "boolean",
|
||||
"char": "v",
|
||||
"description": "Verbose output",
|
||||
"allowNo": false
|
||||
}
|
||||
},
|
||||
"args": {}
|
||||
}
|
||||
},
|
||||
"hooks": {
|
||||
"init": [
|
||||
{
|
||||
"file": "./lib/hooks/init.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
71
skills/oclif-patterns/templates/plugin-package.json
Normal file
71
skills/oclif-patterns/templates/plugin-package.json
Normal file
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"name": "@mycli/plugin-{{PLUGIN_NAME}}",
|
||||
"version": "1.0.0",
|
||||
"description": "{{DESCRIPTION}}",
|
||||
"author": "{{AUTHOR}}",
|
||||
"license": "MIT",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"files": [
|
||||
"/lib",
|
||||
"/oclif.manifest.json"
|
||||
],
|
||||
"keywords": [
|
||||
"oclif-plugin",
|
||||
"cli",
|
||||
"{{PLUGIN_NAME}}"
|
||||
],
|
||||
"oclif": {
|
||||
"commands": "./lib/commands",
|
||||
"bin": "mycli",
|
||||
"devPlugins": [
|
||||
"@oclif/plugin-help"
|
||||
],
|
||||
"topics": {
|
||||
"{{PLUGIN_NAME}}": {
|
||||
"description": "{{DESCRIPTION}}"
|
||||
}
|
||||
},
|
||||
"hooks": {
|
||||
"init": "./lib/hooks/init"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -b",
|
||||
"clean": "rm -rf lib && rm -f tsconfig.tsbuildinfo",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"postpack": "rm -f oclif.manifest.json",
|
||||
"prepack": "npm run build && oclif manifest && oclif readme",
|
||||
"test": "mocha --forbid-only \"test/**/*.test.ts\"",
|
||||
"test:coverage": "nyc npm test",
|
||||
"version": "oclif readme && git add README.md"
|
||||
},
|
||||
"dependencies": {
|
||||
"@oclif/core": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@oclif/test": "^3.0.0",
|
||||
"@types/chai": "^4",
|
||||
"@types/mocha": "^10",
|
||||
"@types/node": "^18",
|
||||
"@typescript-eslint/eslint-plugin": "^6",
|
||||
"@typescript-eslint/parser": "^6",
|
||||
"chai": "^4",
|
||||
"eslint": "^8",
|
||||
"mocha": "^10",
|
||||
"nyc": "^15",
|
||||
"oclif": "^4.0.0",
|
||||
"ts-node": "^10",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/{{GITHUB_USER}}/{{PLUGIN_NAME}}.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/{{GITHUB_USER}}/{{PLUGIN_NAME}}/issues"
|
||||
}
|
||||
}
|
||||
170
skills/oclif-patterns/templates/test-command.ts
Normal file
170
skills/oclif-patterns/templates/test-command.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import { expect, test } from '@oclif/test'
|
||||
import * as fs from 'fs-extra'
|
||||
import * as path from 'path'
|
||||
|
||||
describe('{{COMMAND_NAME}}', () => {
|
||||
// Setup and teardown
|
||||
const testDir = path.join(__dirname, 'fixtures', 'test-output')
|
||||
|
||||
beforeEach(async () => {
|
||||
await fs.ensureDir(testDir)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.remove(testDir)
|
||||
})
|
||||
|
||||
// Test basic execution
|
||||
test
|
||||
.stdout()
|
||||
.command(['{{COMMAND_NAME}}', '--help'])
|
||||
.it('shows help', ctx => {
|
||||
expect(ctx.stdout).to.contain('{{DESCRIPTION}}')
|
||||
})
|
||||
|
||||
// Test with required flags
|
||||
test
|
||||
.stdout()
|
||||
.command(['{{COMMAND_NAME}}', '--name', 'World'])
|
||||
.it('runs with --name flag', ctx => {
|
||||
expect(ctx.stdout).to.contain('Hello, World!')
|
||||
})
|
||||
|
||||
// Test flag validation
|
||||
test
|
||||
.command(['{{COMMAND_NAME}}'])
|
||||
.catch(error => {
|
||||
expect(error.message).to.contain('Missing required flag')
|
||||
})
|
||||
.it('fails without required flags')
|
||||
|
||||
// Test with multiple flags
|
||||
test
|
||||
.stdout()
|
||||
.command(['{{COMMAND_NAME}}', '--name', 'Test', '--verbose'])
|
||||
.it('runs with verbose flag', ctx => {
|
||||
expect(ctx.stdout).to.contain('Verbose mode enabled')
|
||||
expect(ctx.stdout).to.contain('Hello, Test!')
|
||||
})
|
||||
|
||||
// Test force flag behavior
|
||||
test
|
||||
.stdout()
|
||||
.command(['{{COMMAND_NAME}}', '--name', 'Test', '--force'])
|
||||
.it('runs with force flag', ctx => {
|
||||
expect(ctx.stdout).to.contain('Force mode')
|
||||
})
|
||||
|
||||
// Test error handling
|
||||
test
|
||||
.command(['{{COMMAND_NAME}}', '--name', ''])
|
||||
.catch(error => {
|
||||
expect(error.message).to.contain('Invalid')
|
||||
})
|
||||
.it('handles invalid input')
|
||||
|
||||
// Test exit codes
|
||||
test
|
||||
.command(['{{COMMAND_NAME}}', '--name', 'Test'])
|
||||
.exit(0)
|
||||
.it('exits with code 0 on success')
|
||||
|
||||
test
|
||||
.command(['{{COMMAND_NAME}}'])
|
||||
.exit(2)
|
||||
.it('exits with code 2 on missing flags')
|
||||
|
||||
// Test with environment variables
|
||||
test
|
||||
.env({ CLI_NAME: 'EnvTest' })
|
||||
.stdout()
|
||||
.command(['{{COMMAND_NAME}}', '--name', 'fromEnv'])
|
||||
.it('reads from environment variables', ctx => {
|
||||
// Test env-based behavior
|
||||
})
|
||||
|
||||
// Test with stdin input
|
||||
test
|
||||
.stdin('input data\n')
|
||||
.stdout()
|
||||
.command(['{{COMMAND_NAME}}', '--name', 'Test'])
|
||||
.it('handles stdin input', ctx => {
|
||||
// Test stdin handling
|
||||
})
|
||||
|
||||
// Test file operations
|
||||
test
|
||||
.do(() => {
|
||||
const filePath = path.join(testDir, 'test.txt')
|
||||
return fs.writeFile(filePath, 'test content')
|
||||
})
|
||||
.stdout()
|
||||
.command(['{{COMMAND_NAME}}', '--name', 'Test', '--file', path.join(testDir, 'test.txt')])
|
||||
.it('processes file input', ctx => {
|
||||
expect(ctx.stdout).to.contain('Success')
|
||||
})
|
||||
|
||||
// Test async operations
|
||||
test
|
||||
.stdout()
|
||||
.timeout(5000) // Increase timeout for async operations
|
||||
.command(['{{COMMAND_NAME}}', '--name', 'Test', '--async'])
|
||||
.it('handles async operations', ctx => {
|
||||
expect(ctx.stdout).to.contain('completed')
|
||||
})
|
||||
|
||||
// Test with mocked dependencies
|
||||
test
|
||||
.nock('https://api.example.com', api =>
|
||||
api.get('/data').reply(200, { result: 'success' })
|
||||
)
|
||||
.stdout()
|
||||
.command(['{{COMMAND_NAME}}', '--name', 'Test', '--api'])
|
||||
.it('handles API calls', ctx => {
|
||||
expect(ctx.stdout).to.contain('success')
|
||||
})
|
||||
|
||||
// Test JSON output
|
||||
test
|
||||
.stdout()
|
||||
.command(['{{COMMAND_NAME}}', '--name', 'Test', '--json'])
|
||||
.it('outputs JSON format', ctx => {
|
||||
const output = JSON.parse(ctx.stdout)
|
||||
expect(output).to.have.property('name', 'Test')
|
||||
})
|
||||
})
|
||||
|
||||
// Grouped tests by functionality
|
||||
describe('{{COMMAND_NAME}} - Flag Parsing', () => {
|
||||
test
|
||||
.stdout()
|
||||
.command(['{{COMMAND_NAME}}', '-n', 'Short'])
|
||||
.it('accepts short flags', ctx => {
|
||||
expect(ctx.stdout).to.contain('Short')
|
||||
})
|
||||
|
||||
test
|
||||
.stdout()
|
||||
.command(['{{COMMAND_NAME}}', '--name=Inline'])
|
||||
.it('accepts inline flag values', ctx => {
|
||||
expect(ctx.stdout).to.contain('Inline')
|
||||
})
|
||||
})
|
||||
|
||||
describe('{{COMMAND_NAME}} - Error Cases', () => {
|
||||
test
|
||||
.stderr()
|
||||
.command(['{{COMMAND_NAME}}', '--invalid-flag'])
|
||||
.catch(error => {
|
||||
expect(error.message).to.contain('Unexpected argument')
|
||||
})
|
||||
.it('handles invalid flags')
|
||||
|
||||
test
|
||||
.stderr()
|
||||
.command(['{{COMMAND_NAME}}', '--name', 'Test', 'extra-arg'])
|
||||
.catch(error => {
|
||||
expect(error.message).to.contain('Unexpected argument')
|
||||
})
|
||||
.it('rejects unexpected arguments')
|
||||
})
|
||||
253
skills/oclif-patterns/templates/test-helpers.ts
Normal file
253
skills/oclif-patterns/templates/test-helpers.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import * as fs from 'fs-extra'
|
||||
import * as path from 'path'
|
||||
import { Config } from '@oclif/core'
|
||||
|
||||
/**
|
||||
* Test helper utilities for oclif commands
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a temporary test directory
|
||||
*/
|
||||
export async function createTestDir(name: string): Promise<string> {
|
||||
const dir = path.join(__dirname, 'fixtures', name)
|
||||
await fs.ensureDir(dir)
|
||||
return dir
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up test directory
|
||||
*/
|
||||
export async function cleanTestDir(dir: string): Promise<void> {
|
||||
await fs.remove(dir)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a test fixture file
|
||||
*/
|
||||
export async function createFixture(
|
||||
dir: string,
|
||||
filename: string,
|
||||
content: string
|
||||
): Promise<string> {
|
||||
const filePath = path.join(dir, filename)
|
||||
await fs.writeFile(filePath, content)
|
||||
return filePath
|
||||
}
|
||||
|
||||
/**
|
||||
* Read fixture file
|
||||
*/
|
||||
export async function readFixture(dir: string, filename: string): Promise<string> {
|
||||
const filePath = path.join(dir, filename)
|
||||
return fs.readFile(filePath, 'utf-8')
|
||||
}
|
||||
|
||||
/**
|
||||
* Create test config
|
||||
*/
|
||||
export async function createTestConfig(overrides?: any): Promise<any> {
|
||||
const config = {
|
||||
version: '1.0.0',
|
||||
settings: {
|
||||
verbose: false,
|
||||
timeout: 30000,
|
||||
},
|
||||
...overrides,
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock stdin input
|
||||
*/
|
||||
export function mockStdin(input: string): void {
|
||||
const originalStdin = process.stdin
|
||||
const Readable = require('stream').Readable
|
||||
const stdin = new Readable()
|
||||
stdin.push(input)
|
||||
stdin.push(null)
|
||||
// @ts-ignore
|
||||
process.stdin = stdin
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore stdin
|
||||
*/
|
||||
export function restoreStdin(): void {
|
||||
// Restore original stdin if needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture stdout
|
||||
*/
|
||||
export class StdoutCapture {
|
||||
private originalWrite: any
|
||||
private output: string[] = []
|
||||
|
||||
start(): void {
|
||||
this.output = []
|
||||
this.originalWrite = process.stdout.write
|
||||
process.stdout.write = ((chunk: any, encoding?: any, callback?: any) => {
|
||||
this.output.push(chunk.toString())
|
||||
return true
|
||||
}) as any
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
process.stdout.write = this.originalWrite
|
||||
}
|
||||
|
||||
getOutput(): string {
|
||||
return this.output.join('')
|
||||
}
|
||||
|
||||
getLines(): string[] {
|
||||
return this.getOutput().split('\n').filter(Boolean)
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.output = []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture stderr
|
||||
*/
|
||||
export class StderrCapture {
|
||||
private originalWrite: any
|
||||
private output: string[] = []
|
||||
|
||||
start(): void {
|
||||
this.output = []
|
||||
this.originalWrite = process.stderr.write
|
||||
process.stderr.write = ((chunk: any, encoding?: any, callback?: any) => {
|
||||
this.output.push(chunk.toString())
|
||||
return true
|
||||
}) as any
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
process.stderr.write = this.originalWrite
|
||||
}
|
||||
|
||||
getOutput(): string {
|
||||
return this.output.join('')
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.output = []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a condition to be true
|
||||
*/
|
||||
export async function waitFor(
|
||||
condition: () => boolean | Promise<boolean>,
|
||||
timeout = 5000,
|
||||
interval = 100
|
||||
): Promise<void> {
|
||||
const start = Date.now()
|
||||
while (Date.now() - start < timeout) {
|
||||
if (await condition()) {
|
||||
return
|
||||
}
|
||||
await sleep(interval)
|
||||
}
|
||||
throw new Error('Timeout waiting for condition')
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleep for specified milliseconds
|
||||
*/
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
/**
|
||||
* Create mock HTTP response
|
||||
*/
|
||||
export function createMockResponse(status: number, data: any): any {
|
||||
return {
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
statusText: status === 200 ? 'OK' : 'Error',
|
||||
json: async () => data,
|
||||
text: async () => JSON.stringify(data),
|
||||
headers: new Map(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock fetch globally
|
||||
*/
|
||||
export function mockFetch(responses: Map<string, any>): void {
|
||||
const originalFetch = global.fetch
|
||||
global.fetch = async (url: string | URL, options?: any) => {
|
||||
const urlStr = url.toString()
|
||||
const response = responses.get(urlStr)
|
||||
if (!response) {
|
||||
throw new Error(`No mock response for URL: ${urlStr}`)
|
||||
}
|
||||
return response
|
||||
} as any
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore fetch
|
||||
*/
|
||||
export function restoreFetch(): void {
|
||||
// Restore if needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Create test environment variables
|
||||
*/
|
||||
export function withEnv(vars: Record<string, string>, fn: () => void | Promise<void>): any {
|
||||
return async () => {
|
||||
const original = { ...process.env }
|
||||
Object.assign(process.env, vars)
|
||||
try {
|
||||
await fn()
|
||||
} finally {
|
||||
process.env = original
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert file exists
|
||||
*/
|
||||
export async function assertFileExists(filePath: string): Promise<void> {
|
||||
const exists = await fs.pathExists(filePath)
|
||||
if (!exists) {
|
||||
throw new Error(`Expected file to exist: ${filePath}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert file contains
|
||||
*/
|
||||
export async function assertFileContains(filePath: string, content: string): Promise<void> {
|
||||
await assertFileExists(filePath)
|
||||
const fileContent = await fs.readFile(filePath, 'utf-8')
|
||||
if (!fileContent.includes(content)) {
|
||||
throw new Error(`Expected file ${filePath} to contain: ${content}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create test oclif config
|
||||
*/
|
||||
export async function createOclifConfig(root: string): Promise<Config> {
|
||||
return Config.load(root)
|
||||
}
|
||||
|
||||
/**
|
||||
* Run command programmatically
|
||||
*/
|
||||
export async function runCommand(args: string[], config?: Config): Promise<any> {
|
||||
const { run } = await import('@oclif/core')
|
||||
return run(args, config ? config.root : undefined)
|
||||
}
|
||||
202
skills/oclif-patterns/templates/test-integration.ts
Normal file
202
skills/oclif-patterns/templates/test-integration.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import { expect } from 'chai'
|
||||
import { runCommand } from '@oclif/test'
|
||||
import * as fs from 'fs-extra'
|
||||
import * as path from 'path'
|
||||
|
||||
/**
|
||||
* Integration tests for complete CLI workflows
|
||||
*/
|
||||
describe('Integration Tests', () => {
|
||||
const testDir = path.join(__dirname, 'fixtures', 'integration')
|
||||
|
||||
before(async () => {
|
||||
await fs.ensureDir(testDir)
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await fs.remove(testDir)
|
||||
})
|
||||
|
||||
describe('Complete workflow', () => {
|
||||
it('runs full command chain', async () => {
|
||||
// Step 1: Initialize
|
||||
const initResult = await runCommand(['init', '--dir', testDir])
|
||||
expect(initResult).to.have.property('code', 0)
|
||||
|
||||
// Verify initialization
|
||||
const configPath = path.join(testDir, '.clirc')
|
||||
expect(await fs.pathExists(configPath)).to.be.true
|
||||
|
||||
// Step 2: Configure
|
||||
const configResult = await runCommand([
|
||||
'config',
|
||||
'--set',
|
||||
'key=value',
|
||||
'--dir',
|
||||
testDir,
|
||||
])
|
||||
expect(configResult).to.have.property('code', 0)
|
||||
|
||||
// Verify configuration
|
||||
const config = await fs.readJson(configPath)
|
||||
expect(config).to.have.property('key', 'value')
|
||||
|
||||
// Step 3: Execute main operation
|
||||
const execResult = await runCommand(['execute', '--dir', testDir])
|
||||
expect(execResult).to.have.property('code', 0)
|
||||
|
||||
// Verify output
|
||||
const outputPath = path.join(testDir, 'output.json')
|
||||
expect(await fs.pathExists(outputPath)).to.be.true
|
||||
})
|
||||
|
||||
it('handles errors gracefully', async () => {
|
||||
// Attempt operation without initialization
|
||||
try {
|
||||
await runCommand(['execute', '--dir', '/nonexistent'])
|
||||
expect.fail('Should have thrown error')
|
||||
} catch (error: any) {
|
||||
expect(error.message).to.include('not initialized')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Plugin integration', () => {
|
||||
it('loads and executes plugin commands', async () => {
|
||||
// Install plugin
|
||||
const installResult = await runCommand(['plugins:install', '@mycli/plugin-test'])
|
||||
expect(installResult).to.have.property('code', 0)
|
||||
|
||||
// Execute plugin command
|
||||
const pluginResult = await runCommand(['test:command', '--option', 'value'])
|
||||
expect(pluginResult).to.have.property('code', 0)
|
||||
|
||||
// Uninstall plugin
|
||||
const uninstallResult = await runCommand(['plugins:uninstall', '@mycli/plugin-test'])
|
||||
expect(uninstallResult).to.have.property('code', 0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Multi-command workflows', () => {
|
||||
it('chains commands with data flow', async () => {
|
||||
// Generate data
|
||||
const generateResult = await runCommand([
|
||||
'generate',
|
||||
'--output',
|
||||
path.join(testDir, 'data.json'),
|
||||
])
|
||||
expect(generateResult).to.have.property('code', 0)
|
||||
|
||||
// Process data
|
||||
const processResult = await runCommand([
|
||||
'process',
|
||||
'--input',
|
||||
path.join(testDir, 'data.json'),
|
||||
'--output',
|
||||
path.join(testDir, 'processed.json'),
|
||||
])
|
||||
expect(processResult).to.have.property('code', 0)
|
||||
|
||||
// Validate output
|
||||
const validateResult = await runCommand([
|
||||
'validate',
|
||||
path.join(testDir, 'processed.json'),
|
||||
])
|
||||
expect(validateResult).to.have.property('code', 0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Environment-specific behavior', () => {
|
||||
it('respects environment variables', async () => {
|
||||
// Set environment
|
||||
process.env.CLI_ENV = 'production'
|
||||
process.env.CLI_DEBUG = 'false'
|
||||
|
||||
const result = await runCommand(['status'])
|
||||
expect(result).to.have.property('code', 0)
|
||||
|
||||
// Cleanup
|
||||
delete process.env.CLI_ENV
|
||||
delete process.env.CLI_DEBUG
|
||||
})
|
||||
|
||||
it('handles CI environment', async () => {
|
||||
// Simulate CI environment
|
||||
process.env.CI = 'true'
|
||||
|
||||
// Commands should not prompt in CI
|
||||
const result = await runCommand(['deploy', '--auto-confirm'])
|
||||
expect(result).to.have.property('code', 0)
|
||||
|
||||
// Cleanup
|
||||
delete process.env.CI
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error recovery', () => {
|
||||
it('recovers from partial failures', async () => {
|
||||
// Start operation
|
||||
const startResult = await runCommand([
|
||||
'start-operation',
|
||||
'--output',
|
||||
path.join(testDir, 'operation.lock'),
|
||||
])
|
||||
expect(startResult).to.have.property('code', 0)
|
||||
|
||||
// Simulate failure (lock file exists)
|
||||
expect(await fs.pathExists(path.join(testDir, 'operation.lock'))).to.be.true
|
||||
|
||||
// Retry with cleanup
|
||||
const retryResult = await runCommand([
|
||||
'start-operation',
|
||||
'--output',
|
||||
path.join(testDir, 'operation.lock'),
|
||||
'--force',
|
||||
])
|
||||
expect(retryResult).to.have.property('code', 0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Performance', () => {
|
||||
it('handles large datasets efficiently', async () => {
|
||||
const largeFile = path.join(testDir, 'large.json')
|
||||
|
||||
// Generate large dataset
|
||||
const largeData = Array.from({ length: 10000 }, (_, i) => ({
|
||||
id: i,
|
||||
name: `item-${i}`,
|
||||
data: 'x'.repeat(100),
|
||||
}))
|
||||
await fs.writeJson(largeFile, largeData)
|
||||
|
||||
// Process large file
|
||||
const startTime = Date.now()
|
||||
const result = await runCommand(['process-large', '--input', largeFile])
|
||||
const duration = Date.now() - startTime
|
||||
|
||||
expect(result).to.have.property('code', 0)
|
||||
expect(duration).to.be.lessThan(30000) // Should complete within 30 seconds
|
||||
})
|
||||
})
|
||||
|
||||
describe('Concurrent operations', () => {
|
||||
it('handles concurrent commands', async () => {
|
||||
// Run multiple commands in parallel
|
||||
const results = await Promise.all([
|
||||
runCommand(['task-1', '--output', path.join(testDir, 'out1.json')]),
|
||||
runCommand(['task-2', '--output', path.join(testDir, 'out2.json')]),
|
||||
runCommand(['task-3', '--output', path.join(testDir, 'out3.json')]),
|
||||
])
|
||||
|
||||
// All should succeed
|
||||
results.forEach(result => {
|
||||
expect(result).to.have.property('code', 0)
|
||||
})
|
||||
|
||||
// Verify all outputs
|
||||
expect(await fs.pathExists(path.join(testDir, 'out1.json'))).to.be.true
|
||||
expect(await fs.pathExists(path.join(testDir, 'out2.json'))).to.be.true
|
||||
expect(await fs.pathExists(path.join(testDir, 'out3.json'))).to.be.true
|
||||
})
|
||||
})
|
||||
})
|
||||
109
skills/oclif-patterns/templates/test-setup.ts
Normal file
109
skills/oclif-patterns/templates/test-setup.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { expect } from 'chai'
|
||||
import * as path from 'path'
|
||||
|
||||
/**
|
||||
* Global test setup for oclif commands
|
||||
*
|
||||
* This file is loaded before all tests
|
||||
*/
|
||||
|
||||
// Extend chai with custom assertions if needed
|
||||
expect.extend = function (assertions: any) {
|
||||
Object.assign(expect, assertions)
|
||||
}
|
||||
|
||||
// Set test environment
|
||||
process.env.NODE_ENV = 'test'
|
||||
process.env.CLI_TEST = 'true'
|
||||
|
||||
// Disable colors in test output
|
||||
process.env.FORCE_COLOR = '0'
|
||||
|
||||
// Set test timeout
|
||||
const DEFAULT_TIMEOUT = 10000
|
||||
if (typeof (global as any).setTimeout !== 'undefined') {
|
||||
;(global as any).setTimeout(DEFAULT_TIMEOUT)
|
||||
}
|
||||
|
||||
// Setup global test fixtures directory
|
||||
export const FIXTURES_DIR = path.join(__dirname, 'fixtures')
|
||||
|
||||
// Mock console methods if needed
|
||||
export function mockConsole() {
|
||||
const originalLog = console.log
|
||||
const originalError = console.error
|
||||
const originalWarn = console.warn
|
||||
|
||||
const logs: string[] = []
|
||||
const errors: string[] = []
|
||||
const warns: string[] = []
|
||||
|
||||
console.log = (...args: any[]) => {
|
||||
logs.push(args.join(' '))
|
||||
}
|
||||
|
||||
console.error = (...args: any[]) => {
|
||||
errors.push(args.join(' '))
|
||||
}
|
||||
|
||||
console.warn = (...args: any[]) => {
|
||||
warns.push(args.join(' '))
|
||||
}
|
||||
|
||||
return {
|
||||
logs,
|
||||
errors,
|
||||
warns,
|
||||
restore: () => {
|
||||
console.log = originalLog
|
||||
console.error = originalError
|
||||
console.warn = originalWarn
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Global before hook
|
||||
before(async () => {
|
||||
// Setup test database, services, etc.
|
||||
})
|
||||
|
||||
// Global after hook
|
||||
after(async () => {
|
||||
// Cleanup test resources
|
||||
})
|
||||
|
||||
// Global beforeEach hook
|
||||
beforeEach(() => {
|
||||
// Reset state before each test
|
||||
})
|
||||
|
||||
// Global afterEach hook
|
||||
afterEach(() => {
|
||||
// Cleanup after each test
|
||||
})
|
||||
|
||||
/**
|
||||
* Custom matchers for oclif tests
|
||||
*/
|
||||
export const customMatchers = {
|
||||
/**
|
||||
* Check if output contains text
|
||||
*/
|
||||
toContainOutput(received: string, expected: string): boolean {
|
||||
return received.includes(expected)
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if command succeeded
|
||||
*/
|
||||
toSucceed(received: { code: number }): boolean {
|
||||
return received.code === 0
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if command failed with specific code
|
||||
*/
|
||||
toFailWith(received: { code: number }, expectedCode: number): boolean {
|
||||
return received.code === expectedCode
|
||||
},
|
||||
}
|
||||
27
skills/oclif-patterns/templates/tsconfig.json
Normal file
27
skills/oclif-patterns/templates/tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2022"],
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./lib",
|
||||
"rootDir": "./src",
|
||||
"composite": true,
|
||||
"incremental": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "node",
|
||||
"types": ["node"],
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "lib", "test"]
|
||||
}
|
||||
Reference in New Issue
Block a user