Improve script robustness and idempotency with better error handling

Co-authored-by: AstroSteveo <34114851+AstroSteveo@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-09-23 17:56:49 +00:00
parent 91c4a88c6f
commit 9f88bd46c8
3 changed files with 106 additions and 43 deletions

View File

@ -202,16 +202,26 @@ async function applyConfig(configPath = "awesome-copilot.config.yml") {
* Ensure directory exists, create if it doesn't * Ensure directory exists, create if it doesn't
*/ */
function ensureDirectoryExists(dirPath) { function ensureDirectoryExists(dirPath) {
try {
if (!fs.existsSync(dirPath)) { if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true }); fs.mkdirSync(dirPath, { recursive: true });
console.log(`📁 Created directory: ${dirPath}`); console.log(`📁 Created directory: ${dirPath}`);
} }
} catch (error) {
throw new Error(`Failed to create directory ${dirPath}: ${error.message}`);
}
} }
/** /**
* Copy file from source to destination with idempotency check * Copy file from source to destination with idempotency check
*/ */
function copyFile(sourcePath, destPath) { function copyFile(sourcePath, destPath) {
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) // Check if destination exists and has same content (idempotency)
if (fs.existsSync(destPath)) { if (fs.existsSync(destPath)) {
const sourceContent = fs.readFileSync(sourcePath, 'utf8'); const sourceContent = fs.readFileSync(sourcePath, 'utf8');
@ -223,9 +233,17 @@ function copyFile(sourcePath, destPath) {
} }
} }
// Ensure destination directory exists
const destDir = path.dirname(destPath);
ensureDirectoryExists(destDir);
fs.copyFileSync(sourcePath, destPath); fs.copyFileSync(sourcePath, destPath);
console.log(`✓ Copied: ${path.basename(sourcePath)}`); console.log(`✓ Copied: ${path.basename(sourcePath)}`);
return true; // File was copied return true; // File was copied
} catch (error) {
console.error(`❌ Failed to copy ${path.basename(sourcePath)}: ${error.message}`);
return false; // Copy failed
}
} }
/** /**
@ -253,6 +271,7 @@ function cleanupDisabledFiles(outputDir, effectivelyEnabledSets, rootDir) {
const sectionDir = path.join(outputDir, section.name); const sectionDir = path.join(outputDir, section.name);
if (!fs.existsSync(sectionDir)) continue; if (!fs.existsSync(sectionDir)) continue;
try {
const existingFiles = fs.readdirSync(sectionDir); const existingFiles = fs.readdirSync(sectionDir);
for (const fileName of existingFiles) { for (const fileName of existingFiles) {
if (!fileName.endsWith(section.ext)) continue; if (!fileName.endsWith(section.ext)) continue;
@ -262,12 +281,19 @@ function cleanupDisabledFiles(outputDir, effectivelyEnabledSets, rootDir) {
// Check if this item is still enabled // Check if this item is still enabled
if (!effectivelyEnabledSets[section.name].has(itemName)) { if (!effectivelyEnabledSets[section.name].has(itemName)) {
const filePath = path.join(sectionDir, fileName); const filePath = path.join(sectionDir, fileName);
try {
fs.unlinkSync(filePath); fs.unlinkSync(filePath);
removedCounts[section.name]++; removedCounts[section.name]++;
console.log(`🗑️ Removed: ${section.name}/${fileName}`); 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}`);
}
}
return removedCounts; return removedCounts;
} }

View File

@ -353,7 +353,7 @@ function extractToggleOptions(rawArgs) {
if (i === args.length - 1) { if (i === args.length - 1) {
throw new Error("Missing configuration file after --config flag."); throw new Error("Missing configuration file after --config flag.");
} }
configPath = args[i + 1]; configPath = validateConfigPath(args[i + 1]);
args.splice(i, 2); args.splice(i, 2);
} }
} }
@ -362,7 +362,7 @@ function extractToggleOptions(rawArgs) {
if (args.length > 0) { if (args.length > 0) {
const potentialPath = args[args.length - 1]; const potentialPath = args[args.length - 1];
if (isConfigFilePath(potentialPath)) { if (isConfigFilePath(potentialPath)) {
configPath = potentialPath; configPath = validateConfigPath(potentialPath);
args.pop(); args.pop();
} }
} }
@ -380,7 +380,7 @@ function extractConfigOption(rawArgs) {
if (i === args.length - 1) { if (i === args.length - 1) {
throw new Error("Missing configuration file after --config flag."); throw new Error("Missing configuration file after --config flag.");
} }
configPath = args[i + 1]; configPath = validateConfigPath(args[i + 1]);
args.splice(i, 2); args.splice(i, 2);
i -= 1; i -= 1;
} }
@ -389,7 +389,7 @@ function extractConfigOption(rawArgs) {
if (args.length > 0) { if (args.length > 0) {
const potentialPath = args[args.length - 1]; const potentialPath = args[args.length - 1];
if (isConfigFilePath(potentialPath)) { if (isConfigFilePath(potentialPath)) {
configPath = potentialPath; configPath = validateConfigPath(potentialPath);
args.pop(); args.pop();
} }
} }
@ -404,6 +404,24 @@ function isConfigFilePath(value) {
return value.endsWith(".yml") || value.endsWith(".yaml") || value.includes("/") || value.includes("\\"); 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) { function validateSectionType(input) {
const normalized = String(input || "").toLowerCase(); const normalized = String(input || "").toLowerCase();
if (!SECTION_METADATA[normalized]) { if (!SECTION_METADATA[normalized]) {
@ -482,20 +500,31 @@ function handleResetCommand(rawArgs) {
console.log(`🔄 Resetting ${outputDir} directory...`); console.log(`🔄 Resetting ${outputDir} directory...`);
let removedCount = 0; let removedCount = 0;
let failedCount = 0;
// Remove all files from subdirectories but keep the directory structure // Remove all files from subdirectories but keep the directory structure
const subdirs = ["prompts", "instructions", "chatmodes"]; const subdirs = ["prompts", "instructions", "chatmodes"];
for (const subdir of subdirs) { for (const subdir of subdirs) {
const subdirPath = path.join(outputDir, subdir); const subdirPath = path.join(outputDir, subdir);
if (fs.existsSync(subdirPath)) { if (fs.existsSync(subdirPath)) {
try {
const files = fs.readdirSync(subdirPath); const files = fs.readdirSync(subdirPath);
for (const file of files) { for (const file of files) {
const filePath = path.join(subdirPath, file); const filePath = path.join(subdirPath, file);
try {
if (fs.statSync(filePath).isFile()) { if (fs.statSync(filePath).isFile()) {
fs.unlinkSync(filePath); fs.unlinkSync(filePath);
removedCount++; removedCount++;
console.log(`🗑️ Removed: ${subdir}/${file}`); 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 // Remove README.md if it exists
const readmePath = path.join(outputDir, "README.md"); const readmePath = path.join(outputDir, "README.md");
if (fs.existsSync(readmePath)) { if (fs.existsSync(readmePath)) {
try {
fs.unlinkSync(readmePath); fs.unlinkSync(readmePath);
removedCount++; removedCount++;
console.log(`🗑️ Removed: README.md`); 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.`); 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(`📁 Directory structure preserved: ${outputDir}/`);
console.log("Run 'awesome-copilot apply' to repopulate with current configuration."); console.log("Run 'awesome-copilot apply' to repopulate with current configuration.");
} }

View File

@ -48,7 +48,7 @@ async function runCommand(command) {
function setTestOutputDir(configFile) { function setTestOutputDir(configFile) {
if (fs.existsSync(configFile)) { if (fs.existsSync(configFile)) {
let content = fs.readFileSync(configFile, 'utf8'); 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); fs.writeFileSync(configFile, content);
} }
} }
@ -72,7 +72,7 @@ async function runTests() {
} }
// Test 1: Reset command // 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}`); await runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`);
setTestOutputDir(TEST_CONFIG); setTestOutputDir(TEST_CONFIG);