claude-code-templates
Version:
CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects
275 lines (242 loc) • 8.73 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
/**
* Scans and returns available commands for a given language
* @param {string} language - The language template to scan
* @returns {Array} Array of available commands with metadata
*/
function getAvailableCommands(language) {
const templatesDir = path.join(__dirname, '../templates');
const languageDir = path.join(templatesDir, language);
// Check if language directory exists
if (!fs.existsSync(languageDir)) {
return [];
}
const commands = [];
// Scan main .claude/commands directory
const mainCommandsDir = path.join(languageDir, '.claude', 'commands');
if (fs.existsSync(mainCommandsDir)) {
const mainCommands = scanCommandsInDirectory(mainCommandsDir, 'core');
commands.push(...mainCommands);
}
// Scan framework-specific commands in examples
const frameworksDir = path.join(languageDir, 'examples');
if (fs.existsSync(frameworksDir)) {
const frameworkDirs = fs.readdirSync(frameworksDir).filter(dir => {
return fs.statSync(path.join(frameworksDir, dir)).isDirectory();
});
frameworkDirs.forEach(framework => {
const frameworkCommandsDir = path.join(frameworksDir, framework, '.claude', 'commands');
if (fs.existsSync(frameworkCommandsDir)) {
const frameworkCommands = scanCommandsInDirectory(frameworkCommandsDir, framework);
commands.push(...frameworkCommands);
}
});
}
// Remove duplicates based on command name
const uniqueCommands = commands.reduce((acc, command) => {
const existing = acc.find(c => c.name === command.name);
if (!existing) {
acc.push(command);
} else {
// If duplicate, prefer core commands over framework-specific ones
if (command.category === 'core' && existing.category !== 'core') {
const index = acc.findIndex(c => c.name === command.name);
acc[index] = command;
}
}
return acc;
}, []);
return uniqueCommands.sort((a, b) => {
// Sort by category first (core first), then by name
if (a.category !== b.category) {
return a.category === 'core' ? -1 : 1;
}
return a.name.localeCompare(b.name);
});
}
/**
* Scans a directory for command files and returns command metadata
* @param {string} commandsDir - Directory to scan
* @param {string} category - Category of commands (core, react-app, node-api, etc.)
* @returns {Array} Array of command objects
*/
function scanCommandsInDirectory(commandsDir, category) {
const commands = [];
try {
const files = fs.readdirSync(commandsDir);
files.forEach(file => {
if (path.extname(file) === '.md') {
const commandName = path.basename(file, '.md');
const filePath = path.join(commandsDir, file);
// Read the command file to extract metadata
const content = fs.readFileSync(filePath, 'utf8');
const metadata = parseCommandMetadata(content, commandName);
commands.push({
name: commandName,
displayName: createShortDisplayName(commandName, metadata.title),
description: createShortDescription(metadata.description, commandName),
category: category,
filePath: filePath,
checked: metadata.defaultChecked || false
});
}
});
} catch (error) {
console.warn(`Warning: Could not scan commands in ${commandsDir}:`, error.message);
}
return commands;
}
/**
* Parses command metadata from markdown file content
* @param {string} content - The markdown content
* @param {string} commandName - The command name
* @returns {Object} Parsed metadata
*/
function parseCommandMetadata(content, commandName) {
const metadata = {
title: null,
description: null,
defaultChecked: false
};
const lines = content.split('\n');
// Extract title from first heading
for (let i = 0; i < Math.min(lines.length, 10); i++) {
const line = lines[i].trim();
if (line.startsWith('# ')) {
metadata.title = line.substring(2).trim();
break;
}
}
// Extract description from purpose section or first paragraph
const purposeMatch = content.match(/## Purpose\s*\n\s*(.+?)(?=\n\n|\n##|$)/s);
if (purposeMatch) {
metadata.description = purposeMatch[1].trim().replace(/\n/g, ' ');
} else {
// Try to find first paragraph after title
const paragraphMatch = content.match(/^#[^\n]*\n\s*\n(.+?)(?=\n\n|\n##|$)/s);
if (paragraphMatch) {
metadata.description = paragraphMatch[1].trim().replace(/\n/g, ' ');
}
}
// Determine default checked state (core commands usually checked by default)
const coreCommands = ['test', 'lint', 'debug', 'refactor'];
metadata.defaultChecked = coreCommands.includes(commandName);
return metadata;
}
/**
* Creates a short display name from command name for better console display
* @param {string} commandName - The command name
* @param {string} title - The full title from markdown
* @returns {string} Short display name
*/
function createShortDisplayName(commandName, title) {
// Define mapping for common command names to short display names
const shortNames = {
'api-endpoint': 'API Endpoint',
'debug': 'Debug',
'lint': 'Lint',
'test': 'Test',
'refactor': 'Refactor',
'typescript-migrate': 'TS Migration',
'npm-scripts': 'NPM Scripts',
'component': 'Component',
'hooks': 'Hooks',
'state-management': 'State Mgmt',
'middleware': 'Middleware',
'route': 'Route',
'database': 'Database',
'components': 'Components',
'services': 'Services',
'composables': 'Composables',
'django-model': 'Django Model',
'flask-route': 'Flask Route',
'git-workflow': 'Git Workflow',
'project-setup': 'Project Setup'
};
// Return predefined short name if available
if (shortNames[commandName]) {
return shortNames[commandName];
}
// If title is short enough, use it
if (title && title.length <= 15) {
return title;
}
// Create short name from command name
return commandName
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
/**
* Creates a short description for better console display
* @param {string} description - The full description
* @param {string} commandName - The command name
* @returns {string} Short description
*/
function createShortDescription(description, commandName) {
// Define short descriptions for common commands
const shortDescriptions = {
'api-endpoint': 'Generate API endpoint',
'debug': 'Debug issues',
'lint': 'Fix linting issues',
'test': 'Run tests',
'refactor': 'Refactor code',
'typescript-migrate': 'Migrate to TypeScript',
'npm-scripts': 'Manage NPM scripts',
'component': 'Create component',
'hooks': 'React hooks helper',
'state-management': 'Manage state',
'middleware': 'Create middleware',
'route': 'Create route',
'database': 'Database operations',
'components': 'Create components',
'services': 'Create services',
'composables': 'Create composables',
'django-model': 'Create Django model',
'flask-route': 'Create Flask route',
'git-workflow': 'Git workflow helper',
'project-setup': 'Setup project'
};
// Return predefined short description if available
if (shortDescriptions[commandName]) {
return shortDescriptions[commandName];
}
// If description exists and is short enough, use it
if (description && description.length <= 40) {
return description;
}
// Truncate long descriptions
if (description && description.length > 40) {
return description.substring(0, 37) + '...';
}
// Fallback to command name
return `${commandName.replace('-', ' ')} command`;
}
/**
* Get commands available for a specific language and framework combination
* @param {string} language - The language template
* @param {string} framework - The framework (optional)
* @returns {Array} Array of available commands
*/
function getCommandsForLanguageAndFramework(language, framework = null) {
const allCommands = getAvailableCommands(language);
if (!framework || framework === 'none') {
// Return only core commands
return allCommands.filter(cmd => cmd.category === 'core');
}
// Return core commands + framework-specific commands
return allCommands.filter(cmd =>
cmd.category === 'core' ||
cmd.category === framework ||
cmd.category === `${framework}-app`
);
}
module.exports = {
getAvailableCommands,
getCommandsForLanguageAndFramework,
scanCommandsInDirectory,
parseCommandMetadata,
createShortDisplayName,
createShortDescription
};