Merge branch 'main' into copilot/vscode1758504985723

This commit is contained in:
Steven Mosley 2025-09-23 01:06:16 -05:00 committed by GitHub
commit 8f9437646a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 410 additions and 22 deletions

View File

@ -137,13 +137,17 @@ async function applyConfig(configPath = "awesome-copilot.config.yml") {
} }
} }
// Clean up files that are no longer enabled (requirement #3: Toggling instructions off will remove them)
const cleanupSummary = cleanupDisabledFiles(outputDir, effectivelyEnabledSets, rootDir);
// Process prompts using effective states // Process prompts using effective states
for (const promptName of effectivelyEnabledSets.prompts) { for (const promptName of effectivelyEnabledSets.prompts) {
const sourcePath = path.join(rootDir, "prompts", `${promptName}.prompt.md`); const sourcePath = path.join(rootDir, "prompts", `${promptName}.prompt.md`);
if (fs.existsSync(sourcePath)) { if (fs.existsSync(sourcePath)) {
const destPath = path.join(outputDir, "prompts", `${promptName}.prompt.md`); const destPath = path.join(outputDir, "prompts", `${promptName}.prompt.md`);
copyFile(sourcePath, destPath); if (copyFile(sourcePath, destPath)) {
copiedCount++; copiedCount++;
}
summary.prompts++; summary.prompts++;
} }
} }
@ -153,8 +157,9 @@ async function applyConfig(configPath = "awesome-copilot.config.yml") {
const sourcePath = path.join(rootDir, "instructions", `${instructionName}.instructions.md`); const sourcePath = path.join(rootDir, "instructions", `${instructionName}.instructions.md`);
if (fs.existsSync(sourcePath)) { if (fs.existsSync(sourcePath)) {
const destPath = path.join(outputDir, "instructions", `${instructionName}.instructions.md`); const destPath = path.join(outputDir, "instructions", `${instructionName}.instructions.md`);
copyFile(sourcePath, destPath); if (copyFile(sourcePath, destPath)) {
copiedCount++; copiedCount++;
}
summary.instructions++; summary.instructions++;
} }
} }
@ -164,8 +169,9 @@ async function applyConfig(configPath = "awesome-copilot.config.yml") {
const sourcePath = path.join(rootDir, "chatmodes", `${chatmodeName}.chatmode.md`); const sourcePath = path.join(rootDir, "chatmodes", `${chatmodeName}.chatmode.md`);
if (fs.existsSync(sourcePath)) { if (fs.existsSync(sourcePath)) {
const destPath = path.join(outputDir, "chatmodes", `${chatmodeName}.chatmode.md`); const destPath = path.join(outputDir, "chatmodes", `${chatmodeName}.chatmode.md`);
copyFile(sourcePath, destPath); if (copyFile(sourcePath, destPath)) {
copiedCount++; copiedCount++;
}
summary.chatmodes++; summary.chatmodes++;
} }
} }
@ -203,11 +209,66 @@ function ensureDirectoryExists(dirPath) {
} }
/** /**
* Copy file from source to destination * 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)
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
}
}
fs.copyFileSync(sourcePath, destPath); fs.copyFileSync(sourcePath, destPath);
console.log(`✓ Copied: ${path.basename(sourcePath)}`); console.log(`✓ Copied: ${path.basename(sourcePath)}`);
return true; // File was copied
}
/**
* Cleans up files in the output directory that are no longer enabled.
*
* @param {string} outputDir - The root directory where generated files are stored.
* @param {Object} effectivelyEnabledSets - An object mapping section names to Sets of enabled item names.
* @param {string} rootDir - The root directory of the project (used for path resolution).
* @returns {Object} Summary of the number of files removed per section.
*/
function cleanupDisabledFiles(outputDir, effectivelyEnabledSets, rootDir) {
prompts: 0,
instructions: 0,
chatmodes: 0
};
const sections = [
{ name: "prompts", ext: ".prompt.md" },
{ name: "instructions", ext: ".instructions.md" },
{ name: "chatmodes", ext: ".chatmode.md" }
];
for (const section of sections) {
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);
cleanupSummary[section.name]++;
console.log(`🗑️ Removed: ${section.name}/${fileName}`);
}
}
}
return cleanupSummary;
} }
// CLI usage // CLI usage

View File

