Improve script robustness and idempotency with better error handling
Co-authored-by: AstroSteveo <34114851+AstroSteveo@users.noreply.github.com>
This commit is contained in:
parent
91c4a88c6f
commit
9f88bd46c8
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
fs.copyFileSync(sourcePath, destPath);
|
||||
console.log(`✓ Copied: ${path.basename(sourcePath)}`);
|
||||
return true; // File was copied
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -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;
|
||||
try {
|
||||
const existingFiles = fs.readdirSync(sectionDir);
|
||||
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
|
||||
if (!effectivelyEnabledSets[section.name].has(itemName)) {
|
||||
const filePath = path.join(sectionDir, fileName);
|
||||
fs.unlinkSync(filePath);
|
||||
removedCounts[section.name]++;
|
||||
console.log(`🗑️ Removed: ${section.name}/${fileName}`);
|
||||
// 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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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.");
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user