UNPKG

claude-code-templates

Version:

CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects

377 lines (319 loc) 10.4 kB
const BaseValidator = require('../BaseValidator'); const yaml = require('js-yaml'); /** * StructuralValidator - Validates component structure and format * * Checks: * - YAML frontmatter presence and validity * - Required fields (name, description) * - File size limits * - UTF-8 encoding * - Section count limits * - Component type-specific requirements */ class StructuralValidator extends BaseValidator { constructor() { super(); // Configuration limits this.MAX_FILE_SIZE = 100 * 1024; // 100KB this.MAX_SECTION_COUNT = 20; // Prevent context overflow this.MIN_DESCRIPTION_LENGTH = 20; this.MAX_DESCRIPTION_LENGTH = 500; // Required fields by component type this.REQUIRED_FIELDS = { agent: ['name', 'description', 'tools'], command: ['name', 'description'], mcp: ['name', 'description', 'command'], setting: ['name', 'description'], hook: ['name', 'description', 'trigger'] }; // Optional but recommended fields this.RECOMMENDED_FIELDS = { agent: ['model'], command: ['usage', 'examples'], mcp: ['args'], setting: ['type'], hook: ['conditions'] }; } /** * Validate component structure * @param {object} component - Component data * @param {string} component.content - Raw markdown content * @param {string} component.path - File path * @param {string} component.type - Component type (agent, command, mcp, etc.) * @param {object} options - Validation options * @returns {Promise<object>} Validation results */ async validate(component, options = {}) { this.reset(); const { content, path, type } = component; if (!content) { this.addError('STRUCT_E001', 'Component content is empty or missing', { path }); return this.getResults(); } // 1. File size validation this.validateFileSize(content, path); // 2. UTF-8 encoding validation this.validateEncoding(content, path); // 3. Frontmatter validation const frontmatter = this.validateFrontmatter(content, path); if (frontmatter) { // 4. Required fields validation this.validateRequiredFields(frontmatter, type, path); // 5. Description validation this.validateDescription(frontmatter, path); // 6. Tools validation (for agents) if (type === 'agent') { this.validateTools(frontmatter, path); } // 7. Model validation (for agents) if (type === 'agent') { this.validateModel(frontmatter, path); } // 8. Recommended fields check this.checkRecommendedFields(frontmatter, type, path); } // 9. Content structure validation this.validateContentStructure(content, path); // 10. Section count validation this.validateSectionCount(content, path); return this.getResults(); } /** * Validate file size */ validateFileSize(content, path) { const size = Buffer.byteLength(content, 'utf8'); if (size > this.MAX_FILE_SIZE) { this.addError( 'STRUCT_E003', `File size (${(size / 1024).toFixed(2)}KB) exceeds maximum allowed size (${this.MAX_FILE_SIZE / 1024}KB)`, { path, size, limit: this.MAX_FILE_SIZE } ); } else if (size > this.MAX_FILE_SIZE * 0.8) { this.addWarning( 'STRUCT_W002', `File size (${(size / 1024).toFixed(2)}KB) is approaching the limit`, { path, size, limit: this.MAX_FILE_SIZE } ); } this.addInfo('STRUCT_I001', `File size: ${(size / 1024).toFixed(2)}KB`, { path, size }); } /** * Validate UTF-8 encoding */ validateEncoding(content, path) { try { // Try to detect non-UTF-8 characters const buffer = Buffer.from(content, 'utf8'); const decoded = buffer.toString('utf8'); if (decoded !== content) { this.addError( 'STRUCT_E004', 'File contains invalid UTF-8 encoding', { path } ); } // Check for null bytes (potential binary content) if (content.includes('\0')) { this.addError( 'STRUCT_E005', 'File contains null bytes (possible binary content)', { path } ); } } catch (error) { this.addError( 'STRUCT_E004', 'Failed to validate encoding', { path, error: error.message } ); } } /** * Validate and parse YAML frontmatter * @returns {object|null} Parsed frontmatter or null if invalid */ validateFrontmatter(content, path) { const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); if (!frontmatterMatch) { this.addError( 'STRUCT_E001', 'Missing YAML frontmatter (must start with --- and end with ---)', { path } ); return null; } try { const frontmatter = yaml.load(frontmatterMatch[1]); if (!frontmatter || typeof frontmatter !== 'object') { this.addError( 'STRUCT_E002', 'Frontmatter is empty or not a valid object', { path } ); return null; } this.addInfo('STRUCT_I002', 'Valid YAML frontmatter found', { path }); return frontmatter; } catch (error) { this.addError( 'STRUCT_E002', `Invalid YAML syntax in frontmatter: ${error.message}`, { path, error: error.message } ); return null; } } /** * Validate required fields */ validateRequiredFields(frontmatter, type, path) { const requiredFields = this.REQUIRED_FIELDS[type] || ['name', 'description']; for (const field of requiredFields) { if (!frontmatter[field]) { this.addError( 'STRUCT_E006', `Missing required field: ${field}`, { path, field, type } ); } } } /** * Validate description field */ validateDescription(frontmatter, path) { const description = frontmatter.description; if (!description) return; // Already caught by required fields if (typeof description !== 'string') { this.addError( 'STRUCT_E007', 'Description must be a string', { path, type: typeof description } ); return; } const length = description.trim().length; if (length < this.MIN_DESCRIPTION_LENGTH) { this.addWarning( 'STRUCT_W003', `Description is too short (${length} chars, minimum ${this.MIN_DESCRIPTION_LENGTH})`, { path, length, min: this.MIN_DESCRIPTION_LENGTH } ); } if (length > this.MAX_DESCRIPTION_LENGTH) { this.addWarning( 'STRUCT_W004', `Description is too long (${length} chars, maximum ${this.MAX_DESCRIPTION_LENGTH})`, { path, length, max: this.MAX_DESCRIPTION_LENGTH } ); } } /** * Validate tools field for agents */ validateTools(frontmatter, path) { const tools = frontmatter.tools; if (!tools) return; // Already caught by required fields // Tools can be a string (comma-separated) or array if (typeof tools === 'string') { const toolsList = tools.split(',').map(t => t.trim()).filter(t => t); if (toolsList.length === 0) { this.addWarning('STRUCT_W005', 'Tools field is empty', { path }); } // Validate known tool names const validTools = ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep', 'WebSearch', 'WebFetch', '*']; const invalidTools = toolsList.filter(t => !validTools.includes(t) && t !== '*'); if (invalidTools.length > 0) { this.addWarning( 'STRUCT_W006', `Unknown tools specified: ${invalidTools.join(', ')}`, { path, invalidTools } ); } } else if (Array.isArray(tools)) { if (tools.length === 0) { this.addWarning('STRUCT_W005', 'Tools array is empty', { path }); } } else { this.addError( 'STRUCT_E008', 'Tools field must be a string or array', { path, type: typeof tools } ); } } /** * Validate model field for agents */ validateModel(frontmatter, path) { const model = frontmatter.model; if (!model) { this.addWarning('STRUCT_W007', 'No model specified (recommended)', { path }); return; } const validModels = ['sonnet', 'opus', 'haiku', 'claude-3-5-sonnet', 'claude-3-opus', 'claude-3-haiku']; if (!validModels.includes(model)) { this.addWarning( 'STRUCT_W008', `Unknown model: ${model}. Valid models: ${validModels.join(', ')}`, { path, model } ); } } /** * Check for recommended fields */ checkRecommendedFields(frontmatter, type, path) { const recommendedFields = this.RECOMMENDED_FIELDS[type] || []; const missingFields = recommendedFields.filter(field => !frontmatter[field]); if (missingFields.length > 0) { this.addInfo( 'STRUCT_I003', `Missing recommended fields: ${missingFields.join(', ')}`, { path, missingFields } ); } } /** * Validate content structure */ validateContentStructure(content, path) { // Remove frontmatter for content analysis const contentWithoutFrontmatter = content.replace(/^---\n[\s\S]*?\n---\n/, ''); if (contentWithoutFrontmatter.trim().length < 50) { this.addWarning( 'STRUCT_W009', 'Component content is very short (less than 50 characters)', { path, length: contentWithoutFrontmatter.trim().length } ); } // Check for basic markdown structure const hasHeaders = /^#{1,6}\s+.+$/m.test(contentWithoutFrontmatter); if (!hasHeaders) { this.addWarning( 'STRUCT_W010', 'No markdown headers found in content (recommended for organization)', { path } ); } } /** * Validate section count (prevent context overflow) */ validateSectionCount(content, path) { const sections = content.match(/^#{1,6}\s+.+$/gm) || []; const count = sections.length; if (count > this.MAX_SECTION_COUNT) { this.addWarning( 'STRUCT_W011', `Too many sections (${count}), may cause context overflow. Maximum recommended: ${this.MAX_SECTION_COUNT}`, { path, count, max: this.MAX_SECTION_COUNT } ); } this.addInfo('STRUCT_I004', `Section count: ${count}`, { path, count }); } } module.exports = StructuralValidator;