@ -54,9 +54,17 @@ const commands = {
toggle: { toggle: {
description: "Enable or disable prompts, instructions, chat modes, or collections", description: "Enable or disable prompts, instructions, chat modes, or collections",
usage: "awesome-copilot toggle <section> <name|all> [on|off] [--config <file>]", usage: "awesome-copilot toggle <section> <name|all> [on|off] [--all] [--apply] [--config <file>]",
action: async (args) => {
await handleToggleCommand(args);
}
},
reset: {
description: "Reset .awesome-copilot directory (remove all files but keep structure)",
usage: "awesome-copilot reset [config-file]",
action: (args) => { action: (args) => {
handleToggleCommand(args); handleResetCommand(args);
} }
}, },
@ -87,9 +95,12 @@ function showHelp() {
console.log(" awesome-copilot init # Create default config file"); console.log(" awesome-copilot init # Create default config file");
console.log(" awesome-copilot init my-config.yml # Create named config file"); console.log(" awesome-copilot init my-config.yml # Create named config file");
console.log(" awesome-copilot apply # Apply default config"); console.log(" awesome-copilot apply # Apply default config");
console.log(" awesome-copilot reset # Clear .awesome-copilot directory");
console.log(" awesome-copilot list instructions # See which instructions are enabled"); console.log(" awesome-copilot list instructions # See which instructions are enabled");
console.log(" awesome-copilot toggle prompts create-readme on # Enable a specific prompt"); console.log(" awesome-copilot toggle prompts create-readme on # Enable a specific prompt");
console.log(" awesome-copilot toggle instructions all off --config team.yml # Disable all instructions"); console.log(" awesome-copilot toggle instructions all off --config team.yml # Disable all instructions");
console.log(" awesome-copilot toggle prompts all on --all # Force enable ALL prompts (override explicit settings)");
console.log(" awesome-copilot toggle collections testing-automation on --apply # Enable collection and apply");
console.log(""); console.log("");
console.log("Workflow:"); console.log("Workflow:");
console.log(" 1. Run 'awesome-copilot init' to create a configuration file"); console.log(" 1. Run 'awesome-copilot init' to create a configuration file");
@ -158,9 +169,9 @@ function handleListCommand(rawArgs) {
const effectiveState = effectiveStates[section]?.[itemName]; const effectiveState = effectiveStates[section]?.[itemName];
if (effectiveState) { if (effectiveState) {
const symbol = effectiveState.enabled ? "✓" : " "; const symbol = effectiveState.enabled ? "✓" : " ";
const reasonText = effectiveState.enabled const reasonText = effectiveState.reason === "explicit"
? ` (${effectiveState.reason})` ? ` (${effectiveState.reason})`
: ""; : effectiveState.enabled ? ` (${effectiveState.reason})` : "";
console.log(` [${symbol}] ${itemName}${reasonText}`); console.log(` [${symbol}] ${itemName}${reasonText}`);
} else { } else {
console.log(` [ ] ${itemName}`); console.log(` [ ] ${itemName}`);
@ -172,18 +183,23 @@ function handleListCommand(rawArgs) {
console.log("\nUse 'awesome-copilot toggle' to enable or disable specific items."); console.log("\nUse 'awesome-copilot toggle' to enable or disable specific items.");
} }
function handleToggleCommand(rawArgs) { async function handleToggleCommand(rawArgs) {
const { args, configPath } = extractConfigOption(rawArgs); const { args, configPath, flags } = extractToggleOptions(rawArgs);
if (args.length < 2) { if (args.length < 2) {
throw new Error("Usage: awesome-copilot toggle <section> <name|all> [on|off] [--config <file>]"); throw new Error("Usage: awesome-copilot toggle <section> <name|all> [on|off] [--all] [--apply] [--config <file>]");
} }
const section = validateSectionType(args[0]); const section = validateSectionType(args[0]);
const itemName = args[1]; let itemName = args[1];
const stateArg = args[2]; const stateArg = args[2];
const desiredState = stateArg ? parseStateToken(stateArg) : null; const desiredState = stateArg ? parseStateToken(stateArg) : null;
// Handle --all flag
if (flags.all) {
itemName = "all";
}
const availableItems = getAllAvailableItems(section); const availableItems = getAllAvailableItems(section);
const availableSet = new Set(availableItems); const availableSet = new Set(availableItems);
if (!availableItems.length) { if (!availableItems.length) {
@ -260,10 +276,20 @@ function handleToggleCommand(rawArgs) {
if (desiredState === null) { if (desiredState === null) {
throw new Error("Specify 'on' or 'off' when toggling all items."); throw new Error("Specify 'on' or 'off' when toggling all items.");
} }
// Enhanced --all behavior: override ALL items, even explicit ones
if (flags.all) {
console.log(`${desiredState ? "Force-enabling" : "Force-disabling"} ALL ${SECTION_METADATA[section].label.toLowerCase()} (including explicit overrides).`);
availableItems.forEach(item => {
sectionState[item] = desiredState;
});
} else {
// Regular "all" behavior: set all items explicitly but respect that they are now explicit
availableItems.forEach(item => { availableItems.forEach(item => {
sectionState[item] = desiredState; sectionState[item] = desiredState;
}); });
console.log(`${desiredState ? "Enabled" : "Disabled"} all ${SECTION_METADATA[section].label.toLowerCase()}.`); console.log(`${desiredState ? "Enabled" : "Disabled"} all ${SECTION_METADATA[section].label.toLowerCase()}.`);
}
if (section === "instructions" && desiredState) { if (section === "instructions" && desiredState) {
console.log("⚠️ Enabling every instruction can exceed Copilot Agent's context window. Consider enabling only what you need."); console.log("⚠️ Enabling every instruction can exceed Copilot Agent's context window. Consider enabling only what you need.");
@ -295,8 +321,54 @@ function handleToggleCommand(rawArgs) {
console.log(`Estimated ${SECTION_METADATA[section].label.toLowerCase()} context size: ${formatNumber(totalCharacters)} characters.`); console.log(`Estimated ${SECTION_METADATA[section].label.toLowerCase()} context size: ${formatNumber(totalCharacters)} characters.`);
} }
maybeWarnAboutContext(section, totalCharacters); maybeWarnAboutContext(section, totalCharacters);
// Handle automatic application
if (flags.apply) {
console.log("\n🔄 Applying configuration automatically...");
await applyConfig(configPath);
} else {
console.log("Run 'awesome-copilot apply' to copy updated selections into your project."); console.log("Run 'awesome-copilot apply' to copy updated selections into your project.");
} }
}
function extractToggleOptions(rawArgs) {
const args = [...rawArgs];
let configPath = DEFAULT_CONFIG_PATH;
const flags = {
all: false,
apply: false
};
// Process flags
for (let i = args.length - 1; i >= 0; i--) {
const arg = args[i];
if (arg === "--all") {
flags.all = true;
args.splice(i, 1);
} else if (arg === "--apply") {
flags.apply = true;
args.splice(i, 1);
} else if (CONFIG_FLAG_ALIASES.includes(arg)) {
if (i === args.length - 1) {
throw new Error("Missing configuration file after --config flag.");
}
configPath = args[i + 1];
args.splice(i, 2);
}
}
// Check for config file as last argument
if (args.length > 0) {
const potentialPath = args[args.length - 1];
if (isConfigFilePath(potentialPath)) {
configPath = potentialPath;
args.pop();
}
}
return { args, configPath, flags };
}
function extractConfigOption(rawArgs) { function extractConfigOption(rawArgs) {
const args = [...rawArgs]; const args = [...rawArgs];
@ -396,6 +468,51 @@ function findClosestMatch(target, candidates) {
return candidates.find(candidate => candidate.toLowerCase().includes(normalizedTarget)); return candidates.find(candidate => candidate.toLowerCase().includes(normalizedTarget));
} }
function handleResetCommand(rawArgs) {
const { args, configPath } = extractConfigOption(rawArgs);
const { config } = loadConfig(configPath);
const outputDir = config.project?.output_directory || ".awesome-copilot";
if (!fs.existsSync(outputDir)) {
console.log(`📁 Directory ${outputDir} does not exist - nothing to reset.`);
return;
}
console.log(`🔄 Resetting ${outputDir} directory...`);
let removedCount = 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}`);
}
}
}
}
// 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`);
}
console.log(`\n✅ Reset complete! Removed ${removedCount} files.`);
console.log(`📁 Directory structure preserved: ${outputDir}/`);
console.log("Run 'awesome-copilot apply' to repopulate with current configuration.");
}
async function main() { async function main() {
const args = process.argv.slice(2); const args = process.argv.slice(2);

View File

@ -105,8 +105,8 @@ async function runTests() {
assert(result.success, 'Individual toggle should succeed'); assert(result.success, 'Individual toggle should succeed');
const listResult = await runCommand(`node awesome-copilot.js list prompts --config ${TEST_CONFIG}`); const listResult = await runCommand(`node awesome-copilot.js list prompts --config ${TEST_CONFIG}`);
assert(listResult.stdout.includes('playwright-generate-test') && !listResult.stdout.includes('playwright-generate-test ('), assert(listResult.stdout.includes('playwright-generate-test (explicit)'),
'Explicitly disabled item should not show reason'); 'Explicitly disabled item should show explicit reason');
}); });
// Test 6: Error handling for invalid commands // Test 6: Error handling for invalid commands

210
test-new-features.js Executable file
View File

@ -0,0 +1,210 @@
#!/usr/bin/env node
/**
* Tests for new features added to awesome-copilot
*/
const fs = require('fs');
const path = require('path');
const { exec } = require('child_process');
const { promisify } = require('util');
const execAsync = promisify(exec);
// Change to project directory for tests
process.chdir(__dirname);
const TEST_CONFIG = 'test-new-features.yml';
const TEST_OUTPUT_DIR = '.test-new-features-awesome-copilot';
function assert(condition, message) {
if (!condition) {
throw new Error(`Assertion failed: ${message}`);
}
}
function cleanup() {
try {
if (fs.existsSync(TEST_CONFIG)) {
fs.unlinkSync(TEST_CONFIG);
}
if (fs.existsSync(TEST_OUTPUT_DIR)) {
fs.rmSync(TEST_OUTPUT_DIR, { recursive: true, force: true });
}
} catch (error) {
// Ignore cleanup errors
}
}
async function runCommand(command) {
try {
const { stdout, stderr } = await execAsync(command);
return { success: true, stdout, stderr };
} catch (error) {
return { success: false, stdout: error.stdout || '', stderr: error.stderr || '', error };
}
}
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}"`);
fs.writeFileSync(configFile, content);
}
}
async function runTests() {
console.log('Running tests for new awesome-copilot features...\n');
let passedTests = 0;
let totalTests = 0;
async function test(name, testFn) {
totalTests++;
try {
cleanup(); // Clean up before each test
await testFn();
console.log(`${name}`);
passedTests++;
} catch (error) {
console.log(`${name}: ${error.message}`);
}
}
// Test 1: Reset command
await test("Reset command clears .awesome-copilot directory", async () => {
await runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`);
setTestOutputDir(TEST_CONFIG);
// Enable something and apply
await runCommand(`node awesome-copilot.js toggle collections testing-automation on --config ${TEST_CONFIG}`);
await runCommand(`node awesome-copilot.js apply ${TEST_CONFIG}`);
// Check files exist
const filesBefore = fs.readdirSync(TEST_OUTPUT_DIR, { recursive: true });
assert(filesBefore.some(f => f.includes('.md')), 'Should have some files before reset');
// Reset
const resetResult = await runCommand(`node awesome-copilot.js reset ${TEST_CONFIG}`);
assert(resetResult.success, 'Reset should succeed');
// Check files are gone but directories remain
assert(fs.existsSync(TEST_OUTPUT_DIR), 'Output directory should still exist');
assert(fs.existsSync(path.join(TEST_OUTPUT_DIR, 'prompts')), 'Prompts directory should still exist');
const filesAfter = fs.readdirSync(TEST_OUTPUT_DIR, { recursive: true });
assert(!filesAfter.some(f => f.includes('.md')), 'Should have no .md files after reset');
});
// Test 2: --apply flag
await test("--apply flag automatically applies after toggle", async () => {
await runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`);
setTestOutputDir(TEST_CONFIG);
// Toggle with --apply flag
const result = await runCommand(`node awesome-copilot.js toggle collections testing-automation on --apply --config ${TEST_CONFIG}`);
assert(result.success, 'Toggle with --apply should succeed');
assert(result.stdout.includes('Applying configuration automatically'), 'Should show auto-apply message');
// Check files were applied
const files = fs.readdirSync(TEST_OUTPUT_DIR, { recursive: true });
assert(files.some(f => f.includes('.md')), 'Files should be automatically applied');
});
// Test 3: --all flag force overrides
await test("--all flag overrides explicit settings", async () => {
await runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`);
// Set explicit false
await runCommand(`node awesome-copilot.js toggle prompts create-readme off --config ${TEST_CONFIG}`);
// Check it's explicitly disabled
const listResult1 = await runCommand(`node awesome-copilot.js list prompts --config ${TEST_CONFIG}`);
assert(listResult1.stdout.includes('create-readme (explicit)'), 'Should show explicit disabled');
// Force enable all with --all flag
await runCommand(`node awesome-copilot.js toggle prompts all on --all --config ${TEST_CONFIG}`);
// Check it's now explicitly enabled
const listResult2 = await runCommand(`node awesome-copilot.js list prompts --config ${TEST_CONFIG}`);
assert(listResult2.stdout.includes('create-readme (explicit)') && listResult2.stdout.includes('[✓]'), 'Should be explicitly enabled');
});
// Test 4: File cleanup on disable
await test("Files are removed when items are disabled", async () => {
await runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`);
setTestOutputDir(TEST_CONFIG);
// Enable and apply
await runCommand(`node awesome-copilot.js toggle collections testing-automation on --apply --config ${TEST_CONFIG}`);
const filesBefore = fs.readdirSync(TEST_OUTPUT_DIR, { recursive: true });
assert(filesBefore.some(f => f.includes('.md')), 'Should have files after enable');
// Disable and apply
await runCommand(`node awesome-copilot.js toggle collections testing-automation off --apply --config ${TEST_CONFIG}`);
const filesAfter = fs.readdirSync(TEST_OUTPUT_DIR, { recursive: true });
assert(!filesAfter.some(f => f.includes('.md')), 'Should have no files after disable');
});
// Test 5: Idempotency with file content checking
await test("Idempotency checks file content correctly", async () => {
await runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`);
setTestOutputDir(TEST_CONFIG);
// Enable and apply
await runCommand(`node awesome-copilot.js toggle collections testing-automation on --apply --config ${TEST_CONFIG}`);
// Apply again (should show already up-to-date)
const result = await runCommand(`node awesome-copilot.js apply ${TEST_CONFIG}`);
assert(result.stdout.includes('Already exists and up-to-date'), 'Should show idempotency message');
assert(!result.stdout.includes('Copied:'), 'Should not copy any files');
});
// Test 6: Explicit overrides preserved across collection toggles
await test("Explicit overrides preserved across collection toggles", async () => {
await runCommand(`node awesome-copilot.js init ${TEST_CONFIG}`);
// Enable collection
await runCommand(`node awesome-copilot.js toggle collections testing-automation on --config ${TEST_CONFIG}`);
// Set explicit override
await runCommand(`node awesome-copilot.js toggle prompts playwright-generate-test off --config ${TEST_CONFIG}`);
// Toggle collection off and on
await runCommand(`node awesome-copilot.js toggle collections testing-automation off --config ${TEST_CONFIG}`);
const toggleResult = await runCommand(`node awesome-copilot.js toggle collections testing-automation on --config ${TEST_CONFIG}`);
// Check that the explicit override is preserved (playwright-generate-test should NOT be in enabled list)
assert(!toggleResult.stdout.includes('playwright-generate-test'), 'Explicit override should be preserved');
// Double-check with list command
const listResult = await runCommand(`node awesome-copilot.js list prompts --config ${TEST_CONFIG}`);
assert(listResult.stdout.includes('playwright-generate-test (explicit)'), "'playwright-generate-test (explicit)' should be present in the list output");
assert(!listResult.stdout.includes('[✓] playwright-generate-test'), "'[✓] playwright-generate-test' should NOT be present in the list output (should remain explicitly disabled)");
});
console.log(`\nNew Features Test Results: ${passedTests}/${totalTests} passed`);
cleanup(); // Final cleanup
if (passedTests === totalTests) {
console.log('🎉 All new features tests passed!');
return true;
} else {
console.log('💥 Some new features tests failed!');
return false;
}
}
if (require.main === module) {
runTests().then(success => {
process.exit(success ? 0 : 1);
}).catch(error => {
console.error('Test runner error:', error);
cleanup();
process.exit(1);
});
}
module.exports = { runTests };