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
*/
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}`);
}
}

View File

@ -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.");
}

View File

@ -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);