UNPKG

ez-vibecoder

Version:

WORD-IN-PROGRESS: Vibe Coding Tools installer for VS Code AI-assisted development

452 lines (383 loc) 14.3 kB
#!/usr/bin/env node /** * YAML Sanitizer - Fixes and validates YAML-based system prompt files * * This utility handles detecting and repairing duplicate capabilities sections * in system prompt files for multiple AI tools, and performs YAML validation. */ const fs = require("fs"); const path = require("path"); const yaml = require("js-yaml"); const crypto = require("crypto"); // Terminal color constants for better readability const GREEN = "\x1b[32m"; const BLUE = "\x1b[34m"; const YELLOW = "\x1b[33m"; const RED = "\x1b[31m"; const RESET = "\x1b[0m"; /** * Supported AI tool directories - easily extend by adding new entries */ const SUPPORTED_AI_TOOL_DIRS = [ ".roo", //".cline", //".windsurf", // ".cursor" ]; /** * System prompt file types shared across AI tools */ const SYSTEM_PROMPT_FILES = [ "system-prompt-architect", "system-prompt-ask", "system-prompt-code", "system-prompt-debug", "system-prompt-test" ]; /** * Generates all permitted file paths by combining tool directories with file types * @returns {string[]} Array of allowed file paths */ function getAllowedFilePaths() { return SUPPORTED_AI_TOOL_DIRS.flatMap(dir => SYSTEM_PROMPT_FILES.map(file => path.join(dir, file)) ); } // The complete list of allowed file paths const ALLOWED_SYSTEM_PROMPT_FILES = getAllowedFilePaths(); /** * Validates and sanitizes paths to prevent path traversal attacks * @param {string} basePath - Base directory path * @param {string} filePath - Relative file path to validate * @returns {string|null} Absolute path if valid, null if invalid */ function validatePath(basePath, filePath) { if (!basePath || !filePath) { console.error(`${RED}Security Error${RESET}: Invalid input paths`); return null; } // Normalize and resolve the complete path const normalizedBase = path.normalize(basePath); const targetPath = path.resolve(normalizedBase, filePath); // Check if the target path is within the base path (prevent directory traversal) if (!targetPath.startsWith(normalizedBase)) { console.error(`${RED}Security Error${RESET}: Path traversal attempt detected for ${filePath}`); return null; } // Check if the file is in the allowed list (whitelist approach) if (!ALLOWED_SYSTEM_PROMPT_FILES.includes(filePath)) { console.error(`${RED}Security Error${RESET}: Attempted to access non-allowed file: ${filePath}`); return null; } return targetPath; } /** * Creates a timestamped backup of a file before modification * @param {string} filePath - Path to the file * @returns {boolean} True if backup was created successfully */ function createBackup(filePath) { try { if (!filePath || !fs.existsSync(filePath)) { return false; } const backupPath = `${filePath}.bak.${Date.now()}`; fs.copyFileSync(filePath, backupPath); console.log(` ${BLUE}Backup${RESET}: Created backup at ${backupPath}`); return true; } catch (error) { console.error(` ${RED}Error${RESET}: Failed to create backup: ${error.message}`); return false; } } /** * Fixes duplicate capabilities sections in system prompt files * @param {string} projectRoot - Path to the project root * @returns {boolean} True if fixes were successful */ function fixDuplicateCapabilities(projectRoot) { console.log("\nChecking system prompt files for duplicate capabilities sections..."); // Validate the project root if (!projectRoot || !fs.existsSync(projectRoot)) { console.error(`${RED}Error${RESET}: Project root path does not exist: ${projectRoot}`); return false; } try { const stats = fs.statSync(projectRoot); if (!stats.isDirectory()) { console.error(`${RED}Error${RESET}: Project root path is not a directory: ${projectRoot}`); return false; } } catch (error) { console.error(`${RED}Error${RESET}: Failed to check project root: ${error.message}`); return false; } let fixesMade = false; let allSuccessful = true; // Process each allowed file for (const file of ALLOWED_SYSTEM_PROMPT_FILES) { const filePath = validatePath(projectRoot, file); if (!filePath) { allSuccessful = false; continue; } if (!fs.existsSync(filePath)) { console.log(` ${BLUE}${file}${RESET}: File not found, skipping`); continue; } try { // Read file content safely const content = fs.readFileSync(filePath, "utf8"); // Detect capabilities sections with improved regex that handles whitespace const matches = content.match(/^\s*capabilities:/gm); if (!matches || matches.length <= 1) { console.log(` ${BLUE}${file}${RESET}: No duplicate capabilities found`); continue; } console.log(` ${BLUE}${file}${RESET}: Found ${matches.length} capabilities sections`); // Create a backup before modifying if (!createBackup(filePath)) { allSuccessful = false; continue; } // Split content into YAML sections const sections = splitIntoYamlSections(content); // Separate capabilities sections from other content const capabilitiesSections = []; const otherSections = []; sections.forEach((section) => { if (section.match(/^\s*capabilities:/mi)) { capabilitiesSections.push(section); } else { otherSections.push(section); } }); // Handle multiple capabilities sections if (capabilitiesSections.length > 1) { let validCapabilitiesSection = null; let validIndex = -1; // Find the first valid capabilities section for (let i = 0; i < capabilitiesSections.length; i++) { const validationResult = validateYamlSection(capabilitiesSections[i]); if (validationResult.valid) { validCapabilitiesSection = capabilitiesSections[i]; validIndex = i; break; } else { console.log(` ${YELLOW}Warning${RESET}: Section ${i+1} validation error: ${validationResult.error}`); } } if (validCapabilitiesSection) { // Find the mode section to insert capabilities after it const modeIndex = otherSections.findIndex(s => s.match(/^\s*mode:/mi)); if (modeIndex !== -1) { // Construct new content with just one capabilities section const newContent = [ ...otherSections.slice(0, modeIndex + 1), validCapabilitiesSection, ...otherSections.slice(modeIndex + 1) ].join(""); // Verify changes with checksums const originalChecksum = crypto.createHash('sha256').update(content).digest('hex'); // Write fixed content fs.writeFileSync(filePath, newContent, "utf8"); // Verify successful write const newFileContent = fs.readFileSync(filePath, "utf8"); const newChecksum = crypto.createHash('sha256').update(newFileContent).digest('hex'); if (originalChecksum !== newChecksum) { console.log(` ${GREEN}Fixed${RESET}: Used capabilities section ${validIndex + 1} in ${BLUE}${file}${RESET}`); fixesMade = true; } else { console.error(` ${RED}Error${RESET}: File content was not updated for ${BLUE}${file}${RESET}`); allSuccessful = false; } } else { console.error(` ${RED}Error${RESET}: Could not find 'mode:' section in ${BLUE}${file}${RESET}`); allSuccessful = false; } } else { console.error(` ${RED}Error${RESET}: No valid capabilities section found in ${BLUE}${file}${RESET}`); allSuccessful = false; } } } catch (error) { console.error(` ${RED}Error${RESET}: Failed to process file ${BLUE}${file}${RESET}: ${error.message}`); allSuccessful = false; } } // Report overall results if (fixesMade && allSuccessful) { console.log(`${GREEN}Fixed duplicate capabilities sections in system prompt files${RESET}`); } else if (fixesMade) { console.log(`${YELLOW}Warning${RESET}: Fixed some duplicate capabilities sections, but errors occurred`); } else { console.log("No duplicate capabilities sections found or fixed"); } return allSuccessful; } /** * Validates system prompt files using js-yaml * @param {string} projectRoot - Path to the project root * @returns {boolean} True if all files are valid */ function validateFilesWithYaml(projectRoot) { console.log("\nValidating system prompt files with js-yaml..."); // Validate the project root if (!projectRoot || !fs.existsSync(projectRoot)) { console.error(`${RED}Error${RESET}: Project root path does not exist: ${projectRoot}`); return false; } try { if (!fs.statSync(projectRoot).isDirectory()) { console.error(`${RED}Error${RESET}: Project root path is not a directory: ${projectRoot}`); return false; } } catch (error) { console.error(`${RED}Error${RESET}: Failed to check project root: ${error.message}`); return false; } let allValid = true; // Process each allowed file for (const file of ALLOWED_SYSTEM_PROMPT_FILES) { const filePath = validatePath(projectRoot, file); if (!filePath) { allValid = false; continue; } if (!fs.existsSync(filePath)) { console.log(` ${BLUE}${file}${RESET}: File not found, skipping`); continue; } try { // Read file content const content = fs.readFileSync(filePath, "utf8"); // Check for duplicate capabilities sections const matches = content.match(/^\s*capabilities:/gm); if (matches && matches.length > 1) { console.log(` ${YELLOW}Warning${RESET}: ${BLUE}${file}${RESET} still has ${matches.length} capabilities sections`); allValid = false; continue; } // Split into sections const sections = splitIntoYamlSections(content); // Validate each section let invalidSections = 0; for (const section of sections) { // Only validate sections that look like YAML if (section.includes(":")) { const validationResult = validateYamlSection(section); if (!validationResult.valid) { invalidSections++; console.log(` ${YELLOW}Warning${RESET}: Invalid YAML in section: ${section.substring(0, 40).replace(/\n/g, "\\n")}...`); console.log(` Error: ${validationResult.error}`); } } } // Report validation results for this file if (invalidSections > 0) { console.log(` ${YELLOW}Warning${RESET}: ${BLUE}${file}${RESET} has ${invalidSections} sections with YAML errors`); allValid = false; } else { console.log(` ${GREEN}Valid${RESET}: ${BLUE}${file}${RESET} - All YAML sections validated successfully`); } } catch (error) { console.error(` ${RED}Error${RESET}: Failed to process file ${BLUE}${file}${RESET}: ${error.message}`); allValid = false; } } // Report overall validation results if (allValid) { console.log(`${GREEN}All system prompt files validated successfully${RESET}`); } else { console.log(`${YELLOW}Warning${RESET}: Some files have issues that need attention`); } return allValid; } /** * Splits content into YAML sections using structure awareness * @param {string} content - File content * @returns {string[]} Array of sections */ function splitIntoYamlSections(content) { if (!content) return []; const sections = []; let currentSection = ""; let inYamlBlock = false; const lines = content.split("\n"); for (let i = 0; i < lines.length; i++) { const line = lines[i]; const trimmedLine = line.trim(); // Skip empty lines between sections if (!currentSection && trimmedLine === "") { continue; } // Detect new section (unindented line) if (trimmedLine !== "" && !line.match(/^\s+/) && !inYamlBlock) { // Save previous section before starting a new one if (currentSection) { sections.push(currentSection); } currentSection = line + "\n"; // Mark start of YAML block if this looks like a YAML property if (line.includes(":")) { inYamlBlock = true; } } else if (inYamlBlock && trimmedLine !== "" && !line.match(/^\s+/)) { // Found a new unindented line while in a YAML block - it's a new section inYamlBlock = false; sections.push(currentSection); currentSection = line + "\n"; // Check if new section starts a YAML block if (line.includes(":")) { inYamlBlock = true; } } else { // Continue current section currentSection += line + "\n"; } } // Add the last section if (currentSection) { sections.push(currentSection); } return sections; } /** * Validates a YAML section with safe parsing * @param {string} section - Section content to validate * @returns {Object} Result with valid flag and error message */ function validateYamlSection(section) { if (!section || !section.includes(":")) { return { valid: true, error: null }; } try { // Handle different YAML formats safely if (section.trim().startsWith("---") || !section.trim().match(/^\w+:/)) { // Looks like a complete YAML document yaml.load(section); } else { // For YAML fragments, wrap with a temporary root element const yamlStr = `temp_root:\n${section.split("\n").map(line => ` ${line}`).join("\n")}`; yaml.load(yamlStr); } return { valid: true, error: null }; } catch (yamlError) { return { valid: false, error: yamlError.message }; } } // Export public API module.exports = { // Main functions fixDuplicateCapabilities, validateFilesWithYaml, // Helper functions (exported for testing) validatePath, createBackup, splitIntoYamlSections, validateYamlSection, // Configuration constants SUPPORTED_AI_TOOL_DIRS, SYSTEM_PROMPT_FILES, ALLOWED_SYSTEM_PROMPT_FILES };