Add utility files: spec-parser, file-writer, helpers, wizard
This commit is contained in:
401
src/utils/wizard.ts
Normal file
401
src/utils/wizard.ts
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
import inquirer from 'inquirer';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import type { CLISpec, Option, Command, Argument, Example } from '../types/spec.js';
|
||||||
|
import { validateSpec } from '../validators/schema.js';
|
||||||
|
|
||||||
|
async function promptBasicInfo(): Promise<Partial<CLISpec>> {
|
||||||
|
const answers = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'name',
|
||||||
|
message: 'CLI name (kebab-case):',
|
||||||
|
validate: (input: string) => input.length > 0 || 'Name is required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'version',
|
||||||
|
message: 'Version (semantic, e.g., 1.0.0):',
|
||||||
|
default: '1.0.0',
|
||||||
|
validate: (input: string) => /^\d+\.\d+\.\d+/.test(input) || 'Version must be semantic (e.g., 1.0.0)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'description',
|
||||||
|
message: 'Description:',
|
||||||
|
validate: (input: string) => input.length > 0 || 'Description is required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'author',
|
||||||
|
message: 'Author (optional):',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'license',
|
||||||
|
message: 'License (optional):',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'bin',
|
||||||
|
message: 'Binary name (optional, defaults to CLI name):',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
return answers;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptGlobalOptions(): Promise<Option[]> {
|
||||||
|
const options: Option[] = [];
|
||||||
|
let adding = true;
|
||||||
|
|
||||||
|
while (adding) {
|
||||||
|
const { shouldAdd } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'shouldAdd',
|
||||||
|
message: 'Add a global option?',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!shouldAdd) break;
|
||||||
|
|
||||||
|
const optionAnswers = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'name',
|
||||||
|
message: 'Option name (--name):',
|
||||||
|
validate: (input: string) => input.length > 0 || 'Option name is required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'short',
|
||||||
|
message: 'Short flag (-s, optional):',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'description',
|
||||||
|
message: 'Description:',
|
||||||
|
validate: (input: string) => input.length > 0 || 'Description is required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'list',
|
||||||
|
name: 'type',
|
||||||
|
message: 'Type:',
|
||||||
|
choices: ['string', 'number', 'boolean', 'array'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'required',
|
||||||
|
message: 'Required?',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
options.push({
|
||||||
|
name: optionAnswers.name,
|
||||||
|
short: optionAnswers.short || undefined,
|
||||||
|
description: optionAnswers.description,
|
||||||
|
type: optionAnswers.type,
|
||||||
|
required: optionAnswers.required,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptArguments(): Promise<Argument[]> {
|
||||||
|
const args: Argument[] = [];
|
||||||
|
let adding = true;
|
||||||
|
|
||||||
|
while (adding) {
|
||||||
|
const { shouldAdd } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'shouldAdd',
|
||||||
|
message: 'Add an argument?',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!shouldAdd) break;
|
||||||
|
|
||||||
|
const argAnswers = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'name',
|
||||||
|
message: 'Argument name:',
|
||||||
|
validate: (input: string) => input.length > 0 || 'Argument name is required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'description',
|
||||||
|
message: 'Description:',
|
||||||
|
validate: (input: string) => input.length > 0 || 'Description is required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'required',
|
||||||
|
message: 'Required?',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'variadic',
|
||||||
|
message: 'Variadic (multiple values)?',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
args.push({
|
||||||
|
name: argAnswers.name,
|
||||||
|
description: argAnswers.description,
|
||||||
|
required: argAnswers.required,
|
||||||
|
variadic: argAnswers.variadic,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptOptions(): Promise<Option[]> {
|
||||||
|
const options: Option[] = [];
|
||||||
|
let adding = true;
|
||||||
|
|
||||||
|
while (adding) {
|
||||||
|
const { shouldAdd } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'shouldAdd',
|
||||||
|
message: 'Add an option to this command?',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!shouldAdd) break;
|
||||||
|
|
||||||
|
const optionAnswers = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'name',
|
||||||
|
message: 'Option name (--name):',
|
||||||
|
validate: (input: string) => input.length > 0 || 'Option name is required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'short',
|
||||||
|
message: 'Short flag (-s, optional):',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'description',
|
||||||
|
message: 'Description:',
|
||||||
|
validate: (input: string) => input.length > 0 || 'Description is required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'list',
|
||||||
|
name: 'type',
|
||||||
|
message: 'Type:',
|
||||||
|
choices: ['string', 'number', 'boolean', 'array'],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
options.push({
|
||||||
|
name: optionAnswers.name,
|
||||||
|
short: optionAnswers.short || undefined,
|
||||||
|
description: optionAnswers.description,
|
||||||
|
type: optionAnswers.type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptSubcommands(): Promise<Command[]> {
|
||||||
|
const subcommands: Command[] = [];
|
||||||
|
let adding = true;
|
||||||
|
|
||||||
|
while (adding) {
|
||||||
|
const { shouldAdd } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'shouldAdd',
|
||||||
|
message: 'Add a subcommand?',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!shouldAdd) break;
|
||||||
|
|
||||||
|
const subcmdAnswers = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'name',
|
||||||
|
message: 'Subcommand name:',
|
||||||
|
validate: (input: string) => input.length > 0 || 'Subcommand name is required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'description',
|
||||||
|
message: 'Description:',
|
||||||
|
validate: (input: string) => input.length > 0 || 'Description is required',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const args = await promptArguments();
|
||||||
|
const opts = await promptOptions();
|
||||||
|
|
||||||
|
subcommands.push({
|
||||||
|
name: subcmdAnswers.name,
|
||||||
|
description: subcmdAnswers.description,
|
||||||
|
arguments: args,
|
||||||
|
options: opts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return subcommands;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptCommands(): Promise<Command[]> {
|
||||||
|
const commands: Command[] = [];
|
||||||
|
let adding = true;
|
||||||
|
|
||||||
|
while (adding) {
|
||||||
|
const { shouldAdd } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'shouldAdd',
|
||||||
|
message: 'Add a command?',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!shouldAdd) break;
|
||||||
|
|
||||||
|
const cmdAnswers = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'name',
|
||||||
|
message: 'Command name:',
|
||||||
|
validate: (input: string) => input.length > 0 || 'Command name is required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'description',
|
||||||
|
message: 'Description:',
|
||||||
|
validate: (input: string) => input.length > 0 || 'Description is required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'aliases',
|
||||||
|
message: 'Aliases (comma-separated, optional):',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const args = await promptArguments();
|
||||||
|
const opts = await promptOptions();
|
||||||
|
const subcommands = await promptSubcommands();
|
||||||
|
|
||||||
|
commands.push({
|
||||||
|
name: cmdAnswers.name,
|
||||||
|
description: cmdAnswers.description,
|
||||||
|
aliases: cmdAnswers.aliases ? cmdAnswers.aliases.split(',').map((a: string) => a.trim()) : undefined,
|
||||||
|
arguments: args,
|
||||||
|
options: opts,
|
||||||
|
subcommands: subcommands.length > 0 ? subcommands : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promptExamples(): Promise<Example[]> {
|
||||||
|
const examples: Example[] = [];
|
||||||
|
let adding = true;
|
||||||
|
|
||||||
|
while (adding) {
|
||||||
|
const { shouldAdd } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'shouldAdd',
|
||||||
|
message: 'Add an example?',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!shouldAdd) break;
|
||||||
|
|
||||||
|
const exampleAnswers = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'description',
|
||||||
|
message: 'Example description:',
|
||||||
|
validate: (input: string) => input.length > 0 || 'Description is required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'command',
|
||||||
|
message: 'Command:',
|
||||||
|
validate: (input: string) => input.length > 0 || 'Command is required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'output',
|
||||||
|
message: 'Expected output (optional):',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
examples.push({
|
||||||
|
description: exampleAnswers.description,
|
||||||
|
command: exampleAnswers.command,
|
||||||
|
output: exampleAnswers.output || undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return examples;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createSpecWizard(): Promise<CLISpec> {
|
||||||
|
console.log('\n=== CLI Spec Generator Wizard ===\n');
|
||||||
|
|
||||||
|
const basicInfo = await promptBasicInfo();
|
||||||
|
const globalOptions = await promptGlobalOptions();
|
||||||
|
const commands = await promptCommands();
|
||||||
|
const examples = await promptExamples();
|
||||||
|
|
||||||
|
const spec: CLISpec = {
|
||||||
|
name: basicInfo.name!,
|
||||||
|
version: basicInfo.version!,
|
||||||
|
description: basicInfo.description!,
|
||||||
|
author: basicInfo.author,
|
||||||
|
license: basicInfo.license,
|
||||||
|
bin: basicInfo.bin || undefined,
|
||||||
|
globalOptions: globalOptions.length > 0 ? globalOptions : undefined,
|
||||||
|
commands,
|
||||||
|
examples: examples.length > 0 ? examples : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const validation = validateSpec(spec);
|
||||||
|
if (!validation.success) {
|
||||||
|
console.warn('\n⚠️ Spec validation warnings:');
|
||||||
|
validation.errors.forEach(err => console.warn(` - ${err}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n✅ Spec created successfully!\n');
|
||||||
|
|
||||||
|
return spec;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveSpec(spec: CLISpec, outputPath?: string): Promise<string> {
|
||||||
|
const fileName = outputPath || `${spec.name}.yaml`;
|
||||||
|
const yaml = await import('js-yaml');
|
||||||
|
const content = yaml.default.dump(spec, { indent: 2, lineWidth: -1 });
|
||||||
|
|
||||||
|
fs.writeFileSync(fileName, content, 'utf-8');
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function editSpecWizard(existingSpec: CLISpec): Promise<CLISpec> {
|
||||||
|
console.log('\n=== Spec Editor ===\n');
|
||||||
|
console.log('Note: Full editing not yet implemented. Please manually edit the file.\n');
|
||||||
|
return existingSpec;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user