diff --git a/apply-config.js b/apply-config.js index 62bea4c..1e66ace 100755 --- a/apply-config.js +++ b/apply-config.js @@ -202,9 +202,13 @@ async function applyConfig(configPath = "awesome-copilot.config.yml") { * Ensure directory exists, create if it doesn't */ function ensureDirectoryExists(dirPath) { - if (!fs.existsSync(dirPath)) { - fs.mkdirSync(dirPath, { recursive: true }); - console.log(`šŸ“ Created directory: ${dirPath}`); + try { + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + console.log(`šŸ“ Created directory: ${dirPath}`); + } + } catch (error) { + throw new Error(`Failed to create directory ${dirPath}: ${error.message}`); } } @@ -212,20 +216,34 @@ function ensureDirectoryExists(dirPath) { * Copy file from source to destination with idempotency check */ function copyFile(sourcePath, destPath) { - // Check if destination exists and has same content (idempotency) - if (fs.existsSync(destPath)) { - const sourceContent = fs.readFileSync(sourcePath, 'utf8'); - const destContent = fs.readFileSync(destPath, 'utf8'); - - if (sourceContent === destContent) { - console.log(`āœ“ Already exists and up-to-date: ${path.basename(sourcePath)}`); - return false; // No copy needed + try { + // Validate source file exists + if (!fs.existsSync(sourcePath)) { + throw new Error(`Source file does not exist: ${sourcePath}`); } + + // Check if destination exists and has same content (idempotency) + if (fs.existsSync(destPath)) { + const sourceContent = fs.readFileSync(sourcePath, 'utf8'); + const destContent = fs.readFileSync(destPath, 'utf8'); + + if (sourceContent === destContent) { + console.log(`āœ“ Already exists and up-to-date: ${path.basename(sourcePath)}`); + return false; // No copy needed + } + } + + // Ensure destination directory exists + const destDir = path.dirname(destPath); + ensureDirectoryExists(destDir); + + fs.copyFileSync(sourcePath, destPath); + console.log(`āœ“ Copied: ${path.basename(sourcePath)}`); + return true; // File was copied + } catch (error) { + console.error(`āŒ Failed to copy ${path.basename(sourcePath)}: ${error.message}`); + return false; // Copy failed } - - fs.copyFileSync(sourcePath, destPath); - console.log(`āœ“ Copied: ${path.basename(sourcePath)}`); - return true; // File was copied } /** @@ -253,19 +271,27 @@ function cleanupDisabledFiles(outputDir, effectivelyEnabledSets, rootDir) { const sectionDir = path.join(outputDir, section.name); if (!fs.existsSync(sectionDir)) continue; - const existingFiles = fs.readdirSync(sectionDir); - for (const fileName of existingFiles) { - if (!fileName.endsWith(section.ext)) continue; - - const itemName = fileName.replace(section.ext, ''); - - // Check if this item is still enabled - if (!effectivelyEnabledSets[section.name].has(itemName)) { - const filePath = path.join(sectionDir, fileName); - fs.unlinkSync(filePath); - removedCounts[section.name]++; - console.log(`šŸ—‘ļø Removed: ${section.name}/${fileName}`); + try { + const existingFiles = fs.readdirSync(sectionDir); + for (const fileName of existingFiles) { + if (!fileName.endsWith(section.ext)) continue; + + const itemName = fileName.replace(section.ext, ''); + + // Check if this item is still enabled + if (!effectivelyEnabledSets[section.name].has(itemName)) { + const filePath = path.join(sectionDir, fileName); + try { + fs.unlinkSync(filePath); + removedCounts[section.name]++; + console.log(`šŸ—‘ļø Removed: ${section.name}/${fileName}`); + } catch (error) { + console.error(`āŒ Failed to remove ${section.name}/${fileName}: ${error.message}`); + } + } } + } catch (error) { + console.error(`āŒ Failed to read directory ${sectionDir}: ${error.message}`); } } diff --git a/awesome-copilot.js b/awesome-copilot.js index 661a64d..61f185e 100755 --- a/awesome-copilot.js +++ b/awesome-copilot.js @@ -353,7 +353,7 @@ function extractToggleOptions(rawArgs) { if (i === args.length - 1) { throw new Error("Missing configuration file after --config flag."); } - configPath = args[i + 1]; + configPath = validateConfigPath(args[i + 1]); args.splice(i, 2); } } @@ -362,7 +362,7 @@ function extractToggleOptions(rawArgs) { if (args.length > 0) { const potentialPath = args[args.length - 1]; if (isConfigFilePath(potentialPath)) { - configPath = potentialPath; + configPath = validateConfigPath(potentialPath); args.pop(); } } @@ -380,7 +380,7 @@ function extractConfigOption(rawArgs) { if (i === args.length - 1) { throw new Error("Missing configuration file after --config flag."); } - configPath = args[i + 1]; + configPath = validateConfigPath(args[i + 1]); args.splice(i, 2); i -= 1; } @@ -389,7 +389,7 @@ function extractConfigOption(rawArgs) { if (args.length > 0) { const potentialPath = args[args.length - 1]; if (isConfigFilePath(potentialPath)) { - configPath = potentialPath; + configPath = validateConfigPath(potentialPath); args.pop(); } } @@ -404,6 +404,24 @@ function isConfigFilePath(value) { return value.endsWith(".yml") || value.endsWith(".yaml") || value.includes("/") || value.includes("\\"); } +function validateConfigPath(configPath) { + if (typeof configPath !== "string") { + throw new Error("Configuration path must be a string"); + } + + // Basic path traversal protection + if (configPath.includes("..") || configPath.includes("~")) { + throw new Error("Configuration path cannot contain path traversal sequences (..) or home directory references (~)"); + } + + // Ensure it's a reasonable file extension + if (!configPath.endsWith(".yml") && !configPath.endsWith(".yaml")) { + throw new Error("Configuration file must have a .yml or .yaml extension"); + } + + return configPath; +} + function validateSectionType(input) { const normalized = String(input || "").toLowerCase(); if (!SECTION_METADATA[normalized]) { @@ -482,20 +500,31 @@ function handleResetCommand(rawArgs) { console.log(`šŸ”„ Resetting ${outputDir} directory...`); let removedCount = 0; + let failedCount = 0; // Remove all files from subdirectories but keep the directory structure const subdirs = ["prompts", "instructions", "chatmodes"]; for (const subdir of subdirs) { const subdirPath = path.join(outputDir, subdir); if (fs.existsSync(subdirPath)) { - const files = fs.readdirSync(subdirPath); - for (const file of files) { - const filePath = path.join(subdirPath, file); - if (fs.statSync(filePath).isFile()) { - fs.unlinkSync(filePath); - removedCount++; - console.log(`šŸ—‘ļø Removed: ${subdir}/${file}`); + try { + const files = fs.readdirSync(subdirPath); + for (const file of files) { + const filePath = path.join(subdirPath, file); + try { + if (fs.statSync(filePath).isFile()) { + fs.unlinkSync(filePath); + removedCount++; + console.log(`šŸ—‘ļø Removed: ${subdir}/${file}`); + } + } catch (error) { + console.error(`āŒ Failed to remove ${subdir}/${file}: ${error.message}`); + failedCount++; + } } + } catch (error) { + console.error(`āŒ Failed to read directory ${subdirPath}: ${error.message}`); + failedCount++; } } } @@ -503,12 +532,20 @@ function handleResetCommand(rawArgs) { // Remove README.md if it exists const readmePath = path.join(outputDir, "README.md"); if (fs.existsSync(readmePath)) { - fs.unlinkSync(readmePath); - removedCount++; - console.log(`šŸ—‘ļø Removed: README.md`); + try { + fs.unlinkSync(readmePath); + removedCount++; + console.log(`šŸ—‘ļø Removed: README.md`); + } catch (error) { + console.error(`āŒ Failed to remove README.md: ${error.message}`); + failedCount++; + } } console.log(`\nāœ… Reset complete! Removed ${removedCount} files.`); + if (failedCount > 0) { + console.log(`āš ļø ${failedCount} files could not be removed.`); + } console.log(`šŸ“ Directory structure preserved: ${outputDir}/`); console.log("Run 'awesome-copilot apply' to repopulate with current configuration."); } diff --git a/test-new-features.js b/test-new-features.js index 8c2a9c7..badbe82 100755 --- a/test-new-features.js +++ b/test-new-features.js @@ -48,7 +48,7 @@ async function runCommand(command) { function setTestOutputDir(configFile) { if (fs.existsSync(configFile)) { let content = fs.readFileSync(configFile, 'utf8'); - content = content.replace(/output_directory: "\.awesome-copilot"/, `output_directory: "${TEST_OUTPUT_DIR}"`); + content = content.replace(/output_directory: "\.github"/, `output_directory: "${TEST_OUTPUT_DIR}"`); fs.writeFileSync(configFile, content); } } @@ -72,7 +72,7 @@ async function runTests() { } // Test 1: Reset command - await test("Reset command clears .awesome-copilot directory", async () => { + await test("Reset command clears output directory", async () => { await runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`); setTestOutputDir(TEST_CONFIG);