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