Merge pull request #7 from AstroSteveo/copilot/fix-4aed708d-cf0e-4c75-b37b-5ef81c1ef895
Enhance awesome-copilot with collection cascading, auto-apply, file management, and robust validation
This commit is contained in:
commit
5797607a54
36
.vscode/settings.json
vendored
36
.vscode/settings.json
vendored
@ -1,37 +1,11 @@
|
|||||||
{
|
{
|
||||||
"chat.modeFilesLocations": {
|
"chat.modeFilesLocations": {
|
||||||
"chatmodes": true
|
".awesome-copilot/chatmodes": true
|
||||||
},
|
},
|
||||||
"chat.promptFilesLocations": {
|
"chat.promptFilesLocations": {
|
||||||
"prompts": true
|
".awesome-copilot/prompts": true
|
||||||
},
|
},
|
||||||
"chat.instructionsFilesLocations": {
|
"chat.instructionsFilesLocations": {
|
||||||
"instructions": true
|
".awesome-copilot/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,
|
|
||||||
}
|
|
||||||
319
apply-config.js
319
apply-config.js
@ -5,61 +5,134 @@ const path = require("path");
|
|||||||
const { parseCollectionYaml } = require("./yaml-parser");
|
const { parseCollectionYaml } = require("./yaml-parser");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple YAML parser for configuration files
|
* Simple YAML parser for configuration files with enhanced error handling
|
||||||
*/
|
*/
|
||||||
function parseConfigYamlContent(content) {
|
function parseConfigYamlContent(content) {
|
||||||
const lines = content.split("\n");
|
const lines = content.split("\n");
|
||||||
const result = {};
|
const result = {};
|
||||||
let currentSection = null;
|
let currentSection = null;
|
||||||
|
let lineNumber = 0;
|
||||||
|
|
||||||
for (const line of lines) {
|
try {
|
||||||
const trimmed = line.trim();
|
for (const line of lines) {
|
||||||
|
lineNumber++;
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
// Skip comments and empty lines
|
// Skip comments and empty lines
|
||||||
if (!trimmed || trimmed.startsWith("#")) continue;
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
||||||
|
|
||||||
if (!trimmed.includes(":")) {
|
if (!trimmed.includes(":")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const colonIndex = trimmed.indexOf(":");
|
const colonIndex = trimmed.indexOf(":");
|
||||||
const key = trimmed.substring(0, colonIndex).trim();
|
const key = trimmed.substring(0, colonIndex).trim();
|
||||||
let value = trimmed.substring(colonIndex + 1).trim();
|
let value = trimmed.substring(colonIndex + 1).trim();
|
||||||
|
|
||||||
// Remove quotes if present
|
// Validate key format
|
||||||
if ((value.startsWith('"') && value.endsWith('"')) ||
|
if (!key || key.includes(" ")) {
|
||||||
(value.startsWith("'") && value.endsWith("'"))) {
|
throw new Error(`Invalid key format on line ${lineNumber}: "${key}"`);
|
||||||
value = value.slice(1, -1);
|
}
|
||||||
}
|
|
||||||
|
// Remove quotes if present
|
||||||
// Handle sections (no value)
|
if ((value.startsWith('"') && value.endsWith('"')) ||
|
||||||
if (!value) {
|
(value.startsWith("'") && value.endsWith("'"))) {
|
||||||
currentSection = key;
|
value = value.slice(1, -1);
|
||||||
if (!result[currentSection]) {
|
}
|
||||||
result[currentSection] = {};
|
|
||||||
|
// 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
|
return result;
|
||||||
if (value === "true") value = true;
|
} catch (error) {
|
||||||
else if (value === "false") value = false;
|
throw new Error(`YAML parsing error: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (currentSection) {
|
/**
|
||||||
result[currentSection][key] = value;
|
* Validate configuration structure and content
|
||||||
} else {
|
*/
|
||||||
result[key] = value;
|
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) {
|
function parseConfigYaml(filePath) {
|
||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(filePath, "utf8");
|
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) {
|
} catch (error) {
|
||||||
|
if (error.message === "Configuration validation failed") {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
console.error(`Error parsing config file ${filePath}: ${error.message}`);
|
console.error(`Error parsing config file ${filePath}: ${error.message}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -92,7 +165,21 @@ async function applyConfig(configPath = "awesome-copilot.config.yml") {
|
|||||||
ensureDirectoryExists(path.join(outputDir, "instructions"));
|
ensureDirectoryExists(path.join(outputDir, "instructions"));
|
||||||
ensureDirectoryExists(path.join(outputDir, "chatmodes"));
|
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 copiedCount = 0;
|
||||||
|
let removedCount = 0;
|
||||||
|
let skippedCount = 0;
|
||||||
const summary = {
|
const summary = {
|
||||||
prompts: 0,
|
prompts: 0,
|
||||||
instructions: 0,
|
instructions: 0,
|
||||||
@ -120,69 +207,123 @@ async function applyConfig(configPath = "awesome-copilot.config.yml") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process prompts
|
// Helper function to process section files
|
||||||
if (config.prompts) {
|
function processSection(sectionName, sourceDir, destDir, fileExtension) {
|
||||||
for (const [promptName, enabled] of Object.entries(config.prompts)) {
|
const enabledInSection = new Set();
|
||||||
if (enabled) {
|
|
||||||
const sourcePath = path.join(rootDir, "prompts", `${promptName}.prompt.md`);
|
if (config[sectionName]) {
|
||||||
if (fs.existsSync(sourcePath)) {
|
for (const [itemName, enabled] of Object.entries(config[sectionName])) {
|
||||||
const destPath = path.join(outputDir, "prompts", `${promptName}.prompt.md`);
|
const sourcePath = path.join(rootDir, sourceDir, `${itemName}${fileExtension}`);
|
||||||
copyFile(sourcePath, destPath);
|
const destPath = path.join(outputDir, destDir, `${itemName}${fileExtension}`);
|
||||||
copiedCount++;
|
|
||||||
summary.prompts++;
|
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
|
// Process instructions
|
||||||
if (config.instructions) {
|
processSection("instructions", "instructions", "instructions", ".instructions.md");
|
||||||
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++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process chat modes
|
// Process chat modes
|
||||||
if (config.chatmodes) {
|
processSection("chatmodes", "chatmodes", "chatmodes", ".chatmode.md");
|
||||||
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++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process items from enabled collections
|
// Process items from enabled collections, but respect individual overrides
|
||||||
for (const itemPath of enabledItems) {
|
for (const itemPath of enabledItems) {
|
||||||
const sourcePath = path.join(rootDir, itemPath);
|
const sourcePath = path.join(rootDir, itemPath);
|
||||||
if (fs.existsSync(sourcePath)) {
|
if (fs.existsSync(sourcePath)) {
|
||||||
const fileName = path.basename(itemPath);
|
const fileName = path.basename(itemPath);
|
||||||
|
const itemName = fileName.replace(/\.(prompt|instructions|chatmode)\.md$/, '');
|
||||||
let destPath;
|
let destPath;
|
||||||
|
let section;
|
||||||
|
|
||||||
if (fileName.endsWith('.prompt.md')) {
|
if (fileName.endsWith('.prompt.md')) {
|
||||||
destPath = path.join(outputDir, "prompts", fileName);
|
destPath = path.join(outputDir, "prompts", fileName);
|
||||||
|
section = "prompts";
|
||||||
} else if (fileName.endsWith('.chatmode.md')) {
|
} else if (fileName.endsWith('.chatmode.md')) {
|
||||||
destPath = path.join(outputDir, "chatmodes", fileName);
|
destPath = path.join(outputDir, "chatmodes", fileName);
|
||||||
|
section = "chatmodes";
|
||||||
} else if (fileName.endsWith('.instructions.md')) {
|
} else if (fileName.endsWith('.instructions.md')) {
|
||||||
destPath = path.join(outputDir, "instructions", fileName);
|
destPath = path.join(outputDir, "instructions", fileName);
|
||||||
|
section = "instructions";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (destPath && !fs.existsSync(destPath)) {
|
// Only copy if not explicitly disabled in individual settings
|
||||||
copyFile(sourcePath, destPath);
|
const isExplicitlyDisabled = config[section] && config[section][itemName] === false;
|
||||||
copiedCount++;
|
|
||||||
|
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("=".repeat(50));
|
||||||
console.log(`📂 Output directory: ${outputDir}`);
|
console.log(`📂 Output directory: ${outputDir}`);
|
||||||
console.log(`📝 Total files copied: ${copiedCount}`);
|
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(`🎯 Prompts: ${summary.prompts}`);
|
||||||
console.log(`📋 Instructions: ${summary.instructions}`);
|
console.log(`📋 Instructions: ${summary.instructions}`);
|
||||||
console.log(`💭 Chat modes: ${summary.chatmodes}`);
|
console.log(`💭 Chat modes: ${summary.chatmodes}`);
|
||||||
@ -201,6 +348,20 @@ async function applyConfig(configPath = "awesome-copilot.config.yml") {
|
|||||||
if (config.project?.name) {
|
if (config.project?.name) {
|
||||||
console.log(`🏷️ Project: ${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("\nNext steps:");
|
||||||
console.log("1. Add the files to your version control system");
|
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
|
// CLI usage
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
const configPath = process.argv[2] || "awesome-copilot.config.yml";
|
const configPath = process.argv[2] || "awesome-copilot.config.yml";
|
||||||
|
|||||||
@ -12,7 +12,10 @@ const {
|
|||||||
saveConfig,
|
saveConfig,
|
||||||
ensureConfigStructure,
|
ensureConfigStructure,
|
||||||
countEnabledItems,
|
countEnabledItems,
|
||||||
getAllAvailableItems
|
getAllAvailableItems,
|
||||||
|
getCollectionItems,
|
||||||
|
updateCollectionItems,
|
||||||
|
getItemToCollectionsMap
|
||||||
} = require("./config-manager");
|
} = require("./config-manager");
|
||||||
|
|
||||||
const CONFIG_FLAG_ALIASES = ["--config", "-c"];
|
const CONFIG_FLAG_ALIASES = ["--config", "-c"];
|
||||||
@ -115,10 +118,22 @@ function handleListCommand(rawArgs) {
|
|||||||
|
|
||||||
const { config } = loadConfig(configPath);
|
const { config } = loadConfig(configPath);
|
||||||
const sanitizedConfig = ensureConfigStructure(config);
|
const sanitizedConfig = ensureConfigStructure(config);
|
||||||
|
const itemToCollectionsMap = getItemToCollectionsMap();
|
||||||
|
|
||||||
console.log(`📄 Configuration: ${configPath}`);
|
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 => {
|
sectionsToShow.forEach(section => {
|
||||||
|
if (section !== 'collections') {
|
||||||
|
orderedSections.push(section);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
orderedSections.forEach(section => {
|
||||||
const availableItems = getAllAvailableItems(section);
|
const availableItems = getAllAvailableItems(section);
|
||||||
const enabledCount = countEnabledItems(sanitizedConfig[section]);
|
const enabledCount = countEnabledItems(sanitizedConfig[section]);
|
||||||
const { totalCharacters } = calculateSectionFootprint(section, sanitizedConfig[section]);
|
const { totalCharacters } = calculateSectionFootprint(section, sanitizedConfig[section]);
|
||||||
@ -139,11 +154,27 @@ function handleListCommand(rawArgs) {
|
|||||||
|
|
||||||
availableItems.forEach(itemName => {
|
availableItems.forEach(itemName => {
|
||||||
const isEnabled = Boolean(sanitizedConfig[section]?.[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("\nUse 'awesome-copilot toggle' to enable or disable specific items.");
|
||||||
|
console.log("📦 indicates items that are part of collections.");
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleToggleCommand(rawArgs) {
|
function handleToggleCommand(rawArgs) {
|
||||||
@ -165,7 +196,7 @@ function handleToggleCommand(rawArgs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { config, header } = loadConfig(configPath);
|
const { config, header } = loadConfig(configPath);
|
||||||
const configCopy = {
|
let configCopy = {
|
||||||
...config,
|
...config,
|
||||||
[section]: { ...config[section] }
|
[section]: { ...config[section] }
|
||||||
};
|
};
|
||||||
@ -195,7 +226,32 @@ function handleToggleCommand(rawArgs) {
|
|||||||
const currentState = Boolean(sectionState[itemName]);
|
const currentState = Boolean(sectionState[itemName]);
|
||||||
const newState = desiredState === null ? !currentState : desiredState;
|
const newState = desiredState === null ? !currentState : desiredState;
|
||||||
sectionState[itemName] = newState;
|
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);
|
const sanitizedConfig = ensureConfigStructure(configCopy);
|
||||||
@ -210,7 +266,12 @@ 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);
|
||||||
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) {
|
function extractConfigOption(rawArgs) {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ const path = require("path");
|
|||||||
|
|
||||||
const { parseConfigYamlContent } = require("./apply-config");
|
const { parseConfigYamlContent } = require("./apply-config");
|
||||||
const { objectToYaml, generateConfigHeader, getAvailableItems } = require("./generate-config");
|
const { objectToYaml, generateConfigHeader, getAvailableItems } = require("./generate-config");
|
||||||
|
const { parseCollectionYaml } = require("./yaml-parser");
|
||||||
|
|
||||||
const DEFAULT_CONFIG_PATH = "awesome-copilot.config.yml";
|
const DEFAULT_CONFIG_PATH = "awesome-copilot.config.yml";
|
||||||
const SECTION_METADATA = {
|
const SECTION_METADATA = {
|
||||||
@ -158,6 +159,115 @@ function getAllAvailableItems(type) {
|
|||||||
return getAvailableItems(path.join(__dirname, meta.dir), meta.ext);
|
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 = {
|
module.exports = {
|
||||||
DEFAULT_CONFIG_PATH,
|
DEFAULT_CONFIG_PATH,
|
||||||
CONFIG_SECTIONS,
|
CONFIG_SECTIONS,
|
||||||
@ -168,5 +278,8 @@ module.exports = {
|
|||||||
ensureConfigStructure,
|
ensureConfigStructure,
|
||||||
sortObjectKeys,
|
sortObjectKeys,
|
||||||
countEnabledItems,
|
countEnabledItems,
|
||||||
getAllAvailableItems
|
getAllAvailableItems,
|
||||||
|
getCollectionItems,
|
||||||
|
updateCollectionItems,
|
||||||
|
getItemToCollectionsMap
|
||||||
};
|
};
|
||||||
|
|||||||
165
test-functionality.js
Executable file
165
test-functionality.js
Executable file
@ -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.');
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user