Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 09:04:14 +08:00
commit 70c36b5eff
248 changed files with 47482 additions and 0 deletions

View 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"
}
}

View 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
}
}

View 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')
}
}

View 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))
}
}

View 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')
}
}
}

View 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
}
}

View 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"
}

View 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(', ')}`)
}
}
}

View 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
}

View 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"
}
]
}
}

View 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"
}
}

View 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')
})

View 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)
}

View 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
})
})
})

View 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
},
}

View 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"]
}