diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..103a175 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,245 @@ +#!/usr/bin/env node + +import { Command } from 'commander'; +import * as fs from 'fs'; +import * as path from 'path'; +import figlet from 'figlet'; +import { parseSpec } from './utils/spec-parser.js'; +import { writeFile, ensureDirectory } from './utils/file-writer.js'; +import { + generateCode, + generateCompletion, + generateManPage, + generateReadmeSection, + getOutputFileName, + getCompletionFileName, + getManPageFileName, + getReadmeFileName, +} from './generators/index.js'; +import { createSpecWizard, saveSpec } from './utils/wizard.js'; +import { LanguageTarget, ShellType } from './types/spec.js'; + +const program = new Command(); + +program + .name('cli-spec-gen') + .description('Generate CLI parsers, completions, and docs from YAML/JSON specs') + .version('1.0.0'); + +program + .command('generate ') + .alias('gen') + .description('Generate code from a spec file') + .option('-l, --language ', 'Target language', 'python') + .option('-o, --output ', 'Output directory', '.') + .option('--all', 'Generate for all languages') + .action(async (specFile, options) => { + const result = parseSpec({ filePath: specFile }); + if (!result.success) { + console.error('āŒ Error parsing spec:'); + result.errors?.forEach(err => console.error(` - ${err}`)); + process.exit(1); + } + + const spec = result.spec!; + const languages: LanguageTarget[] = options.all + ? ['python', 'go', 'rust', 'node-commander', 'node-yargs'] + : [options.language as LanguageTarget]; + + console.log(`\nšŸ“¦ Generating code for ${spec.name} v${spec.version}\n`); + + for (const lang of languages) { + const genResult = generateCode(spec, lang); + if (!genResult.success) { + console.error(`āŒ Error generating ${lang}: ${genResult.error}`); + continue; + } + + const fileName = getOutputFileName(spec, lang); + const writeResult = writeFile({ + outputDir: options.output, + fileName, + content: genResult.code!, + overwrite: true, + }); + + if (writeResult.success) { + console.log(`āœ… Generated ${fileName}`); + } else { + console.error(`āŒ Failed to write ${fileName}: ${writeResult.error}`); + } + } + }); + +program + .command('completion ') + .alias('comp') + .description('Generate shell completion scripts') + .option('-s, --shell ', 'Target shell', 'bash') + .option('-o, --output ', 'Output directory', '.') + .option('--all', 'Generate for all shells') + .action(async (specFile, options) => { + const result = parseSpec({ filePath: specFile }); + if (!result.success) { + console.error('āŒ Error parsing spec:'); + result.errors?.forEach(err => console.error(` - ${err}`)); + process.exit(1); + } + + const spec = result.spec!; + const shells: ShellType[] = options.all + ? ['bash', 'zsh', 'fish'] + : [options.shell as ShellType]; + + console.log(`\n🐚 Generating completions for ${spec.name}\n`); + + for (const shell of shells) { + const genResult = generateCompletion(spec, shell); + if (!genResult.success) { + console.error(`āŒ Error generating ${shell} completion: ${genResult.error}`); + continue; + } + + const fileName = getCompletionFileName(spec, shell); + const writeResult = writeFile({ + outputDir: options.output, + fileName, + content: genResult.script!, + overwrite: true, + }); + + if (writeResult.success) { + console.log(`āœ… Generated ${fileName}`); + } else { + console.error(`āŒ Failed to write ${fileName}: ${writeResult.error}`); + } + } + }); + +program + .command('docs ') + .description('Generate documentation (man page, README)') + .option('-o, --output ', 'Output directory', '.') + .action(async (specFile, options) => { + const result = parseSpec({ filePath: specFile }); + if (!result.success) { + console.error('āŒ Error parsing spec:'); + result.errors?.forEach(err => console.error(` - ${err}`)); + process.exit(1); + } + + const spec = result.spec!; + ensureDirectory(options.output); + + console.log(`\nšŸ“š Generating docs for ${spec.name}\n`); + + const manResult = generateManPage(spec); + if (manResult.success) { + const manFileName = getManPageFileName(spec); + const manPath = path.join(options.output, manFileName); + fs.writeFileSync(manPath, manResult.content!, 'utf-8'); + console.log(`āœ… Generated ${manFileName}`); + } + + const readmeResult = generateReadmeSection(spec); + if (readmeResult.success) { + const readmeFileName = getReadmeFileName(); + const readmePath = path.join(options.output, readmeFileName); + fs.writeFileSync(readmePath, readmeResult.content!, 'utf-8'); + console.log(`āœ… Generated ${readmeFileName}`); + } + }); + +program + .command('init') + .alias('new') + .description('Interactive wizard to create a new spec') + .option('-o, --output ', 'Output file', 'cli-spec.yaml') + .action(async (options) => { + const spec = await createSpecWizard(); + const fileName = await saveSpec(spec, options.output); + console.log(`\nšŸ“„ Spec saved to: ${fileName}`); + }); + +program + .command('validate ') + .alias('check') + .description('Validate a spec file') + .action((specFile) => { + const result = parseSpec({ filePath: specFile }); + if (result.success) { + console.log(`āœ… Valid spec: ${result.spec!.name} v${result.spec!.version}`); + console.log(` Commands: ${result.spec!.commands.length}`); + console.log(` Global options: ${result.spec!.globalOptions?.length || 0}`); + } else { + console.error('āŒ Invalid spec:'); + result.errors?.forEach(err => console.error(` - ${err}`)); + process.exit(1); + } + }); + +program + .command('all ') + .description('Generate everything (code, completions, docs)') + .option('-o, --output ', 'Output directory', 'generated') + .action(async (specFile, options) => { + const result = parseSpec({ filePath: specFile }); + if (!result.success) { + console.error('āŒ Error parsing spec:'); + result.errors?.forEach(err => console.error(` - ${err}`)); + process.exit(1); + } + + const spec = result.spec!; + ensureDirectory(options.output); + + console.log(`\nšŸš€ Generating everything for ${spec.name}\n`); + + const languages: LanguageTarget[] = ['python', 'go', 'rust', 'node-commander', 'node-yargs']; + for (const lang of languages) { + const genResult = generateCode(spec, lang); + if (genResult.success) { + const fileName = getOutputFileName(spec, lang); + const filePath = path.join(options.output, fileName); + fs.writeFileSync(filePath, genResult.code!, 'utf-8'); + console.log(`āœ… Generated ${fileName}`); + } + } + + const shells: ShellType[] = ['bash', 'zsh', 'fish']; + for (const shell of shells) { + const compResult = generateCompletion(spec, shell); + if (compResult.success) { + const fileName = getCompletionFileName(spec, shell); + const filePath = path.join(options.output, fileName); + fs.writeFileSync(filePath, compResult.script!, 'utf-8'); + console.log(`āœ… Generated ${fileName}`); + } + } + + const manResult = generateManPage(spec); + if (manResult.success) { + const manFileName = getManPageFileName(spec); + const manPath = path.join(options.output, manFileName); + fs.writeFileSync(manPath, manResult.content!, 'utf-8'); + console.log(`āœ… Generated ${manFileName}`); + } + + const readmeResult = generateReadmeSection(spec); + if (readmeResult.success) { + const readmeFileName = getReadmeFileName(); + const readmePath = path.join(options.output, readmeFileName); + fs.writeFileSync(readmePath, readmeResult.content!, 'utf-8'); + console.log(`āœ… Generated ${readmeFileName}`); + } + + console.log('\n✨ All files generated successfully!\n'); + }); + +function main() { + console.log(figlet.textSync('CLI Spec', { font: 'Standard' })); + console.log('Generator v1.0.0\n'); + program.parse(process.argv); +} + +main();