/ src / plugin-scaffold.ts
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  }