diff --git a/apply-config.js b/apply-config.js index d36a1e1..8a1afd5 100755 --- a/apply-config.js +++ b/apply-config.js @@ -165,8 +165,21 @@ async function applyConfig(configPath = "awesome-copilot.config.yml") { ensureDirectoryExists(path.join(outputDir, "instructions")); ensureDirectoryExists(path.join(outputDir, "chatmodes")); + // Check if this is a subsequent run by looking for existing state + const stateFilePath = path.join(outputDir, ".awesome-copilot-state.json"); + let previousState = {}; + if (fs.existsSync(stateFilePath)) { + try { + previousState = JSON.parse(fs.readFileSync(stateFilePath, 'utf8')); + } catch (error) { + // If state file is corrupted, treat as first run + previousState = {}; + } + } + let copiedCount = 0; let removedCount = 0; + let skippedCount = 0; const summary = { prompts: 0, instructions: 0, @@ -204,9 +217,13 @@ async function applyConfig(configPath = "awesome-copilot.config.yml") { const destPath = path.join(outputDir, destDir, `${itemName}${fileExtension}`); if (enabled && fs.existsSync(sourcePath)) { - copyFile(sourcePath, destPath); - copiedCount++; - summary[sectionName]++; + const copyResult = copyFileWithTracking(sourcePath, destPath); + if (copyResult.copied) { + copiedCount++; + summary[sectionName]++; + } else if (copyResult.skipped) { + skippedCount++; + } enabledInSection.add(itemName); } else if (!enabled && fs.existsSync(destPath)) { // Remove file if it's disabled @@ -235,6 +252,29 @@ async function applyConfig(configPath = "awesome-copilot.config.yml") { } } + // Helper function to copy files with tracking + function copyFileWithTracking(sourcePath, destPath) { + // Check if file already exists and is identical + if (fs.existsSync(destPath)) { + try { + const sourceContent = fs.readFileSync(sourcePath); + const destContent = fs.readFileSync(destPath); + + if (sourceContent.equals(destContent)) { + // Files are identical, no need to copy + console.log(`โšก Skipped (up to date): ${path.basename(sourcePath)}`); + return { copied: false, skipped: true }; + } + } catch (error) { + // If we can't read files for comparison, just proceed with copy + } + } + + fs.copyFileSync(sourcePath, destPath); + console.log(`โœ“ Copied: ${path.basename(sourcePath)}`); + return { copied: true, skipped: false }; + } + // Helper function to check if an item is in an enabled collection function isItemInEnabledCollection(filename, enabledItems) { for (const itemPath of enabledItems) { @@ -277,9 +317,13 @@ async function applyConfig(configPath = "awesome-copilot.config.yml") { // Only copy if not explicitly disabled in individual settings const isExplicitlyDisabled = config[section] && config[section][itemName] === false; - if (destPath && !fs.existsSync(destPath) && !isExplicitlyDisabled) { - copyFile(sourcePath, destPath); - copiedCount++; + if (destPath && !isExplicitlyDisabled) { + const copyResult = copyFileWithTracking(sourcePath, destPath); + if (copyResult.copied) { + copiedCount++; + } else if (copyResult.skipped) { + skippedCount++; + } } } } @@ -290,6 +334,9 @@ async function applyConfig(configPath = "awesome-copilot.config.yml") { console.log("=".repeat(50)); console.log(`๐Ÿ“‚ Output directory: ${outputDir}`); console.log(`๐Ÿ“ Total files copied: ${copiedCount}`); + if (skippedCount > 0) { + console.log(`โšก Files skipped (up to date): ${skippedCount}`); + } if (removedCount > 0) { console.log(`๐Ÿ—‘๏ธ Total files removed: ${removedCount}`); } @@ -301,6 +348,20 @@ async function applyConfig(configPath = "awesome-copilot.config.yml") { if (config.project?.name) { console.log(`๐Ÿท๏ธ Project: ${config.project.name}`); } + + // Save current state for future idempotency checks + const currentState = { + lastApplied: new Date().toISOString(), + configHash: Buffer.from(JSON.stringify(config)).toString('base64'), + outputDir: outputDir + }; + + try { + fs.writeFileSync(stateFilePath, JSON.stringify(currentState, null, 2)); + } catch (error) { + // State saving failure is not critical + console.log("โš ๏ธ Warning: Could not save state file for future optimization"); + } console.log("\nNext steps:"); console.log("1. Add the files to your version control system"); @@ -319,14 +380,6 @@ function ensureDirectoryExists(dirPath) { } } -/** - * Copy file from source to destination - */ -function copyFile(sourcePath, destPath) { - fs.copyFileSync(sourcePath, destPath); - console.log(`โœ“ Copied: ${path.basename(sourcePath)}`); -} - // CLI usage if (require.main === module) { const configPath = process.argv[2] || "awesome-copilot.config.yml"; diff --git a/test-functionality.js b/test-functionality.js new file mode 100755 index 0000000..ef81de5 --- /dev/null +++ b/test-functionality.js @@ -0,0 +1,165 @@ +#!/usr/bin/env node + +/** + * Simple test script to validate enhanced awesome-copilot functionality + * This script tests the key features implemented in the enhancement + */ + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +const TEST_CONFIG = 'test-functionality.yml'; +const TEST_OUTPUT_DIR = '.test-output'; + +console.log('๐Ÿงช Testing enhanced awesome-copilot functionality...\n'); + +// Cleanup function +function cleanup() { + if (fs.existsSync(TEST_CONFIG)) { + fs.unlinkSync(TEST_CONFIG); + } + if (fs.existsSync(TEST_OUTPUT_DIR)) { + execSync(`rm -rf ${TEST_OUTPUT_DIR}`); + } + if (fs.existsSync('.awesome-copilot')) { + execSync(`rm -rf .awesome-copilot`); + } +} + +function runCommand(cmd, description) { + console.log(`๐Ÿ“‹ ${description}`); + try { + const output = execSync(cmd, { encoding: 'utf8', cwd: __dirname }); + console.log(`โœ… Success: ${description}`); + return output; + } catch (error) { + console.log(`โŒ Failed: ${description}`); + console.log(` Error: ${error.message}`); + throw error; + } +} + +function checkFileExists(filePath, shouldExist = true) { + const exists = fs.existsSync(filePath); + if (shouldExist && exists) { + console.log(`โœ… File exists: ${filePath}`); + return true; + } else if (!shouldExist && !exists) { + console.log(`โœ… File correctly removed: ${filePath}`); + return true; + } else { + console.log(`โŒ File ${exists ? 'exists' : 'missing'}: ${filePath} (expected ${shouldExist ? 'to exist' : 'to be removed'})`); + return false; + } +} + +try { + // Test 1: Initialize a test configuration + console.log('\n๐Ÿ”ง Test 1: Initialize test configuration'); + runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`, 'Initialize test config'); + + // Test 2: Validate configuration file was created + console.log('\n๐Ÿ”ง Test 2: Validate configuration file'); + if (!checkFileExists(TEST_CONFIG)) { + throw new Error('Configuration file was not created'); + } + + // Test 3: Test enhanced list command (should show collections first) + console.log('\n๐Ÿ”ง Test 3: Test enhanced list command'); + const listOutput = runCommand(`node awesome-copilot.js list --config ${TEST_CONFIG}`, 'List all items with enhanced display'); + + if (!listOutput.includes('Collections (')) { + throw new Error('Enhanced list should show Collections section first'); + } + + if (!listOutput.includes('๐Ÿ“ฆ indicates items that are part of collections')) { + throw new Error('Enhanced list should show collection indicators help text'); + } + + // Test 4: Test collection toggle with cascading + console.log('\n๐Ÿ”ง Test 4: Test collection toggle with cascading'); + const toggleOutput = runCommand(`node awesome-copilot.js toggle collections project-planning on --config ${TEST_CONFIG}`, 'Enable collection with cascading'); + + if (!toggleOutput.includes('individual items from collection')) { + throw new Error('Collection toggle should report cascading to individual items'); + } + + if (!toggleOutput.includes('Applying configuration automatically')) { + throw new Error('Collection toggle should auto-apply configuration'); + } + + // Test 5: Test individual item override + console.log('\n๐Ÿ”ง Test 5: Test individual item override'); + runCommand(`node awesome-copilot.js toggle prompts breakdown-epic-arch off --config ${TEST_CONFIG}`, 'Disable individual item in collection'); + + // Check that the file was removed + const promptPath = path.join('.awesome-copilot', 'prompts', 'breakdown-epic-arch.prompt.md'); + if (!checkFileExists(promptPath, false)) { + throw new Error('Individual item override did not remove file'); + } + + // Test 6: Test idempotency (running apply twice should skip files) + console.log('\n๐Ÿ”ง Test 6: Test idempotency'); + const applyOutput1 = runCommand(`node awesome-copilot.js apply ${TEST_CONFIG}`, 'First apply run'); + const applyOutput2 = runCommand(`node awesome-copilot.js apply ${TEST_CONFIG}`, 'Second apply run (should be idempotent)'); + + if (!applyOutput2.includes('Skipped (up to date)')) { + throw new Error('Second apply should skip files that are up to date'); + } + + // Test 7: Test config validation with invalid config + console.log('\n๐Ÿ”ง Test 7: Test config validation'); + const invalidConfig = ` +version: 1.0 +prompts: + test-prompt: invalid_boolean_value +unknown_section: + test: true +`; + + fs.writeFileSync('test-invalid-config.yml', invalidConfig); + + try { + execSync(`node awesome-copilot.js apply test-invalid-config.yml`, { encoding: 'utf8' }); + throw new Error('Invalid config should have been rejected'); + } catch (error) { + if (error.stderr && error.stderr.includes('Configuration validation errors')) { + console.log('โœ… Config validation correctly rejected invalid configuration'); + } else if (error.message && error.message.includes('Configuration validation failed')) { + console.log('โœ… Config validation correctly rejected invalid configuration'); + } else { + throw new Error('Config validation did not provide proper error message'); + } + } finally { + if (fs.existsSync('test-invalid-config.yml')) { + fs.unlinkSync('test-invalid-config.yml'); + } + } + + // Test 8: Check that state file is created for idempotency + console.log('\n๐Ÿ”ง Test 8: Check state file creation'); + const stateFilePath = path.join('.awesome-copilot', '.awesome-copilot-state.json'); + if (!checkFileExists(stateFilePath)) { + throw new Error('State file should be created for idempotency tracking'); + } + + console.log('\n๐ŸŽ‰ All tests passed! Enhanced functionality is working correctly.'); + console.log('\nโœจ Features validated:'); + console.log(' โ€ข Collection toggle with item cascading'); + console.log(' โ€ข Enhanced list display with collection indicators'); + console.log(' โ€ข Auto-apply after toggle operations'); + console.log(' โ€ข File removal when items are disabled'); + console.log(' โ€ข Individual item overrides'); + console.log(' โ€ข Idempotent apply operations'); + console.log(' โ€ข Configuration validation with error reporting'); + console.log(' โ€ข State tracking for optimization'); + +} catch (error) { + console.log(`\n๐Ÿ’ฅ Test failed: ${error.message}`); + process.exit(1); +} finally { + // Cleanup + cleanup(); + console.log('\n๐Ÿงน Cleanup completed.'); +} \ No newline at end of file diff --git a/test-invalid.yml b/test-invalid.yml deleted file mode 100644 index 05084e7..0000000 --- a/test-invalid.yml +++ /dev/null @@ -1,16 +0,0 @@ -# Invalid config for testing -version: 1.0 - -project: - name: "Test Project" - output_directory: ".awesome-copilot" - -prompts: - breakdown-epic-arch: invalid_value # This should be boolean - breakdown-epic-pm: true - -invalid_section: - some_key: true - -instructions: - task-implementation: "yes" # This should be boolean \ No newline at end of file