diff --git a/.vscode/settings.json b/.vscode/settings.json index 6776bb9..fc19b34 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,37 +1,11 @@ { "chat.modeFilesLocations": { - "chatmodes": true + ".awesome-copilot/chatmodes": true }, "chat.promptFilesLocations": { - "prompts": true + ".awesome-copilot/prompts": true }, "chat.instructionsFilesLocations": { - "instructions": true - }, - "files.eol": "\n", - "files.insertFinalNewline": true, - "files.trimTrailingWhitespace": true, - "[markdown]": { - "files.trimTrailingWhitespace": false, - "editor.formatOnSave": true - }, - "editor.rulers": [ - 120 - ], - "files.associations": { - "*.chatmode.md": "markdown", - "*.instructions.md": "markdown", - "*.prompt.md": "markdown" - }, - "github.copilot.chat.agent.thinkingTool": true, - "github.copilot.chat.alternateGptPrompt.enabled": true, - "github.copilot.chat.editor.temporalContext.enabled": true, - "github.copilot.chat.edits.temporalContext.enabled": true, - "github.copilot.chat.generateTests.codeLens": true, - "github.copilot.chat.languageContext.fix.typescript.enabled": true, - "github.copilot.chat.languageContext.inline.typescript.enabled": true, - "github.copilot.chat.languageContext.typescript.enabled": true, - "github.copilot.chat.newWorkspace.useContext7": true, - "github.copilot.chat.notebook.enhancedNextEditSuggestions.enabled": true, - "github.copilot.chat.notebook.followCellExecution.enabled": true, -} + ".awesome-copilot/instructions": true + } +} \ No newline at end of file diff --git a/apply-config.js b/apply-config.js index 675acfd..a32126f 100755 --- a/apply-config.js +++ b/apply-config.js @@ -5,61 +5,134 @@ const path = require("path"); const { parseCollectionYaml } = require("./yaml-parser"); /** - * Simple YAML parser for configuration files + * Simple YAML parser for configuration files with enhanced error handling */ function parseConfigYamlContent(content) { const lines = content.split("\n"); const result = {}; let currentSection = null; + let lineNumber = 0; - for (const line of lines) { - const trimmed = line.trim(); + try { + for (const line of lines) { + lineNumber++; + const trimmed = line.trim(); - // Skip comments and empty lines - if (!trimmed || trimmed.startsWith("#")) continue; + // Skip comments and empty lines + if (!trimmed || trimmed.startsWith("#")) continue; - if (!trimmed.includes(":")) { - continue; - } - - const colonIndex = trimmed.indexOf(":"); - const key = trimmed.substring(0, colonIndex).trim(); - let value = trimmed.substring(colonIndex + 1).trim(); - - // Remove quotes if present - if ((value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'"))) { - value = value.slice(1, -1); - } - - // Handle sections (no value) - if (!value) { - currentSection = key; - if (!result[currentSection]) { - result[currentSection] = {}; + if (!trimmed.includes(":")) { + continue; + } + + const colonIndex = trimmed.indexOf(":"); + const key = trimmed.substring(0, colonIndex).trim(); + let value = trimmed.substring(colonIndex + 1).trim(); + + // Validate key format + if (!key || key.includes(" ")) { + throw new Error(`Invalid key format on line ${lineNumber}: "${key}"`); + } + + // Remove quotes if present + if ((value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'"))) { + value = value.slice(1, -1); + } + + // Handle sections (no value) + if (!value) { + currentSection = key; + if (!result[currentSection]) { + result[currentSection] = {}; + } + continue; + } + + // Handle boolean values + if (value === "true") value = true; + else if (value === "false") value = false; + + if (currentSection) { + result[currentSection][key] = value; + } else { + result[key] = value; } - continue; } - // Handle boolean values - if (value === "true") value = true; - else if (value === "false") value = false; + return result; + } catch (error) { + throw new Error(`YAML parsing error: ${error.message}`); + } +} - if (currentSection) { - result[currentSection][key] = value; - } else { - result[key] = value; - } +/** + * Validate configuration structure and content + */ +function validateConfig(config) { + const errors = []; + + if (!config || typeof config !== 'object') { + errors.push("Configuration must be a valid object"); + return errors; } - return result; + // Check version + if (!config.version) { + errors.push("Configuration must have a 'version' field"); + } + + // Check project structure + if (config.project && typeof config.project !== 'object') { + errors.push("'project' field must be an object"); + } + + // Validate sections + const validSections = ['prompts', 'instructions', 'chatmodes', 'collections']; + validSections.forEach(section => { + if (config[section] && typeof config[section] !== 'object') { + errors.push(`'${section}' field must be an object`); + } else if (config[section]) { + // Validate section items + Object.entries(config[section]).forEach(([key, value]) => { + if (typeof value !== 'boolean') { + errors.push(`${section}.${key} must be a boolean value (true/false)`); + } + }); + } + }); + + // Check for unknown top-level fields + const knownFields = ['version', 'project', ...validSections]; + Object.keys(config).forEach(key => { + if (!knownFields.includes(key)) { + errors.push(`Unknown configuration field: '${key}'`); + } + }); + + return errors; } function parseConfigYaml(filePath) { try { const content = fs.readFileSync(filePath, "utf8"); - return parseConfigYamlContent(content); + const config = parseConfigYamlContent(content); + + // Validate the parsed configuration + const validationErrors = validateConfig(config); + if (validationErrors.length > 0) { + console.error(`Configuration validation errors in ${filePath}:`); + validationErrors.forEach(error => { + console.error(` ❌ ${error}`); + }); + throw new Error("Configuration validation failed"); + } + + return config; } catch (error) { + if (error.message === "Configuration validation failed") { + throw error; + } console.error(`Error parsing config file ${filePath}: ${error.message}`); return null; } @@ -92,7 +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, @@ -120,69 +207,123 @@ async function applyConfig(configPath = "awesome-copilot.config.yml") { } } - // Process prompts - if (config.prompts) { - for (const [promptName, enabled] of Object.entries(config.prompts)) { - if (enabled) { - const sourcePath = path.join(rootDir, "prompts", `${promptName}.prompt.md`); - if (fs.existsSync(sourcePath)) { - const destPath = path.join(outputDir, "prompts", `${promptName}.prompt.md`); - copyFile(sourcePath, destPath); - copiedCount++; - summary.prompts++; + // Helper function to process section files + function processSection(sectionName, sourceDir, destDir, fileExtension) { + const enabledInSection = new Set(); + + if (config[sectionName]) { + for (const [itemName, enabled] of Object.entries(config[sectionName])) { + const sourcePath = path.join(rootDir, sourceDir, `${itemName}${fileExtension}`); + const destPath = path.join(outputDir, destDir, `${itemName}${fileExtension}`); + + if (enabled && fs.existsSync(sourcePath)) { + 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 + fs.unlinkSync(destPath); + console.log(`✗ Removed: ${itemName}${fileExtension}`); + removedCount++; } } } + + // Remove any files in destination that are not enabled + const destDirPath = path.join(outputDir, destDir); + if (fs.existsSync(destDirPath)) { + const existingFiles = fs.readdirSync(destDirPath) + .filter(file => file.endsWith(fileExtension)); + + existingFiles.forEach(file => { + const itemName = file.replace(fileExtension, ''); + if (!enabledInSection.has(itemName) && !isItemInEnabledCollection(file, enabledItems)) { + const filePath = path.join(destDirPath, file); + fs.unlinkSync(filePath); + console.log(`✗ Removed orphaned: ${file}`); + removedCount++; + } + }); + } } + // 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) { + if (path.basename(itemPath) === filename) { + return true; + } + } + return false; + } + + // Process prompts + processSection("prompts", "prompts", "prompts", ".prompt.md"); + // Process instructions - if (config.instructions) { - for (const [instructionName, enabled] of Object.entries(config.instructions)) { - if (enabled) { - const sourcePath = path.join(rootDir, "instructions", `${instructionName}.instructions.md`); - if (fs.existsSync(sourcePath)) { - const destPath = path.join(outputDir, "instructions", `${instructionName}.instructions.md`); - copyFile(sourcePath, destPath); - copiedCount++; - summary.instructions++; - } - } - } - } + processSection("instructions", "instructions", "instructions", ".instructions.md"); // Process chat modes - if (config.chatmodes) { - for (const [chatmodeName, enabled] of Object.entries(config.chatmodes)) { - if (enabled) { - const sourcePath = path.join(rootDir, "chatmodes", `${chatmodeName}.chatmode.md`); - if (fs.existsSync(sourcePath)) { - const destPath = path.join(outputDir, "chatmodes", `${chatmodeName}.chatmode.md`); - copyFile(sourcePath, destPath); - copiedCount++; - summary.chatmodes++; - } - } - } - } + processSection("chatmodes", "chatmodes", "chatmodes", ".chatmode.md"); - // Process items from enabled collections + // Process items from enabled collections, but respect individual overrides for (const itemPath of enabledItems) { const sourcePath = path.join(rootDir, itemPath); if (fs.existsSync(sourcePath)) { const fileName = path.basename(itemPath); + const itemName = fileName.replace(/\.(prompt|instructions|chatmode)\.md$/, ''); let destPath; + let section; if (fileName.endsWith('.prompt.md')) { destPath = path.join(outputDir, "prompts", fileName); + section = "prompts"; } else if (fileName.endsWith('.chatmode.md')) { destPath = path.join(outputDir, "chatmodes", fileName); + section = "chatmodes"; } else if (fileName.endsWith('.instructions.md')) { destPath = path.join(outputDir, "instructions", fileName); + section = "instructions"; } - if (destPath && !fs.existsSync(destPath)) { - copyFile(sourcePath, destPath); - copiedCount++; + // Only copy if not explicitly disabled in individual settings + const isExplicitlyDisabled = config[section] && config[section][itemName] === false; + + if (destPath && !isExplicitlyDisabled) { + const copyResult = copyFileWithTracking(sourcePath, destPath); + if (copyResult.copied) { + copiedCount++; + } else if (copyResult.skipped) { + skippedCount++; + } } } } @@ -193,6 +334,12 @@ 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}`); + } console.log(`🎯 Prompts: ${summary.prompts}`); console.log(`📋 Instructions: ${summary.instructions}`); console.log(`💭 Chat modes: ${summary.chatmodes}`); @@ -201,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"); @@ -219,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/awesome-copilot.js b/awesome-copilot.js index a64eed5..a2bb7f2 100755 --- a/awesome-copilot.js +++ b/awesome-copilot.js @@ -12,7 +12,10 @@ const { saveConfig, ensureConfigStructure, countEnabledItems, - getAllAvailableItems + getAllAvailableItems, + getCollectionItems, + updateCollectionItems, + getItemToCollectionsMap } = require("./config-manager"); const CONFIG_FLAG_ALIASES = ["--config", "-c"]; @@ -115,10 +118,22 @@ function handleListCommand(rawArgs) { const { config } = loadConfig(configPath); const sanitizedConfig = ensureConfigStructure(config); + const itemToCollectionsMap = getItemToCollectionsMap(); console.log(`📄 Configuration: ${configPath}`); + // Always show collections first if they're in the sections to show + const orderedSections = []; + if (sectionsToShow.includes('collections')) { + orderedSections.push('collections'); + } sectionsToShow.forEach(section => { + if (section !== 'collections') { + orderedSections.push(section); + } + }); + + orderedSections.forEach(section => { const availableItems = getAllAvailableItems(section); const enabledCount = countEnabledItems(sanitizedConfig[section]); const { totalCharacters } = calculateSectionFootprint(section, sanitizedConfig[section]); @@ -139,11 +154,27 @@ function handleListCommand(rawArgs) { availableItems.forEach(itemName => { const isEnabled = Boolean(sanitizedConfig[section]?.[itemName]); - console.log(` [${isEnabled ? "✓" : " "}] ${itemName}`); + let itemDisplay = ` [${isEnabled ? "✓" : " "}] ${itemName}`; + + // Add collection indicators for non-collection items + if (section !== 'collections' && itemToCollectionsMap[itemName]) { + const collections = itemToCollectionsMap[itemName].collections; + itemDisplay += ` ${collections.map(c => `📦${c}`).join(' ')}`; + } + + // For collections, show how many items they contain + if (section === 'collections') { + const collectionItems = getCollectionItems(itemName); + const totalItems = collectionItems.prompts.length + collectionItems.instructions.length + collectionItems.chatmodes.length; + itemDisplay += ` (${totalItems} items)`; + } + + console.log(itemDisplay); }); }); console.log("\nUse 'awesome-copilot toggle' to enable or disable specific items."); + console.log("📦 indicates items that are part of collections."); } function handleToggleCommand(rawArgs) { @@ -165,7 +196,7 @@ function handleToggleCommand(rawArgs) { } const { config, header } = loadConfig(configPath); - const configCopy = { + let configCopy = { ...config, [section]: { ...config[section] } }; @@ -195,7 +226,32 @@ function handleToggleCommand(rawArgs) { const currentState = Boolean(sectionState[itemName]); const newState = desiredState === null ? !currentState : desiredState; sectionState[itemName] = newState; - console.log(`${newState ? "Enabled" : "Disabled"} ${SECTION_METADATA[section].singular} '${itemName}'.`); + + // Special handling for collections - also toggle individual items + if (section === "collections") { + console.log(`${newState ? "Enabled" : "Disabled"} ${SECTION_METADATA[section].singular} '${itemName}'.`); + + // Update individual items in the collection + configCopy = updateCollectionItems(configCopy, itemName, newState); + + const collectionItems = getCollectionItems(itemName); + const totalItems = collectionItems.prompts.length + collectionItems.instructions.length + collectionItems.chatmodes.length; + + if (totalItems > 0) { + console.log(`${newState ? "Enabled" : "Disabled"} ${totalItems} individual items from collection '${itemName}'.`); + if (collectionItems.prompts.length > 0) { + console.log(` Prompts: ${collectionItems.prompts.join(", ")}`); + } + if (collectionItems.instructions.length > 0) { + console.log(` Instructions: ${collectionItems.instructions.join(", ")}`); + } + if (collectionItems.chatmodes.length > 0) { + console.log(` Chat modes: ${collectionItems.chatmodes.join(", ")}`); + } + } + } else { + console.log(`${newState ? "Enabled" : "Disabled"} ${SECTION_METADATA[section].singular} '${itemName}'.`); + } } const sanitizedConfig = ensureConfigStructure(configCopy); @@ -210,7 +266,12 @@ function handleToggleCommand(rawArgs) { console.log(`Estimated ${SECTION_METADATA[section].label.toLowerCase()} context size: ${formatNumber(totalCharacters)} characters.`); } maybeWarnAboutContext(section, totalCharacters); - console.log("Run 'awesome-copilot apply' to copy updated selections into your project."); + + // Auto-apply functionality - automatically run apply after toggle + console.log("Applying configuration automatically..."); + applyConfig(configPath).catch(error => { + console.error("Error during auto-apply:", error.message); + }); } function extractConfigOption(rawArgs) { diff --git a/config-manager.js b/config-manager.js index 628837f..bd28fbe 100644 --- a/config-manager.js +++ b/config-manager.js @@ -3,6 +3,7 @@ const path = require("path"); const { parseConfigYamlContent } = require("./apply-config"); const { objectToYaml, generateConfigHeader, getAvailableItems } = require("./generate-config"); +const { parseCollectionYaml } = require("./yaml-parser"); const DEFAULT_CONFIG_PATH = "awesome-copilot.config.yml"; const SECTION_METADATA = { @@ -158,6 +159,115 @@ function getAllAvailableItems(type) { return getAvailableItems(path.join(__dirname, meta.dir), meta.ext); } +/** + * Get individual items from a collection + */ +function getCollectionItems(collectionName) { + const collectionPath = path.join(__dirname, "collections", `${collectionName}.collection.yml`); + if (!fs.existsSync(collectionPath)) { + return { prompts: [], instructions: [], chatmodes: [] }; + } + + const collection = parseCollectionYaml(collectionPath); + if (!collection || !collection.items) { + return { prompts: [], instructions: [], chatmodes: [] }; + } + + const result = { prompts: [], instructions: [], chatmodes: [] }; + + collection.items.forEach(item => { + const filePath = item.path; + const filename = path.basename(filePath); + + if (filename.endsWith('.prompt.md')) { + const name = filename.replace('.prompt.md', ''); + result.prompts.push(name); + } else if (filename.endsWith('.instructions.md')) { + const name = filename.replace('.instructions.md', ''); + result.instructions.push(name); + } else if (filename.endsWith('.chatmode.md')) { + const name = filename.replace('.chatmode.md', ''); + result.chatmodes.push(name); + } + }); + + return result; +} + +/** + * Update individual items when a collection is toggled + */ +function updateCollectionItems(config, collectionName, enabled) { + const items = getCollectionItems(collectionName); + const configCopy = { ...config }; + + // Ensure sections exist + CONFIG_SECTIONS.forEach(section => { + if (!configCopy[section]) { + configCopy[section] = {}; + } + }); + + // Update individual items + items.prompts.forEach(prompt => { + configCopy.prompts[prompt] = enabled; + }); + + items.instructions.forEach(instruction => { + configCopy.instructions[instruction] = enabled; + }); + + items.chatmodes.forEach(chatmode => { + configCopy.chatmodes[chatmode] = enabled; + }); + + return configCopy; +} + +/** + * Get which collections contain specific items + */ +function getItemToCollectionsMap() { + const map = {}; + const collectionsDir = path.join(__dirname, "collections"); + + if (!fs.existsSync(collectionsDir)) { + return map; + } + + const collectionFiles = fs.readdirSync(collectionsDir) + .filter(file => file.endsWith('.collection.yml')); + + collectionFiles.forEach(file => { + const collectionName = file.replace('.collection.yml', ''); + const collectionPath = path.join(collectionsDir, file); + const collection = parseCollectionYaml(collectionPath); + + if (collection && collection.items) { + collection.items.forEach(item => { + const filename = path.basename(item.path); + let itemName; + + if (filename.endsWith('.prompt.md')) { + itemName = filename.replace('.prompt.md', ''); + if (!map[itemName]) map[itemName] = { section: 'prompts', collections: [] }; + map[itemName].collections.push(collectionName); + } else if (filename.endsWith('.instructions.md')) { + itemName = filename.replace('.instructions.md', ''); + if (!map[itemName]) map[itemName] = { section: 'instructions', collections: [] }; + map[itemName].collections.push(collectionName); + } else if (filename.endsWith('.chatmode.md')) { + itemName = filename.replace('.chatmode.md', ''); + if (!map[itemName]) map[itemName] = { section: 'chatmodes', collections: [] }; + map[itemName].collections.push(collectionName); + } + }); + } + }); + + return map; +} + module.exports = { DEFAULT_CONFIG_PATH, CONFIG_SECTIONS, @@ -168,5 +278,8 @@ module.exports = { ensureConfigStructure, sortObjectKeys, countEnabledItems, - getAllAvailableItems + getAllAvailableItems, + getCollectionItems, + updateCollectionItems, + getItemToCollectionsMap }; diff --git a/test-functionality.js b/test-functionality.js new file mode 100755 index 0000000..4d005ff --- /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)) { + fs.rmSync(TEST_OUTPUT_DIR, { recursive: true, force: true }); + } + if (fs.existsSync('.awesome-copilot')) { + fs.rmSync('.awesome-copilot', { recursive: true, force: true }); + } +} + +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