metalsmith-plugin-mcp-server
Version:
MCP server for scaffolding and validating high-quality Metalsmith plugins with native methods enforcement
1,033 lines (894 loc) • 36.9 kB
JavaScript
/**
* CLI wrapper for the Metalsmith Plugin MCP Server
*
* This provides a command-line interface for using the MCP server tools
* without requiring an AI assistant setup.
*/
import { spawn } from 'child_process';
import { fileURLToPath } from 'url';
import path, { dirname, join } from 'path';
import { promises as fs } from 'fs';
import { homedir } from 'os';
import chalk from 'chalk';
import readline from 'readline';
import { sanitizePath } from './utils/path-security.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
/**
* Chalk styling constants for consistent output formatting
*/
const styles = {
error: chalk.red,
success: chalk.green,
warning: chalk.yellow,
info: chalk.gray,
bold: chalk.bold,
header: chalk.bold
};
/**
* Default configuration values
*/
const DEFAULT_CONFIG = {
features: ['async-processing'],
license: 'MIT',
author: '',
outputPath: '.'
};
/**
* Read user configuration from .metalsmith-plugin-mcp file
* Looks in current directory and home directory
* @returns {Promise<Object>} User configuration merged with defaults
*/
async function readUserConfig() {
const configFiles = [join(process.cwd(), '.metalsmith-plugin-mcp'), join(homedir(), '.metalsmith-plugin-mcp')];
let userConfig = {};
for (const configFile of configFiles) {
try {
const content = await fs.readFile(configFile, 'utf8');
const config = JSON.parse(content);
userConfig = { ...userConfig, ...config };
} catch {
// Config file doesn't exist or is invalid, skip it
}
}
return { ...DEFAULT_CONFIG, ...userConfig };
}
/**
* Prompt user for input
* @param {string} question - The question to ask
* @param {string} [defaultValue] - Default value if user presses enter
* @returns {Promise<string>} User's input
*/
function prompt(question, defaultValue) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve) => {
const displayQuestion = defaultValue ? `${question} (${styles.info(defaultValue)}): ` : `${question}: `;
rl.question(displayQuestion, (answer) => {
rl.close();
resolve(answer.trim() || defaultValue || '');
});
});
}
const args = process.argv.slice(2);
const command = args[0];
/**
* Display help information for the CLI
* Shows available commands, usage examples, and setup instructions
* @returns {void}
*/
function showHelp() {
console.warn(chalk.bold('\nMetalsmith Plugin MCP Server'));
console.warn(chalk.gray('MCP server for scaffolding and validating high-quality Metalsmith plugins\n'));
console.warn(chalk.bold('Usage:'));
console.warn(' npx metalsmith-plugin-mcp-server <command> [options]\n');
console.warn(chalk.bold('Commands:'));
console.warn(' help Show this help message');
console.warn(' version Show version information');
console.warn(' server Start the MCP server (for AI assistants)');
console.warn(' config Show current configuration and setup');
console.warn(' scaffold [name] [description] [path] Create a new Metalsmith plugin');
console.warn(' validate [path] [--functional] Validate an existing plugin');
console.warn(' audit [path] [--fix] [--output=json|markdown] Run comprehensive plugin audit');
console.warn(' batch-audit [path] [--fix] [--output=json|markdown] Run audit on multiple plugins');
console.warn(' configs [path] Generate configuration files');
console.warn(' show-template [type] Display recommended configuration templates');
console.warn(' list-templates List all available templates');
console.warn(' get-template [name] Get specific template content (e.g., plugin/CLAUDE.md)');
console.warn(' install-claude-md [path] [--replace] [--dry-run] Install CLAUDE.md with AI assistant instructions');
console.warn(' update-deps [path] [--install] [--test] Update dependencies in plugin(s)\n');
console.warn(chalk.gray('Note: Commands can be run in guided mode by omitting parameters\n'));
console.warn(chalk.bold('Examples:'));
console.warn(' npx metalsmith-plugin-mcp-server version # Show version');
console.warn(' npx metalsmith-plugin-mcp-server config # Show current setup');
console.warn(' npx metalsmith-plugin-mcp-server scaffold my-plugin "Processes my files" ./plugins');
console.warn(' npx metalsmith-plugin-mcp-server scaffold # Guided mode');
console.warn(' npx metalsmith-plugin-mcp-server validate ./metalsmith-existing-plugin');
console.warn(' npx metalsmith-plugin-mcp-server validate ./ --functional # Run tests & coverage');
console.warn(' npx metalsmith-plugin-mcp-server audit ./my-plugin # Comprehensive plugin audit');
console.warn(' npx metalsmith-plugin-mcp-server audit ./my-plugin --fix # Audit with automatic fixes');
console.warn(' npx metalsmith-plugin-mcp-server batch-audit ./plugins # Audit multiple plugins');
console.warn(' npx metalsmith-plugin-mcp-server configs ./my-plugin');
console.warn(' npx metalsmith-plugin-mcp-server show-template release-it # Show .release-it.json template');
console.warn(' npx metalsmith-plugin-mcp-server list-templates # Show all available templates');
console.warn(' npx metalsmith-plugin-mcp-server get-template plugin/CLAUDE.md # Get CLAUDE.md template');
console.warn(' npx metalsmith-plugin-mcp-server install-claude-md # Smart merge CLAUDE.md in current dir');
console.warn(' npx metalsmith-plugin-mcp-server install-claude-md --replace # Replace existing CLAUDE.md');
console.warn(' npx metalsmith-plugin-mcp-server install-claude-md --dry-run # Preview changes without applying');
console.warn(' npx metalsmith-plugin-mcp-server update-deps ./plugins # Update all plugins');
console.warn(' npx metalsmith-plugin-mcp-server update-deps ./my-plugin # Update single plugin');
console.warn(' npx metalsmith-plugin-mcp-server update-deps ./ --install --test # Update, install & test\n');
console.warn(chalk.bold('MCP Server Setup:'));
console.warn(' For use with Claude Desktop or Claude Code, run:');
console.warn(' npx metalsmith-plugin-mcp-server server\n');
console.warn(chalk.gray('For more information, visit:'));
console.warn(chalk.gray('https://github.com/wernerglinka/metalsmith-plugin-mcp-server\n'));
}
/**
* Start the MCP server for AI assistant integration
* Spawns the actual MCP server process and handles its lifecycle
* @returns {void}
*/
function startServer() {
// Start the actual MCP server
const serverPath = join(__dirname, 'index.js');
const child = spawn('node', [serverPath], {
stdio: 'inherit'
});
child.on('error', (err) => {
console.error(chalk.red('Failed to start MCP server:'), err);
process.exit(1);
});
child.on('exit', (code) => {
if (code !== 0) {
console.error(chalk.red(`MCP server exited with code ${code}`));
process.exit(code);
}
});
}
/**
* Scaffold a new Metalsmith plugin
* Creates a complete plugin structure with all necessary files and configurations
* @param {string} name - The name of the plugin (exact name as provided by user)
* @param {string} description - What the plugin does (required)
* @param {string} [outputPath] - The directory where the plugin will be created (uses config default if not provided)
* @returns {Promise<void>}
*/
async function runScaffold(name, description, userOutputPath) {
// Interactive mode if parameters are missing
if (!name) {
console.warn(styles.header('\nScaffold a new Metalsmith plugin\n'));
name = await prompt('Plugin name');
if (!name) {
console.error(styles.error('\nError: Plugin name is required'));
process.exit(1);
}
}
if (!description) {
description = await prompt('Plugin description');
if (!description) {
console.error(styles.error('\nError: Plugin description is required'));
process.exit(1);
}
}
// Load user configuration
const config = await readUserConfig();
let outputPath = userOutputPath;
if (!outputPath) {
const defaultPath = config.outputPath || '.';
outputPath = await prompt('Output path', defaultPath);
}
// Sanitize the output path to prevent traversal attacks
outputPath = sanitizePath(outputPath || '.', process.cwd());
try {
// Import and run the scaffold tool directly
const { pluginScaffoldTool } = await import('./tools/plugin-scaffold.js');
const result = await pluginScaffoldTool({
name,
description,
outputPath,
features: config.features,
author: config.author,
license: config.license
});
// The tool returns a content array, extract the text
if (result.content && result.content[0] && result.content[0].text) {
console.warn(result.content[0].text);
} else {
console.warn(styles.success('\n✓ Plugin scaffolded successfully!'));
}
} catch (error) {
console.error(styles.error('Error scaffolding plugin:'), error.message);
process.exit(1);
}
}
/**
* Validate an existing Metalsmith plugin
* Checks the plugin against quality standards including structure, tests, docs, and configuration
* @param {string} path - Path to the plugin directory to validate
* @returns {Promise<void>}
*/
async function runValidate(userPath, functional = false) {
// Interactive mode if path is missing
if (!userPath) {
console.warn(styles.header('\nValidate a Metalsmith plugin\n'));
userPath = await prompt('Plugin path', '.');
if (!userPath) {
console.error(styles.error('\nError: Plugin path is required'));
process.exit(1);
}
}
try {
// Sanitize the path to prevent traversal attacks
const path = sanitizePath(userPath, process.cwd());
// Import and run the validate tool directly
const { validatePluginTool } = await import('./tools/validate-plugin.js');
const result = await validatePluginTool({
path,
checks: ['structure', 'tests', 'docs', 'package-json', 'eslint', 'coverage'],
functional
});
// The tool returns a content array, extract the text
if (result.content && result.content[0] && result.content[0].text) {
console.warn(result.content[0].text);
} else {
console.warn(styles.error('No validation results returned'));
}
} catch (error) {
console.error(chalk.red('Error validating plugin:'), error.message);
process.exit(1);
}
}
/**
* Run comprehensive plugin audit
* Runs validation, linting, formatting, tests, and coverage checks
* @param {string} path - Path to the plugin directory to audit
* @param {boolean} fix - Whether to apply automatic fixes
* @param {string} outputFormat - Output format (console, json, markdown)
* @returns {Promise<void>}
*/
async function runAudit(userPath, fix = false, outputFormat = 'console') {
// Interactive mode if path is missing
if (!userPath) {
console.warn(styles.header('\nRun comprehensive plugin audit\n'));
userPath = await prompt('Plugin path', '.');
if (!userPath) {
console.error(styles.error('\nError: Plugin path is required'));
process.exit(1);
}
}
try {
// Sanitize the path to prevent traversal attacks
const path = sanitizePath(userPath, process.cwd());
// Import and run the audit tool directly
const { auditPlugin } = await import('./tools/audit-plugin.js');
const result = await auditPlugin({
path,
fix,
output: outputFormat
});
// Output the result based on format
if (outputFormat === 'json' || outputFormat === 'markdown') {
console.log(result);
}
// For console format, the output is already displayed by the audit tool
} catch (error) {
console.error(chalk.red('Error running plugin audit:'), error.message);
process.exit(1);
}
}
/**
* Run batch audit on multiple plugins
* Finds and audits all plugins in a directory, providing summary report
* @param {string} path - Path to search for plugin directories
* @param {boolean} fix - Whether to apply automatic fixes
* @param {string} outputFormat - Output format (console, json, markdown)
* @returns {Promise<void>}
*/
async function runBatchAudit(userPath, fix = false, outputFormat = 'console') {
// Interactive mode if path is missing
if (!userPath) {
console.warn(styles.header('\nRun batch audit on multiple plugins\n'));
userPath = await prompt('Search path', '.');
if (!userPath) {
console.error(styles.error('\nError: Search path is required'));
process.exit(1);
}
}
try {
// Sanitize the path to prevent traversal attacks
const path = sanitizePath(userPath, process.cwd());
// Import and run the batch audit tool directly
const { batchAudit } = await import('./tools/batch-audit.js');
const result = await batchAudit({
path,
fix,
output: outputFormat
});
// Output the result based on format
if (outputFormat === 'json' || outputFormat === 'markdown') {
console.log(result);
}
// For console format, the output is already displayed by the batch audit tool
} catch (error) {
console.error(chalk.red('Error running batch audit:'), error.message);
process.exit(1);
}
}
/**
* Generate configuration files for a Metalsmith plugin
* Creates ESLint, Prettier, EditorConfig, .gitignore, and release-it configuration files
* @param {string} outputPath - The directory where config files will be generated
* @returns {Promise<void>}
*/
async function runGenerateConfigs(userOutputPath) {
// Interactive mode if path is missing
let outputPath = userOutputPath;
if (!outputPath) {
console.warn(styles.header('\nGenerate configuration files\n'));
outputPath = await prompt('Output path', '.');
if (!outputPath) {
console.error(styles.error('\nError: Output path is required'));
process.exit(1);
}
}
// Sanitize the output path to prevent traversal attacks
outputPath = sanitizePath(outputPath, process.cwd());
try {
// Import and run the generate configs tool directly
const { generateConfigsTool } = await import('./tools/generate-configs.js');
const result = await generateConfigsTool({
outputPath,
configs: ['eslint', 'prettier', 'editorconfig', 'gitignore', 'release-it']
});
// The tool returns a content array, extract the text
if (result.content && result.content[0] && result.content[0].text) {
console.warn(result.content[0].text);
} else {
console.warn(styles.success('\n✓ Configuration files generated successfully!'));
}
} catch (error) {
console.error(chalk.red('Error generating configs:'), error.message);
process.exit(1);
}
}
/**
* Show version information
* Displays the current version of the metalsmith-plugin-mcp-server
* @returns {Promise<void>}
*/
async function showVersion() {
try {
const packageJsonPath = join(__dirname, '..', 'package.json');
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
console.warn(styles.header(`\n${packageJson.name}`));
console.warn(styles.info(`Version: ${packageJson.version}`));
console.warn(styles.info(`Description: ${packageJson.description}`));
console.warn();
} catch (error) {
console.error(styles.error('Error reading version information:'), error.message);
process.exit(1);
}
}
/**
* Show current configuration and setup
* Displays the current .metalsmith-plugin-mcp configuration with explanations
* @returns {Promise<void>}
*/
async function showConfig() {
console.warn(styles.header('\nMetalsmith Plugin MCP Server Configuration\n'));
// Find config files and their sources
const configFiles = [
{ path: join(process.cwd(), '.metalsmith-plugin-mcp'), label: 'Local (current directory)' },
{ path: join(homedir(), '.metalsmith-plugin-mcp'), label: 'Global (home directory)' }
];
const foundConfigs = [];
let mergedConfig = { ...DEFAULT_CONFIG };
for (const configFile of configFiles) {
try {
const content = await fs.readFile(configFile.path, 'utf8');
const config = JSON.parse(content);
foundConfigs.push({ ...configFile, config, content });
mergedConfig = { ...mergedConfig, ...config };
} catch {
// Config file doesn't exist or is invalid
}
}
// Show configuration sources
console.warn(styles.bold('Configuration Sources:'));
if (foundConfigs.length === 0) {
console.warn(styles.info(' No configuration files found - using defaults\n'));
} else {
foundConfigs.forEach((found) => {
console.warn(styles.success(` ✓ ${found.label}: ${found.path}`));
});
console.warn();
}
// Show effective configuration
console.warn(styles.bold('Current Configuration:'));
console.warn(` Output Path: ${styles.info(mergedConfig.outputPath)}`);
console.warn(` License: ${styles.info(mergedConfig.license)}`);
console.warn(` Author: ${styles.info(mergedConfig.author || 'Not set - will prompt or use git config')}`);
if (mergedConfig.features && mergedConfig.features.length > 0) {
console.warn(` Features: ${styles.info(mergedConfig.features.join(', '))}`);
} else {
console.warn(` Features: ${styles.info('None (override default)')}`);
}
console.warn();
// Show feature explanations
if (mergedConfig.features && mergedConfig.features.length > 0) {
console.warn(styles.bold('Feature Explanations:'));
mergedConfig.features.forEach((feature) => {
switch (feature) {
case 'async-processing':
console.warn(' • async-processing: Adds batch processing and async capabilities');
break;
case 'background-processing':
console.warn(' • background-processing: Adds worker thread support for concurrent processing');
break;
case 'metadata-generation':
console.warn(' • metadata-generation: Adds metadata extraction and generation features');
break;
default:
console.warn(` • ${feature}: ${styles.warning('Unknown feature - may cause errors')}`);
}
});
console.warn();
}
// Show how configuration affects scaffolding
console.warn(styles.bold('How This Affects Plugin Creation:'));
console.warn(` • New plugins will be created in: ${styles.info(mergedConfig.outputPath)}`);
console.warn(` • Default license will be: ${styles.info(mergedConfig.license)}`);
if (mergedConfig.author) {
console.warn(` • Author will be set to: ${styles.info(mergedConfig.author)}`);
} else {
console.warn(' • Author will be detected from git config or prompted');
}
if (mergedConfig.features && mergedConfig.features.length > 0) {
console.warn(` • Features will be automatically included: ${styles.info(mergedConfig.features.join(', '))}`);
} else {
console.warn(' • No features will be included (overrides default async-processing)');
}
console.warn();
// Show example configuration file
console.warn(styles.bold('Example Configuration File:'));
console.warn(styles.info('Create .metalsmith-plugin-mcp in your project root or home directory:'));
console.warn();
console.warn(
chalk.gray(
JSON.stringify(
{
license: 'MIT',
author: 'Your Name <your.email@example.com>',
outputPath: './plugins',
features: ['async-processing', 'metadata-generation']
},
null,
2
)
)
);
console.warn();
// Show config file locations checked
console.warn(styles.bold('Configuration File Search Order:'));
console.warn(styles.info(' 1. .metalsmith-plugin-mcp (current directory)'));
console.warn(styles.info(' 2. .metalsmith-plugin-mcp (home directory)'));
console.warn(styles.info(' Later files override earlier ones\n'));
console.warn(styles.bold('Valid Features:'));
console.warn(styles.info(' • async-processing: Adds batch processing and async capabilities'));
console.warn(styles.info(' • background-processing: Adds worker thread support for concurrent processing'));
console.warn(styles.info(' • metadata-generation: Adds metadata extraction and generation features\n'));
console.warn(styles.bold('Valid Licenses:'));
console.warn(styles.info(' MIT, Apache-2.0, ISC, BSD-3-Clause, UNLICENSED\n'));
}
/**
* Show recommended configuration templates
* Displays the recommended configuration for various file types
* @param {string} template - Template type to display
* @returns {Promise<void>}
*/
async function runShowTemplate(template) {
// Interactive mode if template is missing
if (!template) {
console.warn(styles.header('\nShow configuration template\n'));
console.warn('Available templates:');
console.warn(' release-it - .release-it.json with secure GitHub token handling');
console.warn(' package-scripts - package.json scripts for secure releases');
console.warn(' eslint - eslint.config.js modern flat config');
console.warn(' prettier - prettier.config.js formatting rules');
console.warn(' gitignore - .gitignore for Metalsmith plugins');
console.warn(' editorconfig - .editorconfig for consistent coding style\n');
template = await prompt('Template type');
if (!template) {
console.error(styles.error('\nError: Template type is required'));
process.exit(1);
}
}
const validTemplates = ['release-it', 'package-scripts', 'eslint', 'prettier', 'gitignore', 'editorconfig'];
if (!validTemplates.includes(template)) {
console.error(styles.error(`\nError: Invalid template type. Valid options: ${validTemplates.join(', ')}`));
process.exit(1);
}
try {
// Import and run the show template tool directly
const { showTemplateTool } = await import('./tools/show-template.js');
const result = await showTemplateTool({ template });
// The tool returns a content array, extract the text
if (result.content && result.content[0] && result.content[0].text) {
console.warn(result.content[0].text);
} else {
console.warn(styles.error('No template content returned'));
}
} catch (error) {
console.error(styles.error('Error showing template:'), error.message);
process.exit(1);
}
}
/**
* List all available templates
* Shows all templates that can be retrieved with get-template
* @returns {Promise<void>}
*/
async function runListTemplates() {
try {
// Import and run the list templates tool directly
const { listTemplatesTool } = await import('./tools/list-templates.js');
const result = await listTemplatesTool();
// The tool returns a content array, extract the text
if (result.content && result.content[0] && result.content[0].text) {
console.warn(result.content[0].text);
} else {
console.error(styles.error('No template list returned'));
}
} catch (error) {
console.error(styles.error('Error listing templates:'), error.message);
process.exit(1);
}
}
/**
* Get specific template content
* Retrieves the exact content of a specific template file
* @param {string} template - Template name (e.g., "plugin/CLAUDE.md")
* @returns {Promise<void>}
*/
async function runGetTemplate(template) {
// Interactive mode if template is missing
if (!template) {
console.warn(styles.header('\nGet template content\n'));
console.warn('Use "list-templates" to see all available templates.\n');
console.warn('Examples:');
console.warn(' plugin/CLAUDE.md - AI development context');
console.warn(' configs/release-it.json - Release configuration');
console.warn(' configs/eslint.config.js- ESLint configuration\n');
template = await prompt('Template name (e.g., plugin/CLAUDE.md)');
if (!template) {
console.error(styles.error('\nError: Template name is required'));
process.exit(1);
}
}
try {
// Import and run the get template tool directly
const { getTemplateTool } = await import('./tools/get-template.js');
const result = await getTemplateTool({ template });
// The tool returns a content array, extract the text
if (result.content && result.content[0] && result.content[0].text) {
console.warn(result.content[0].text);
} else {
console.error(styles.error('No template content returned'));
}
} catch (error) {
console.error(styles.error('Error getting template:'), error.message);
process.exit(1);
}
}
/**
* Install CLAUDE.md template directly into a plugin directory
* Supports smart merging with existing CLAUDE.md files to preserve project-specific content
* @param {string} targetPath - Path where to install CLAUDE.md (defaults to current directory)
* @param {Object} options - Installation options
* @param {boolean} options.replace - Force full replacement instead of smart merge
* @param {boolean} options.dryRun - Show what would be changed without making changes
* @returns {Promise<void>}
*/
async function runInstallClaudeMd(userPath = '.', options = {}) {
const { replace = false, dryRun = false } = options;
try {
console.warn(
styles.header(dryRun ? '\nDry run: CLAUDE.md template analysis\n' : '\nInstalling CLAUDE.md template\n')
);
// Sanitize the target path to prevent traversal attacks
const targetPath = sanitizePath(userPath || '.', process.cwd());
// Get the plugin name from package.json if available
let pluginName = 'your-plugin';
let pluginDescription = 'A Metalsmith plugin';
let camelCaseName = 'yourPlugin';
try {
const packageJsonPath = path.join(targetPath, 'package.json');
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
if (packageJson.name) {
pluginName = packageJson.name;
// Convert kebab-case to camelCase for function names
camelCaseName = pluginName
.replace(/^metalsmith-/, '') // Remove metalsmith- prefix
.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
}
if (packageJson.description) {
pluginDescription = packageJson.description;
}
} catch {
// If no package.json or can't read it, use defaults
console.warn(styles.warning('Note: Could not read package.json, using default values'));
}
const claudemdPath = path.join(targetPath, 'CLAUDE.md');
let existingContent = null;
let hasMcpSection = false;
// Check if CLAUDE.md already exists
try {
existingContent = await fs.readFile(claudemdPath, 'utf8');
hasMcpSection = /## MCP Server Integration|### MCP|Essential MCP Commands/i.test(existingContent);
console.warn(styles.info(`Found existing CLAUDE.md (${existingContent.length} characters)`));
console.warn(styles.info(`Has MCP section: ${hasMcpSection ? 'Yes' : 'No'}`));
} catch {
// File doesn't exist
console.warn(styles.info('No existing CLAUDE.md found'));
}
// Get the MCP template content
const { getTemplateTool } = await import('./tools/get-template.js');
const result = await getTemplateTool({ template: 'plugin/CLAUDE.md' });
if (!result.content || !result.content[0] || !result.content[0].text) {
throw new Error('Could not retrieve CLAUDE.md template');
}
// Extract just the template content (remove the wrapper text)
const fullOutput = result.content[0].text;
// Use more specific regex to match the content section, not internal code blocks
const contentMatch = fullOutput.match(/## Content\n\n```\n([\s\S]*?)\n```\n\n## Usage Notes/);
if (!contentMatch) {
throw new Error('Could not extract template content');
}
let templateContent = contentMatch[1];
// Replace template variables only - leave code blocks untouched
templateContent = templateContent
.replace(/\{\{\s*name\s*\}\}/g, pluginName)
.replace(/\{\{\s*description\s*\}\}/g, pluginDescription)
.replace(/\{\{\s*camelCaseName\s*\}\}/g, camelCaseName);
// No conditional processing - this is markdown with code examples, not a nunjucks template
let finalContent;
let operation;
if (!existingContent) {
// No existing file - create new
finalContent = templateContent;
operation = 'create';
} else if (replace) {
// Force full replacement
finalContent = templateContent;
operation = 'replace';
} else {
// Smart merge
finalContent = smartMergeClaudeMd(existingContent, templateContent, { hasMcpSection });
operation = 'merge';
}
if (dryRun) {
console.warn(styles.info(`\nOperation: ${operation}`));
console.warn(styles.info(`Final content length: ${finalContent.length} characters`));
if (existingContent) {
console.warn(styles.info(`Original length: ${existingContent.length} characters`));
console.warn(
styles.info(
`Change: ${finalContent.length - existingContent.length > 0 ? '+' : ''}${finalContent.length - existingContent.length} characters`
)
);
}
console.warn(styles.warning('\nPreview of changes (first 500 characters):'));
console.warn(styles.info(finalContent.substring(0, 500) + (finalContent.length > 500 ? '...' : '')));
console.warn(styles.info('\nUse without --dry-run to apply changes'));
return;
}
// Handle existing file confirmation
if (existingContent && !replace) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const message = hasMcpSection
? 'Update existing CLAUDE.md with latest MCP guidance? (Y/n) '
: 'Add MCP Server integration to existing CLAUDE.md? (Y/n) ';
const answer = await new Promise((resolve) => {
rl.question(message, resolve);
});
rl.close();
if (answer.toLowerCase() === 'n' || answer.toLowerCase() === 'no') {
console.warn(styles.info('Installation cancelled'));
return;
}
}
await fs.writeFile(claudemdPath, finalContent, 'utf8');
const actionWord = operation === 'create' ? 'created' : operation === 'replace' ? 'replaced' : 'updated';
console.warn(styles.success(`✓ CLAUDE.md ${actionWord} successfully at ${claudemdPath}`));
if (operation === 'merge') {
console.warn(styles.info('✓ Preserved existing project-specific content'));
console.warn(
styles.info(
hasMcpSection ? '✓ Updated MCP Server integration section' : '✓ Added MCP Server integration section'
)
);
}
console.warn(styles.info('\nNext steps:'));
console.warn(
'1. Add the MCP server to Claude: claude mcp add metalsmith-plugin npx "metalsmith-plugin-mcp-server@latest" "server"'
);
console.warn('2. Ask Claude to review CLAUDE.md for full context');
console.warn('3. Claude will now have all the instructions to use the MCP server properly\n');
} catch (error) {
console.error(styles.error('Error installing CLAUDE.md:'), error.message);
process.exit(1);
}
}
/**
* Smart merge existing CLAUDE.md content with MCP template
* Preserves project-specific content while adding/updating MCP guidance
* @param {string} existingContent - Current CLAUDE.md content
* @param {string} templateContent - New template content
* @param {Object} context - Merge context information
* @returns {string} Merged content
*/
function smartMergeClaudeMd(existingContent, templateContent, context = {}) {
const { hasMcpSection } = context;
// Extract the MCP section from template
// Match everything from "## MCP Server Integration" until the next ## heading or end of content
const mcpSectionMatch = templateContent.match(/(## MCP Server Integration \(CRITICAL\)[\s\S]*?)(?=\n## |$)/);
const mcpSection = mcpSectionMatch ? mcpSectionMatch[1] : '';
if (!mcpSection) {
throw new Error('Could not extract MCP section from template');
}
let mergedContent;
if (hasMcpSection) {
// Replace existing MCP section with updated one
mergedContent = existingContent.replace(/(## MCP Server Integration[\s\S]*?)(?=\n## |$)/i, `${mcpSection}\n`);
} else {
// Add MCP section after the project overview (or at the beginning)
const overviewMatch = existingContent.match(/(## Project Overview[\s\S]*?)(?=\n## |$)/i);
if (overviewMatch) {
// Insert after Project Overview
mergedContent = existingContent.replace(/(## Project Overview[\s\S]*?)(\n## )/, `$1\n\n${mcpSection}\n$2`);
} else {
// Insert at the beginning after the title
const titleMatch = existingContent.match(/^(# [^\n]*\n)/);
if (titleMatch) {
mergedContent = existingContent.replace(/^(# [^\n]*\n)/, `$1\n${mcpSection}\n\n`);
} else {
// Just prepend
mergedContent = `${mcpSection}\n\n${existingContent}`;
}
}
}
return mergedContent;
}
/**
* Update dependencies in Metalsmith plugin(s)
* Uses npm-check-updates to check and update dependencies
* @param {string} path - Path to plugin directory or parent directory containing plugins
* @param {boolean} install - Auto-install updates after applying them
* @param {boolean} test - Run tests after installing updates
* @returns {Promise<void>}
*/
async function runUpdateDeps(userPath, install = false, test = false) {
// Interactive mode if path is missing
let path = userPath;
if (!path) {
console.warn(styles.header('\nUpdate plugin dependencies\n'));
const choice = await prompt('Update (A)ll plugins in current directory or specific (P)lugin?', 'A');
if (choice.toLowerCase() === 'a' || choice.toLowerCase() === 'all') {
path = '.';
} else {
path = await prompt('Plugin path', '.');
if (!path) {
console.error(styles.error('\nError: Plugin path is required'));
process.exit(1);
}
}
}
// Sanitize the path to prevent traversal attacks
path = sanitizePath(path, process.cwd());
try {
// Import and run the update deps tool directly
const { updateDepsTool } = await import('./tools/update-deps.js');
const result = await updateDepsTool({
path,
major: false, // Only minor/patch updates by default
interactive: false,
dryRun: false,
install,
test
});
// The tool returns a content array, extract the text
if (result.content && result.content[0] && result.content[0].text) {
console.warn(result.content[0].text);
} else {
console.warn(styles.error('No update results returned'));
}
} catch (error) {
console.error(chalk.red('Error updating dependencies:'), error.message);
process.exit(1);
}
}
// Main CLI logic
switch (command) {
case 'help':
case '--help':
case '-h':
case undefined:
showHelp();
break;
case 'version':
case '--version':
case '-v':
showVersion();
break;
case 'server':
startServer();
break;
case 'config':
showConfig();
break;
case 'scaffold':
runScaffold(args[1], args[2], args[3]);
break;
case 'validate':
{
const path = args[1];
const functional = args.includes('--functional');
runValidate(path, functional);
}
break;
case 'audit':
{
const path = args[1];
const fix = args.includes('--fix');
const outputFormat = args.find((arg) => arg.startsWith('--output='))?.split('=')[1] || 'console';
runAudit(path, fix, outputFormat);
}
break;
case 'batch-audit':
{
const path = args[1];
const fix = args.includes('--fix');
const outputFormat = args.find((arg) => arg.startsWith('--output='))?.split('=')[1] || 'console';
runBatchAudit(path, fix, outputFormat);
}
break;
case 'configs':
runGenerateConfigs(args[1]);
break;
case 'show-template':
runShowTemplate(args[1]);
break;
case 'list-templates':
runListTemplates();
break;
case 'get-template':
runGetTemplate(args[1]);
break;
case 'install-claude-md':
{
const targetPath = args[1];
const replace = args.includes('--replace');
const dryRun = args.includes('--dry-run');
runInstallClaudeMd(targetPath, { replace, dryRun });
}
break;
case 'update-deps':
{
const path = args[1];
const install = args.includes('--install');
const test = args.includes('--test');
runUpdateDeps(path, install, test);
}
break;
default:
console.error(styles.error(`Unknown command: ${command}\n`));
showHelp();
process.exit(1);
}