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,9 +202,13 @@ 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) {
if (!fs.existsSync(dirPath)) { try {
fs.mkdirSync(dirPath, { recursive: true }); if (!fs.existsSync(dirPath)) {
console.log(`📁 Created directory: ${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 * Copy file from source to destination with idempotency check
*/ */
function copyFile(sourcePath, destPath) { function copyFile(sourcePath, destPath) {
// Check if destination exists and has same content (idempotency) try {
if (fs.existsSync(destPath)) { // Validate source file exists
const sourceContent = fs.readFileSync(sourcePath, 'utf8'); if (!fs.existsSync(sourcePath)) {
const destContent = fs.readFileSync(destPath, 'utf8'); throw new Error(`Source file does not exist: ${sourcePath}`);
if (sourceContent === destContent) {
console.log(`✓ Already exists and up-to-date: ${path.basename(sourcePath)}`);
return false; // No copy needed
} }
}
fs.copyFileSync(sourcePath, destPath); // Check if destination exists and has same content (idempotency)
console.log(`✓ Copied: ${path.basename(sourcePath)}`); if (fs.existsSync(destPath)) {
return true; // File was copied 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
}
} }
/** /**
@ -253,19 +271,27 @@ 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;
const existingFiles = fs.readdirSync(sectionDir); try {
for (const fileName of existingFiles) { const existingFiles = fs.readdirSync(sectionDir);
if (!fileName.endsWith(section.ext)) continue; for (const fileName of existingFiles) {
if (!fileName.endsWith(section.ext)) continue;
const itemName = fileName.replace(section.ext, ''); const itemName = fileName.replace(section.ext, '');
// 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);
fs.unlinkSync(filePath); try {
removedCounts[section.name]++; fs.unlinkSync(filePath);
console.log(`🗑️ Removed: ${section.name}/${fileName}`); 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}`);
} }
} }

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)) {
const files = fs.readdirSync(subdirPath); try {
for (const file of files) { const files = fs.readdirSync(subdirPath);
const filePath = path.join(subdirPath, file); for (const file of files) {
if (fs.statSync(filePath).isFile()) { const filePath = path.join(subdirPath, file);
fs.unlinkSync(filePath); try {
removedCount++; if (fs.statSync(filePath).isFile()) {
console.log(`🗑️ Removed: ${subdir}/${file}`); 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 // 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)) {
fs.unlinkSync(readmePath); try {
removedCount++; fs.unlinkSync(readmePath);
console.log(`🗑️ Removed: README.md`); 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.`); 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);