accs-cli
Version:
ACCS CLI — Full-featured developer tool for scaffolding, running, building, and managing multi-language projects
269 lines (233 loc) • 8.31 kB
JavaScript
/**
* List templates command - Show available project templates
*/
import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';
import { logger } from '../utils/logger.js';
import { FileUtils } from '../utils/file-utils.js';
const BUILT_IN_TEMPLATES = {
'vanilla-js': {
name: 'Vanilla JavaScript',
description: 'Plain HTML, CSS, and JavaScript project',
tags: ['web', 'frontend', 'beginner'],
features: ['HTML5', 'CSS3', 'Vanilla JS', 'Responsive']
},
'node-express': {
name: 'Node.js Express',
description: 'Express.js web server with modern setup',
tags: ['backend', 'api', 'server'],
features: ['Express.js', 'CORS', 'Error handling', 'REST API']
},
'react': {
name: 'React',
description: 'React application with Vite',
tags: ['frontend', 'spa', 'modern'],
features: ['React 18', 'Vite', 'Hot reload', 'JSX']
},
'vue': {
name: 'Vue.js',
description: 'Vue.js application with Vite',
tags: ['frontend', 'spa', 'progressive'],
features: ['Vue 3', 'Vite', 'Composition API', 'SFC']
},
'typescript': {
name: 'TypeScript',
description: 'TypeScript project with modern tooling',
tags: ['typescript', 'typed', 'modern'],
features: ['TypeScript', 'ES modules', 'Type checking', 'Modern config']
},
'python-flask': {
name: 'Python Flask',
description: 'Flask web application',
tags: ['python', 'backend', 'web'],
features: ['Flask', 'Jinja2', 'SQLAlchemy', 'Blueprint']
},
'php-basic': {
name: 'PHP Basic',
description: 'Basic PHP web application',
tags: ['php', 'backend', 'web'],
features: ['PHP 8+', 'MVC pattern', 'Session handling', 'Database ready']
},
'static-site': {
name: 'Static Site',
description: 'Simple static website template',
tags: ['static', 'simple', 'portfolio'],
features: ['Multi-page', 'SEO friendly', 'Fast loading', 'Mobile first']
}
};
export function listTemplatesCommand(program) {
program
.command('list-templates')
.alias('templates')
.option('-f, --filter <tag>', 'Filter by tag')
.option('-d, --detailed', 'Show detailed information')
.option('-j, --json', 'Output as JSON')
.description('List available project templates')
.action(async (options) => {
try {
await listTemplates(options);
} catch (error) {
logger.error('Failed to list templates:', error.message);
process.exit(1);
}
});
}
async function listTemplates(options) {
// Get built-in templates
let templates = { ...BUILT_IN_TEMPLATES };
// Add custom templates from templates directory
const customTemplates = await getCustomTemplates();
templates = { ...templates, ...customTemplates };
// Filter by tag if specified
if (options.filter) {
templates = Object.fromEntries(
Object.entries(templates).filter(([, template]) =>
template.tags?.includes(options.filter.toLowerCase())
)
);
}
// Output as JSON if requested
if (options.json) {
console.log(JSON.stringify(templates, null, 2));
return;
}
// Display templates
displayTemplates(templates, options);
}
async function getCustomTemplates() {
const templatesDir = FileUtils.getTemplatesDir();
const customTemplates = {};
if (!FileUtils.exists(templatesDir)) {
return customTemplates;
}
try {
const entries = await import('fs').then(fs => fs.promises.readdir(templatesDir));
for (const entry of entries) {
const templatePath = path.join(templatesDir, entry);
if (!FileUtils.isDirectory(templatePath)) continue;
// Skip built-in templates
if (BUILT_IN_TEMPLATES[entry]) continue;
// Look for template metadata
const metaPath = path.join(templatePath, 'template.json');
if (FileUtils.exists(metaPath)) {
try {
const meta = await FileUtils.readJson(metaPath);
customTemplates[entry] = {
name: meta.name || entry,
description: meta.description || 'Custom template',
tags: meta.tags || ['custom'],
features: meta.features || [],
custom: true
};
} catch (error) {
logger.warn(`Invalid template metadata for ${entry}`);
}
} else {
// Default metadata for custom templates without template.json
customTemplates[entry] = {
name: entry.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
description: 'Custom template',
tags: ['custom'],
features: [],
custom: true
};
}
}
} catch (error) {
logger.warn('Could not read custom templates:', error.message);
}
return customTemplates;
}
function displayTemplates(templates, options) {
const templateCount = Object.keys(templates).length;
console.log(boxen(
chalk.blue.bold(`📋 Available Templates (${templateCount})`),
{
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'blue'
}
));
if (templateCount === 0) {
logger.warn('No templates found matching your criteria');
return;
}
// Group templates by category
const categories = groupTemplatesByCategory(templates);
// Display each category
Object.entries(categories).forEach(([category, categoryTemplates]) => {
logger.section(category);
categoryTemplates.forEach(([key, template]) => {
displayTemplate(key, template, options);
});
});
// Show usage instructions
logger.separator();
logger.info('Usage:');
logger.info(` ${chalk.cyan('accs init my-project --template <template-name>')}`);
logger.info(` ${chalk.cyan('accs init my-project')} ${chalk.gray('(interactive selection)')}`);
if (options.filter) {
logger.info(`\nFiltered by tag: ${chalk.yellow(options.filter)}`);
logger.info(`Use ${chalk.cyan('accs list-templates')} to see all templates`);
}
// Show available tags
const allTags = [...new Set(
Object.values(templates).flatMap(t => t.tags || [])
)].sort();
if (allTags.length > 0 && !options.filter) {
logger.info(`\nAvailable tags: ${allTags.map(tag => chalk.yellow(tag)).join(', ')}`);
logger.info(`Filter by tag: ${chalk.cyan('accs list-templates --filter <tag>')}`);
}
}
function groupTemplatesByCategory(templates) {
const categories = {
'Frontend': [],
'Backend': [],
'Full Stack': [],
'Static Sites': [],
'Language Specific': [],
'Custom': []
};
Object.entries(templates).forEach(([key, template]) => {
if (template.custom) {
categories['Custom'].push([key, template]);
} else if (template.tags?.includes('frontend') || template.tags?.includes('spa')) {
categories['Frontend'].push([key, template]);
} else if (template.tags?.includes('backend') || template.tags?.includes('api') || template.tags?.includes('server')) {
categories['Backend'].push([key, template]);
} else if (template.tags?.includes('static') || template.tags?.includes('portfolio')) {
categories['Static Sites'].push([key, template]);
} else if (template.tags?.includes('python') || template.tags?.includes('php') || template.tags?.includes('typescript')) {
categories['Language Specific'].push([key, template]);
} else {
categories['Frontend'].push([key, template]);
}
});
// Remove empty categories
Object.keys(categories).forEach(category => {
if (categories[category].length === 0) {
delete categories[category];
}
});
return categories;
}
function displayTemplate(key, template, options) {
const nameColor = template.custom ? 'magenta' : 'cyan';
const customBadge = template.custom ? chalk.magenta(' [Custom]') : '';
console.log(` ${chalk[nameColor].bold(key)}${customBadge}`);
console.log(` ${template.description}`);
if (options.detailed) {
// Show tags
if (template.tags && template.tags.length > 0) {
const tagString = template.tags.map(tag => chalk.yellow(`#${tag}`)).join(' ');
console.log(` Tags: ${tagString}`);
}
// Show features
if (template.features && template.features.length > 0) {
console.log(` Features: ${template.features.join(', ')}`);
}
console.log('');
}
}