This commit is contained in:
310
src/commands/template.ts
Normal file
310
src/commands/template.ts
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
import inquirer from 'inquirer';
|
||||||
|
import { fileUtils, templateUtils } from '../utils';
|
||||||
|
import { Template, Layout } from '../models/types';
|
||||||
|
|
||||||
|
interface TemplateOptions {
|
||||||
|
name?: string;
|
||||||
|
list?: boolean;
|
||||||
|
create?: boolean;
|
||||||
|
delete?: boolean;
|
||||||
|
apply?: string;
|
||||||
|
variables?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function manageTemplates(options: TemplateOptions): Promise<void> {
|
||||||
|
await fileUtils.initialize();
|
||||||
|
|
||||||
|
if (options.list) {
|
||||||
|
await listTemplates();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.delete) {
|
||||||
|
await deleteTemplate(options.name!);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.create) {
|
||||||
|
await createTemplateInteractive();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.apply) {
|
||||||
|
await applyTemplate(options.apply, options.variables);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await listTemplatesInteractive();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listTemplates(): Promise<void> {
|
||||||
|
const templates = await fileUtils.listTemplates();
|
||||||
|
if (templates.length === 0) {
|
||||||
|
console.log('No templates found. Use "template --create" to create one.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Available Templates:');
|
||||||
|
console.log('-'.repeat(50));
|
||||||
|
|
||||||
|
for (const templateFile of templates) {
|
||||||
|
try {
|
||||||
|
const template = await fileUtils.readTemplate(templateFile) as Template;
|
||||||
|
console.log(` ${template.displayName}`);
|
||||||
|
console.log(` Name: ${template.name}`);
|
||||||
|
console.log(` Description: ${template.description}`);
|
||||||
|
console.log(` Variables: ${template.variables.length}`);
|
||||||
|
console.log('');
|
||||||
|
} catch {
|
||||||
|
console.log(` ${templateFile} (error reading)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listTemplatesInteractive(): Promise<void> {
|
||||||
|
const templates = await fileUtils.listTemplates();
|
||||||
|
if (templates.length === 0) {
|
||||||
|
console.log('No templates found. Use "template --create" to create one.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { selectedTemplate } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'list',
|
||||||
|
name: 'selectedTemplate',
|
||||||
|
message: 'Select a template:',
|
||||||
|
choices: [
|
||||||
|
...templates.map((t) => ({ name: t.replace(/\.(json|yaml|yml)$/, ''), value: t })),
|
||||||
|
new inquirer.Separator(),
|
||||||
|
{ name: 'Create new template', value: '__create__' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (selectedTemplate === '__create__') {
|
||||||
|
await createTemplateInteractive();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await showTemplateDetails(selectedTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showTemplateDetails(templateFile: string): Promise<void> {
|
||||||
|
const template = await fileUtils.readTemplate(templateFile) as Template;
|
||||||
|
|
||||||
|
console.log('\nTemplate Details:');
|
||||||
|
console.log(` Name: ${template.name}`);
|
||||||
|
console.log(` Display Name: ${template.displayName}`);
|
||||||
|
console.log(` Description: ${template.description}`);
|
||||||
|
console.log(` Variables: ${template.variables.length}`);
|
||||||
|
|
||||||
|
if (template.variables.length > 0) {
|
||||||
|
console.log('\nVariables:');
|
||||||
|
for (const variable of template.variables) {
|
||||||
|
const required = variable.required ? ' (required)' : '';
|
||||||
|
const defaultValue = variable.defaultValue ? ` = ${variable.defaultValue}` : '';
|
||||||
|
console.log(` - ${variable.name}${required}${defaultValue}`);
|
||||||
|
console.log(` ${variable.description}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { action } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'list',
|
||||||
|
name: 'action',
|
||||||
|
message: 'What would you like to do?',
|
||||||
|
choices: [
|
||||||
|
{ name: 'Apply template', value: 'apply' },
|
||||||
|
{ name: 'View layout', value: 'view' },
|
||||||
|
{ name: 'Back', value: 'back' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (action === 'apply') {
|
||||||
|
await applyTemplate(template.name);
|
||||||
|
} else if (action === 'view') {
|
||||||
|
console.log('\nTemplate Layout:');
|
||||||
|
console.log(JSON.stringify(template.layout, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createTemplateInteractive(): Promise<void> {
|
||||||
|
const { templateName, displayName, description } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'templateName',
|
||||||
|
message: 'Enter template name (kebab-case):',
|
||||||
|
validate: (input: string) => /^[a-z0-9-]+$/.test(input) || 'Use kebab-case (lowercase with hyphens)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'displayName',
|
||||||
|
message: 'Enter display name:',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'description',
|
||||||
|
message: 'Enter description:',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const layouts = await fileUtils.listLayouts();
|
||||||
|
if (layouts.length === 0) {
|
||||||
|
console.log('No layouts found. Create a layout first, then save it as a template.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { sourceLayout } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'list',
|
||||||
|
name: 'sourceLayout',
|
||||||
|
message: 'Select a layout to use as template:',
|
||||||
|
choices: layouts,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const layout = await fileUtils.readLayout(sourceLayout) as Layout;
|
||||||
|
|
||||||
|
const variables: Record<string, string> = {};
|
||||||
|
const { addVariable } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'addVariable',
|
||||||
|
message: 'Would you like to add template variables?',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (addVariable) {
|
||||||
|
const variableList: Array<{ name: string; description: string; defaultValue: string }> = [];
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-constant-condition
|
||||||
|
while (true) {
|
||||||
|
const { name } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'name',
|
||||||
|
message: 'Enter variable name (e.g., PROJECT_DIR):',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { description } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'description',
|
||||||
|
message: 'Enter variable description:',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { hasDefault } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'hasDefault',
|
||||||
|
message: 'Does this variable have a default value?',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
let defaultValue = '';
|
||||||
|
if (hasDefault) {
|
||||||
|
const { value } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'value',
|
||||||
|
message: 'Enter default value:',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
defaultValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
variableList.push({ name, description, defaultValue });
|
||||||
|
|
||||||
|
const { shouldAddMore } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'shouldAddMore',
|
||||||
|
message: 'Add another variable?',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!shouldAddMore) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const v of variableList) {
|
||||||
|
variables[v.name] = `\${${v.name}}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const substitutedLayout = templateUtils.substituteVariables(layout, variables);
|
||||||
|
|
||||||
|
const template: Template = {
|
||||||
|
name: templateName,
|
||||||
|
displayName,
|
||||||
|
description,
|
||||||
|
variables: Object.entries(variables).map(([name]) => ({
|
||||||
|
name,
|
||||||
|
description: '',
|
||||||
|
required: true,
|
||||||
|
})),
|
||||||
|
layout: substitutedLayout,
|
||||||
|
};
|
||||||
|
|
||||||
|
const filePath = await fileUtils.saveTemplate(templateName, template);
|
||||||
|
console.log(`Template created: ${filePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteTemplate(name: string): Promise<void> {
|
||||||
|
const templates = await fileUtils.listTemplates();
|
||||||
|
const templateFile = templates.find((t) => t.includes(name));
|
||||||
|
|
||||||
|
if (!templateFile) {
|
||||||
|
throw new Error(`Template not found: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { confirm } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'confirm',
|
||||||
|
message: `Delete template "${name}"?`,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (confirm) {
|
||||||
|
await fileUtils.deleteTemplate(templateFile);
|
||||||
|
console.log(`Template deleted: ${name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyTemplate(templateName: string, variables?: Record<string, string>): Promise<void> {
|
||||||
|
const templates = await fileUtils.listTemplates();
|
||||||
|
const templateFile = templates.find((t) => t.includes(templateName));
|
||||||
|
|
||||||
|
if (!templateFile) {
|
||||||
|
throw new Error(`Template not found: ${templateName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = await fileUtils.readTemplate(templateFile) as Template;
|
||||||
|
|
||||||
|
const missingVars = templateUtils.validateTemplateVariables(template, variables || {});
|
||||||
|
if (missingVars.length > 0) {
|
||||||
|
console.log(`Missing required variables: ${missingVars.join(', ')}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const layout = templateUtils.substituteVariables(template.layout, variables || {});
|
||||||
|
|
||||||
|
const { saveAs } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'saveAs',
|
||||||
|
message: 'Save as layout name:',
|
||||||
|
default: `${template.name}-instance`,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const filePath = await fileUtils.saveLayout(saveAs, layout, 'json');
|
||||||
|
console.log(`Layout saved from template: ${filePath}`);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user