plugin-scaffold.ts
1 /** 2 * Plugin scaffold: generates a ready-to-develop plugin directory. 3 * 4 * Usage: opencli plugin create <name> [--dir <path>] 5 * 6 * Creates: 7 * <name>/ 8 * opencli-plugin.json — manifest with name, version, description 9 * package.json — ESM package with opencli peer dependency 10 * hello.ts — sample pipeline command 11 * greet.ts — sample TS command using func() 12 * README.md — basic documentation 13 */ 14 15 import * as fs from 'node:fs'; 16 import * as path from 'node:path'; 17 import { PKG_VERSION } from './version.js'; 18 19 export interface ScaffoldOptions { 20 /** Directory to create the plugin in. Defaults to `./<name>` */ 21 dir?: string; 22 /** Plugin description */ 23 description?: string; 24 } 25 26 export interface ScaffoldResult { 27 name: string; 28 dir: string; 29 files: string[]; 30 } 31 32 /** 33 * Create a new plugin scaffold directory. 34 */ 35 export function createPluginScaffold(name: string, opts: ScaffoldOptions = {}): ScaffoldResult { 36 // Validate name 37 if (!/^[a-z][a-z0-9-]*$/.test(name)) { 38 throw new Error( 39 `Invalid plugin name "${name}". ` + 40 `Plugin names must start with a lowercase letter and contain only lowercase letters, digits, and hyphens.` 41 ); 42 } 43 44 const targetDir = opts.dir 45 ? path.resolve(opts.dir) 46 : path.resolve(name); 47 48 if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) { 49 throw new Error(`Directory "${targetDir}" already exists and is not empty.`); 50 } 51 52 fs.mkdirSync(targetDir, { recursive: true }); 53 54 const files: string[] = []; 55 56 // opencli-plugin.json 57 const manifest = { 58 name, 59 version: '0.1.0', 60 description: opts.description ?? `An opencli plugin: ${name}`, 61 opencli: `>=${PKG_VERSION}`, 62 }; 63 writeFile(targetDir, 'opencli-plugin.json', JSON.stringify(manifest, null, 2) + '\n'); 64 files.push('opencli-plugin.json'); 65 66 // package.json 67 const pkg = { 68 name: `opencli-plugin-${name}`, 69 version: '0.1.0', 70 type: 'module', 71 description: opts.description ?? `An opencli plugin: ${name}`, 72 peerDependencies: { 73 '@jackwener/opencli': `>=${PKG_VERSION}`, 74 }, 75 }; 76 writeFile(targetDir, 'package.json', JSON.stringify(pkg, null, 2) + '\n'); 77 files.push('package.json'); 78 79 // hello.ts — sample pipeline command 80 const helloContent = `/** 81 * Sample pipeline command for ${name}. 82 * Demonstrates the declarative pipeline API. 83 */ 84 85 import { cli, Strategy } from '@jackwener/opencli/registry'; 86 87 cli({ 88 site: '${name}', 89 name: 'hello', 90 description: 'A sample pipeline command', 91 strategy: Strategy.PUBLIC, 92 browser: false, 93 columns: ['greeting'], 94 pipeline: [ 95 { fetch: { url: 'https://httpbin.org/get?greeting=hello' } }, 96 { select: 'args' }, 97 ], 98 }); 99 `; 100 writeFile(targetDir, 'hello.ts', helloContent); 101 files.push('hello.ts'); 102 103 // greet.ts — sample TS command using registry API 104 const tsContent = `/** 105 * Sample TypeScript command for ${name}. 106 * Demonstrates the programmatic cli() registration API. 107 */ 108 109 import { cli, Strategy } from '@jackwener/opencli/registry'; 110 111 cli({ 112 site: '${name}', 113 name: 'greet', 114 description: 'Greet someone by name', 115 strategy: Strategy.PUBLIC, 116 browser: false, 117 args: [ 118 { name: 'name', positional: true, required: true, help: 'Name to greet' }, 119 ], 120 columns: ['greeting'], 121 func: async (_page, kwargs) => [{ greeting: \`Hello, \${String(kwargs.name ?? 'World')}!\` }], 122 }); 123 `; 124 writeFile(targetDir, 'greet.ts', tsContent); 125 files.push('greet.ts'); 126 127 // README.md 128 const readme = `# opencli-plugin-${name} 129 130 ${opts.description ?? `An opencli plugin: ${name}`} 131 132 ## Install 133 134 \`\`\`bash 135 # From local development directory 136 opencli plugin install file://${targetDir} 137 138 # From GitHub (after publishing) 139 opencli plugin install github:<user>/opencli-plugin-${name} 140 \`\`\` 141 142 ## Commands 143 144 | Command | Type | Description | 145 |---------|------|-------------| 146 | \`${name}/hello\` | Pipeline | Sample pipeline command | 147 | \`${name}/greet\` | TypeScript | Sample TS command with func() | 148 149 ## Development 150 151 \`\`\`bash 152 # Install locally for development (symlinked, changes reflect immediately) 153 opencli plugin install file://${targetDir} 154 155 # Verify commands are registered 156 opencli list | grep ${name} 157 158 # Run a command 159 opencli ${name} hello 160 opencli ${name} greet --name World 161 \`\`\` 162 `; 163 writeFile(targetDir, 'README.md', readme); 164 files.push('README.md'); 165 166 return { name, dir: targetDir, files }; 167 } 168 169 function writeFile(dir: string, name: string, content: string): void { 170 fs.writeFileSync(path.join(dir, name), content); 171 }