Add utility files: spec-parser, file-writer, helpers, wizard

This commit is contained in:
2026-01-30 07:09:15 +00:00
parent 566ee11166
commit 399f610cfd

401
src/utils/wizard.ts Normal file
View 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;
}