@re-shell/cli
Version:
Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja
909 lines (908 loc) ⢠37.1 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.TemplateWizard = void 0;
exports.createTemplateInteractive = createTemplateInteractive;
exports.createTemplateWizard = createTemplateWizard;
const events_1 = require("events");
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
const yaml = __importStar(require("js-yaml"));
const template_engine_1 = require("./template-engine");
const template_validator_1 = require("./template-validator");
const interactive_prompts_1 = require("./interactive-prompts");
class TemplateWizard extends events_1.EventEmitter {
constructor(options = {}) {
super();
this.options = options;
this.steps = [];
this.currentTemplate = {};
this.prompter = new interactive_prompts_1.InteractivePrompter();
this.validator = new template_validator_1.TemplateValidator();
this.initializeSteps();
}
initializeSteps() {
// Basic Information
this.steps.push({
id: 'name',
title: 'Template Name',
description: 'Enter a name for your template',
type: 'input',
required: true,
validate: (value) => {
if (!value || value.trim().length < 3) {
return 'Template name must be at least 3 characters';
}
return true;
},
transform: (value) => value.trim()
});
this.steps.push({
id: 'id',
title: 'Template ID',
description: 'Enter a unique identifier (lowercase, hyphens)',
type: 'input',
required: true,
default: (answers) => answers.name.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
validate: (value) => {
if (!/^[a-z0-9-]+$/.test(value)) {
return 'ID must contain only lowercase letters, numbers, and hyphens';
}
return true;
}
});
this.steps.push({
id: 'version',
title: 'Initial Version',
description: 'Enter the initial version (semantic versioning)',
type: 'input',
required: true,
default: '1.0.0',
validate: (value) => {
const semver = require('semver');
if (!semver.valid(value)) {
return 'Version must be valid semantic version (e.g., 1.0.0)';
}
return true;
}
});
this.steps.push({
id: 'description',
title: 'Description',
description: 'Provide a description of your template',
type: 'input',
required: true,
validate: (value) => {
if (!value || value.trim().length < 10) {
return 'Description must be at least 10 characters';
}
return true;
}
});
this.steps.push({
id: 'category',
title: 'Template Category',
description: 'Select the category that best describes your template',
type: 'select',
required: true,
choices: Object.values(template_engine_1.TemplateCategory).map(cat => ({
value: cat,
title: this.formatCategoryName(cat)
})),
default: template_engine_1.TemplateCategory.CUSTOM
});
this.steps.push({
id: 'tags',
title: 'Tags',
description: 'Enter tags for your template (comma-separated)',
type: 'input',
transform: (value) => value.split(',').map(t => t.trim()).filter(Boolean),
default: []
});
// Author Information
this.steps.push({
id: 'author',
title: 'Author',
description: 'Enter the author name',
type: 'input',
default: process.env.USER || process.env.USERNAME
});
this.steps.push({
id: 'license',
title: 'License',
description: 'Select a license for your template',
type: 'select',
choices: [
{ value: 'MIT', title: 'MIT License' },
{ value: 'Apache-2.0', title: 'Apache License 2.0' },
{ value: 'GPL-3.0', title: 'GNU GPL v3' },
{ value: 'BSD-3-Clause', title: 'BSD 3-Clause' },
{ value: 'ISC', title: 'ISC License' },
{ value: 'UNLICENSED', title: 'Unlicensed (Private)' },
{ value: 'custom', title: 'Custom License' }
],
default: 'MIT'
});
this.steps.push({
id: 'repository',
title: 'Repository URL',
description: 'Enter the repository URL (optional)',
type: 'input',
validate: (value) => {
if (value && !value.match(/^https?:\/\/.+/)) {
return 'Repository URL must start with http:// or https://';
}
return true;
}
});
// Template Features
this.steps.push({
id: 'features',
title: 'Template Features',
description: 'Select features to include in your template',
type: 'multiselect',
choices: [
{ value: 'inheritance', title: 'Template Inheritance (extends other templates)' },
{ value: 'interfaces', title: 'Template Interfaces (implements contracts)' },
{ value: 'variables', title: 'User Variables (customizable values)' },
{ value: 'hooks', title: 'Lifecycle Hooks (pre/post actions)' },
{ value: 'conditional', title: 'Conditional Files (based on variables)' },
{ value: 'merge', title: 'File Merging (merge with existing files)' },
{ value: 'examples', title: 'Usage Examples' }
],
default: ['variables']
});
// Variable Configuration
this.steps.push({
id: 'variables',
title: 'Configure Variables',
description: 'Define template variables',
type: 'custom',
when: (answers) => answers.features.includes('variables'),
customHandler: async (answers) => {
const variables = await this.configureVariables();
return variables;
}
});
// File Configuration
this.steps.push({
id: 'files',
title: 'Configure Files',
description: 'Define template files',
type: 'custom',
required: true,
customHandler: async (answers) => {
const files = await this.configureFiles(answers);
return files;
}
});
// Hook Configuration
this.steps.push({
id: 'hooks',
title: 'Configure Hooks',
description: 'Define lifecycle hooks',
type: 'custom',
when: (answers) => answers.features.includes('hooks'),
customHandler: async (answers) => {
const hooks = await this.configureHooks();
return hooks;
}
});
// Inheritance Configuration
this.steps.push({
id: 'extends',
title: 'Parent Templates',
description: 'Enter parent template IDs (comma-separated)',
type: 'input',
when: (answers) => answers.features.includes('inheritance'),
transform: (value) => value.split(',').map(t => t.trim()).filter(Boolean),
default: []
});
// Interface Configuration
this.steps.push({
id: 'implements',
title: 'Interface Templates',
description: 'Enter interface template IDs (comma-separated)',
type: 'input',
when: (answers) => answers.features.includes('interfaces'),
transform: (value) => value.split(',').map(t => t.trim()).filter(Boolean),
default: []
});
}
async run() {
this.emit('wizard:start');
try {
// Collect answers for all steps
const answers = {};
for (const step of this.steps) {
// Check if step should be shown
if (step.when && !step.when(answers)) {
continue;
}
this.emit('step:start', step);
let value;
if (step.type === 'custom' && step.customHandler) {
value = await step.customHandler(answers);
}
else {
value = await this.promptStep(step, answers);
}
// Apply transformation
if (step.transform) {
value = step.transform(value);
}
answers[step.id] = value;
this.emit('step:complete', { step, value });
}
// Build template from answers
const template = this.buildTemplate(answers);
// Validate if requested
let validationResult;
if (this.options.validate !== false) {
validationResult = await this.validator.validate(template);
if (!validationResult.valid) {
const shouldContinue = await this.prompter.prompt({
type: 'confirm',
name: 'continue',
message: `Template validation found ${validationResult.errors.length} errors. Continue anyway?`,
initial: false
});
if (!shouldContinue.continue) {
throw new Error('Template validation failed');
}
}
}
// Determine output path
const outputPath = this.options.outputPath ||
path.join(process.cwd(), 'templates', template.id);
// Save template
await this.saveTemplate(template, outputPath);
// Generate examples if requested
let examples = [];
if (this.options.includeExamples && answers.features.includes('examples')) {
examples = await this.generateExamples(template, outputPath);
}
const result = {
template,
outputPath,
validated: this.options.validate !== false,
validationResult,
examples
};
this.emit('wizard:complete', result);
return result;
}
catch (error) {
this.emit('wizard:error', error);
throw error;
}
}
async promptStep(step, answers) {
const config = {
type: step.type,
name: 'value',
message: step.title,
validate: step.validate
};
if (step.description) {
config.message = `${step.title}\n ${step.description}`;
}
if (step.choices) {
config.choices = step.choices;
}
if (step.default !== undefined) {
config.default = typeof step.default === 'function' ? step.default(answers) : step.default;
}
const result = await this.prompter.prompt(config);
return result.value;
}
async configureVariables() {
const variables = [];
let addMore = true;
while (addMore) {
console.log('\nš Configure a new variable:');
const variable = {
name: '',
type: 'string',
description: '',
required: true
};
// Variable name
const nameResult = await this.prompter.prompt({
type: 'text',
name: 'name',
message: 'Variable name',
validate: (value) => {
if (!value || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(value)) {
return 'Variable name must start with letter/underscore and contain only letters, numbers, underscores';
}
if (variables.some(v => v.name === value)) {
return 'Variable name already exists';
}
return true;
}
});
variable.name = nameResult.name;
// Variable type
const typeResult = await this.prompter.prompt({
type: 'select',
name: 'type',
message: 'Variable type',
choices: [
{ value: 'string', title: 'String' },
{ value: 'number', title: 'Number' },
{ value: 'boolean', title: 'Boolean' },
{ value: 'choice', title: 'Choice (from list)' },
{ value: 'array', title: 'Array' },
{ value: 'object', title: 'Object' }
]
});
variable.type = typeResult.type;
// Variable description
const descResult = await this.prompter.prompt({
type: 'text',
name: 'description',
message: 'Description',
validate: (value) => value.length > 0 || 'Description is required'
});
variable.description = descResult.description;
// Required?
const requiredResult = await this.prompter.prompt({
type: 'confirm',
name: 'required',
message: 'Is this variable required?',
initial: true
});
variable.required = requiredResult.required;
// Default value
if (!variable.required) {
const defaultResult = await this.prompter.prompt({
type: 'text',
name: 'default',
message: 'Default value'
});
if (defaultResult.default !== '') {
variable.default = defaultResult.default;
}
}
// Choices for choice type
if (variable.type === 'choice') {
const choicesResult = await this.prompter.prompt({
type: 'text',
name: 'choices',
message: 'Enter choices (comma-separated)',
validate: (value) => value.length > 0 || 'At least one choice is required'
});
variable.choices = choicesResult.choices.split(',').map((s) => s.trim());
}
// Pattern validation
const usePatternResult = await this.prompter.prompt({
type: 'confirm',
name: 'usePattern',
message: 'Add pattern validation?',
initial: false
});
if (usePatternResult.usePattern && variable.type === 'string') {
const patternResult = await this.prompter.prompt({
type: 'text',
name: 'pattern',
message: 'Regular expression pattern',
validate: (value) => {
try {
new RegExp(value);
return true;
}
catch {
return 'Invalid regular expression';
}
}
});
variable.pattern = patternResult.pattern;
}
variables.push(variable);
// Add more?
const moreResult = await this.prompter.prompt({
type: 'confirm',
name: 'addMore',
message: 'Add another variable?',
initial: false
});
addMore = moreResult.addMore;
}
return variables;
}
async configureFiles(answers) {
const files = [];
let addMore = true;
// Create template directory structure
const templateDir = this.options.templatePath || path.join(process.cwd(), 'template-files');
await fs.ensureDir(templateDir);
console.log(`\nš Template files will be created in: ${templateDir}`);
while (addMore) {
console.log('\nš Configure a new file:');
const file = {
source: '',
destination: ''
};
// File destination
const destResult = await this.prompter.prompt({
type: 'text',
name: 'destination',
message: 'Destination path (in generated project)',
validate: (value) => value.length > 0 || 'Destination is required'
});
file.destination = destResult.destination;
// Source type
const sourceTypeResult = await this.prompter.prompt({
type: 'select',
name: 'sourceType',
message: 'How to create the source file?',
choices: [
{ value: 'create', title: 'Create new file with content' },
{ value: 'existing', title: 'Use existing file' },
{ value: 'inline', title: 'Enter content inline' }
]
});
if (sourceTypeResult.sourceType === 'create') {
// Create source file
const filename = path.basename(file.destination);
const sourcePath = path.join(templateDir, filename + '.hbs');
const contentResult = await this.prompter.prompt({
type: 'text',
name: 'content',
message: 'Enter file content (Handlebars template)'
});
await fs.writeFile(sourcePath, contentResult.content);
file.source = path.relative(process.cwd(), sourcePath);
}
else if (sourceTypeResult.sourceType === 'existing') {
const sourceResult = await this.prompter.prompt({
type: 'text',
name: 'source',
message: 'Source file path',
validate: async (value) => {
if (!value)
return 'Source path is required';
if (!await fs.pathExists(value))
return 'File does not exist';
return true;
}
});
file.source = sourceResult.source;
}
else {
// Inline content
const filename = `inline-${Date.now()}.hbs`;
const sourcePath = path.join(templateDir, filename);
const contentResult = await this.prompter.prompt({
type: 'text',
name: 'content',
message: 'Enter file content (single line)'
});
await fs.writeFile(sourcePath, contentResult.content);
file.source = path.relative(process.cwd(), sourcePath);
}
// Transform type
const transformResult = await this.prompter.prompt({
type: 'select',
name: 'transform',
message: 'Template engine',
choices: [
{ value: 'handlebars', title: 'Handlebars (default)' },
{ value: 'none', title: 'No transformation (copy as-is)' }
],
initial: 'handlebars'
});
if (transformResult.transform !== 'handlebars') {
file.transform = transformResult.transform;
}
// Conditional?
if (answers.features.includes('conditional')) {
const conditionalResult = await this.prompter.prompt({
type: 'confirm',
name: 'conditional',
message: 'Make this file conditional?',
initial: false
});
if (conditionalResult.conditional) {
const conditionResult = await this.prompter.prompt({
type: 'text',
name: 'condition',
message: 'Condition expression (JavaScript)',
initial: 'context.variables.includeFeature === true'
});
file.condition = conditionResult.condition;
}
}
// Merge strategy?
if (answers.features.includes('merge')) {
const mergeResult = await this.prompter.prompt({
type: 'confirm',
name: 'merge',
message: 'Enable merging if file exists?',
initial: false
});
if (mergeResult.merge) {
file.merge = true;
const strategyResult = await this.prompter.prompt({
type: 'select',
name: 'mergeStrategy',
message: 'Merge strategy',
choices: [
{ value: 'override', title: 'Override (replace existing)' },
{ value: 'append', title: 'Append to existing' },
{ value: 'prepend', title: 'Prepend to existing' },
{ value: 'deep', title: 'Deep merge (JSON/YAML only)' }
],
initial: 'override'
});
if (strategyResult.mergeStrategy !== 'override') {
file.mergeStrategy = strategyResult.mergeStrategy;
}
}
}
files.push(file);
// Add more?
const moreResult = await this.prompter.prompt({
type: 'confirm',
name: 'addMore',
message: 'Add another file?',
initial: true
});
addMore = moreResult.addMore;
}
return files;
}
async configureHooks() {
const hooks = [];
let addMore = true;
while (addMore) {
console.log('\nšŖ Configure a new hook:');
const hook = {
type: template_engine_1.HookType.AFTER_PROCESS,
name: ''
};
// Hook type
const typeResult = await this.prompter.prompt({
type: 'select',
name: 'type',
message: 'Hook type',
choices: [
{ value: template_engine_1.HookType.BEFORE_PROCESS, title: 'Before Process (before template processing)' },
{ value: template_engine_1.HookType.AFTER_PROCESS, title: 'After Process (after all files)' },
{ value: template_engine_1.HookType.BEFORE_FILE, title: 'Before File (before each file)' },
{ value: template_engine_1.HookType.AFTER_FILE, title: 'After File (after each file)' },
{ value: template_engine_1.HookType.VALIDATE, title: 'Validate (validation hook)' },
{ value: template_engine_1.HookType.CLEANUP, title: 'Cleanup (cleanup hook)' }
]
});
hook.type = typeResult.type;
// Hook name
const nameResult = await this.prompter.prompt({
type: 'text',
name: 'name',
message: 'Hook name',
validate: (value) => value.length > 0 || 'Name is required'
});
hook.name = nameResult.name;
// Hook description
const descResult = await this.prompter.prompt({
type: 'text',
name: 'description',
message: 'Description (optional)'
});
if (descResult.description) {
hook.description = descResult.description;
}
// Hook implementation
const implResult = await this.prompter.prompt({
type: 'select',
name: 'implementation',
message: 'Hook implementation',
choices: [
{ value: 'command', title: 'Shell command' },
{ value: 'script', title: 'JavaScript code' }
]
});
if (implResult.implementation === 'command') {
const commandResult = await this.prompter.prompt({
type: 'text',
name: 'command',
message: 'Shell command',
validate: (value) => value.length > 0 || 'Command is required'
});
hook.command = commandResult.command;
}
else {
const scriptResult = await this.prompter.prompt({
type: 'text',
name: 'script',
message: 'JavaScript code (has access to context and require)'
});
hook.script = scriptResult.script;
}
// Allow failure?
const allowFailureResult = await this.prompter.prompt({
type: 'confirm',
name: 'allowFailure',
message: 'Allow hook to fail without stopping?',
initial: false
});
hook.allowFailure = allowFailureResult.allowFailure;
hooks.push(hook);
// Add more?
const moreResult = await this.prompter.prompt({
type: 'confirm',
name: 'addMore',
message: 'Add another hook?',
initial: false
});
addMore = moreResult.addMore;
}
return hooks;
}
buildTemplate(answers) {
const template = {
id: answers.id,
name: answers.name,
version: answers.version,
description: answers.description,
category: answers.category,
tags: answers.tags || [],
variables: answers.variables || [],
files: answers.files || [],
hooks: answers.hooks || [],
metadata: {
created: new Date(),
updated: new Date()
}
};
// Add optional fields
if (answers.author)
template.author = answers.author;
if (answers.license)
template.license = answers.license;
if (answers.repository)
template.repository = answers.repository;
if (answers.extends && answers.extends.length > 0)
template.extends = answers.extends;
if (answers.implements && answers.implements.length > 0)
template.implements = answers.implements;
// Merge with defaults if provided
if (this.options.defaults) {
Object.assign(template, this.options.defaults);
}
return template;
}
async saveTemplate(template, outputPath) {
await fs.ensureDir(outputPath);
// Save template.yaml
const templatePath = path.join(outputPath, 'template.yaml');
const yamlContent = yaml.dump(template, {
skipInvalid: true,
noRefs: true,
sortKeys: true
});
await fs.writeFile(templatePath, yamlContent);
// Create README
const readmePath = path.join(outputPath, 'README.md');
const readmeContent = this.generateReadme(template);
await fs.writeFile(readmePath, readmeContent);
// Copy template files to template directory
if (this.options.templatePath && this.options.templatePath !== outputPath) {
const filesDir = path.join(outputPath, 'files');
await fs.ensureDir(filesDir);
for (const file of template.files) {
if (file.source && !path.isAbsolute(file.source)) {
const sourcePath = path.join(this.options.templatePath, file.source);
if (await fs.pathExists(sourcePath)) {
const destPath = path.join(filesDir, path.basename(file.source));
await fs.copy(sourcePath, destPath);
// Update file source to relative path
file.source = path.join('files', path.basename(file.source));
}
}
}
// Update template.yaml with new paths
await fs.writeFile(templatePath, yaml.dump(template));
}
this.emit('template:saved', { template, outputPath });
}
generateReadme(template) {
const sections = [];
sections.push(`# ${template.name}`);
sections.push('');
sections.push(template.description);
sections.push('');
// Metadata
sections.push('## Information');
sections.push('');
sections.push(`- **ID**: ${template.id}`);
sections.push(`- **Version**: ${template.version}`);
sections.push(`- **Category**: ${this.formatCategoryName(template.category)}`);
if (template.author)
sections.push(`- **Author**: ${template.author}`);
if (template.license)
sections.push(`- **License**: ${template.license}`);
if (template.tags.length > 0)
sections.push(`- **Tags**: ${template.tags.join(', ')}`);
sections.push('');
// Usage
sections.push('## Usage');
sections.push('');
sections.push('```bash');
sections.push(`re-shell create my-project --template ${template.id}`);
sections.push('```');
sections.push('');
// Variables
if (template.variables.length > 0) {
sections.push('## Variables');
sections.push('');
sections.push('| Name | Type | Required | Description | Default |');
sections.push('|------|------|----------|-------------|---------|');
for (const variable of template.variables) {
const required = variable.required ? 'Yes' : 'No';
const defaultValue = variable.default !== undefined ? `\`${JSON.stringify(variable.default)}\`` : '-';
sections.push(`| ${variable.name} | ${variable.type} | ${required} | ${variable.description} | ${defaultValue} |`);
}
sections.push('');
}
// Files
if (template.files.length > 0) {
sections.push('## Files');
sections.push('');
sections.push('This template will create the following files:');
sections.push('');
for (const file of template.files) {
let line = `- \`${file.destination}\``;
if (file.condition)
line += ' (conditional)';
sections.push(line);
}
sections.push('');
}
// Hooks
if (template.hooks.length > 0) {
sections.push('## Hooks');
sections.push('');
sections.push('This template includes the following hooks:');
sections.push('');
for (const hook of template.hooks) {
sections.push(`- **${hook.name}** (${hook.type})`);
if (hook.description)
sections.push(` ${hook.description}`);
}
sections.push('');
}
// Features
const features = [];
if (template.extends && template.extends.length > 0) {
features.push(`Extends: ${template.extends.join(', ')}`);
}
if (template.implements && template.implements.length > 0) {
features.push(`Implements: ${template.implements.join(', ')}`);
}
if (features.length > 0) {
sections.push('## Features');
sections.push('');
for (const feature of features) {
sections.push(`- ${feature}`);
}
sections.push('');
}
// Development
sections.push('## Development');
sections.push('');
sections.push('To modify this template:');
sections.push('');
sections.push('1. Edit `template.yaml` to update metadata and configuration');
sections.push('2. Modify files in the template directory');
sections.push('3. Test your changes: `re-shell create test-project --template ./path/to/template`');
sections.push('');
return sections.join('\n');
}
async generateExamples(template, outputPath) {
const examples = [];
// Basic example
const basicExample = {
name: 'basic',
description: 'Basic usage with default values',
files: [],
command: `re-shell create my-app --template ${template.id}`
};
// Create example output
const exampleDir = path.join(outputPath, 'examples', 'basic');
await fs.ensureDir(exampleDir);
// Generate sample files
for (const file of template.files.slice(0, 3)) { // Show first 3 files
const content = `# Example output for ${file.destination}\n# This file would be generated with default variable values`;
basicExample.files.push({
path: file.destination,
content
});
await fs.writeFile(path.join(exampleDir, path.basename(file.destination)), content);
}
examples.push(basicExample);
// Advanced example with custom variables
if (template.variables.length > 0) {
const advancedExample = {
name: 'advanced',
description: 'Advanced usage with custom variables',
files: [],
command: `re-shell create my-app --template ${template.id}`
};
// Add variable flags
for (const variable of template.variables.slice(0, 3)) {
advancedExample.command += ` --var ${variable.name}=customValue`;
}
examples.push(advancedExample);
}
return examples;
}
formatCategoryName(category) {
return category
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ');
}
// Public methods
addStep(step) {
this.steps.push(step);
}
removeStep(id) {
const index = this.steps.findIndex(s => s.id === id);
if (index >= 0) {
this.steps.splice(index, 1);
return true;
}
return false;
}
getSteps() {
return [...this.steps];
}
setOptions(options) {
Object.assign(this.options, options);
}
}
exports.TemplateWizard = TemplateWizard;
// Convenience function for quick template creation
async function createTemplateInteractive(options) {
const wizard = new TemplateWizard(options);
return await wizard.run();
}
// Export for CLI integration
function createTemplateWizard(options) {
return new TemplateWizard(options);
}