543 lines
18 KiB
JavaScript
Executable File
543 lines
18 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
const { applyConfig } = require("./apply-config");
|
|
const {
|
|
DEFAULT_CONFIG_PATH,
|
|
CONFIG_SECTIONS,
|
|
SECTION_METADATA,
|
|
loadConfig,
|
|
saveConfig,
|
|
ensureConfigStructure,
|
|
countEnabledItems,
|
|
getAllAvailableItems
|
|
} = require("./config-manager");
|
|
|
|
const CONFIG_FLAG_ALIASES = ["--config", "-c"];
|
|
const CONTEXT_WARNING_CHAR_LIMIT = {
|
|
instructions: 90000,
|
|
prompts: 45000,
|
|
chatmodes: 30000
|
|
};
|
|
|
|
const numberFormatter = new Intl.NumberFormat("en-US");
|
|
|
|
const commands = {
|
|
init: {
|
|
description: "Initialize a new project with awesome-copilot configuration",
|
|
usage: "awesome-copilot init [config-file]",
|
|
action: async (args) => {
|
|
const configFile = args[0] || DEFAULT_CONFIG_PATH;
|
|
const { initializeProject } = require("./initialize-project");
|
|
await initializeProject(configFile);
|
|
}
|
|
},
|
|
|
|
apply: {
|
|
description: "Apply configuration and copy files to project",
|
|
usage: "awesome-copilot apply [config-file]",
|
|
action: async (args) => {
|
|
const configFile = args[0] || DEFAULT_CONFIG_PATH;
|
|
await applyConfig(configFile);
|
|
}
|
|
},
|
|
|
|
list: {
|
|
description: "List items in the configuration with their enabled status",
|
|
usage: "awesome-copilot list [section] [--config <file>]",
|
|
action: (args) => {
|
|
handleListCommand(args);
|
|
}
|
|
},
|
|
|
|
toggle: {
|
|
description: "Enable or disable prompts, instructions, chat modes, or collections",
|
|
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) => {
|
|
handleResetCommand(args);
|
|
}
|
|
},
|
|
|
|
help: {
|
|
description: "Show help information",
|
|
usage: "awesome-copilot help",
|
|
action: () => {
|
|
showHelp();
|
|
}
|
|
}
|
|
};
|
|
|
|
function showHelp() {
|
|
console.log("🤖 Awesome GitHub Copilot Configuration Tool");
|
|
console.log("=".repeat(50));
|
|
console.log("");
|
|
console.log("Usage: awesome-copilot <command> [options]");
|
|
console.log("");
|
|
console.log("Commands:");
|
|
|
|
for (const [name, cmd] of Object.entries(commands)) {
|
|
console.log(` ${name.padEnd(10)} ${cmd.description}`);
|
|
console.log(` ${" ".repeat(10)} ${cmd.usage}`);
|
|
console.log("");
|
|
}
|
|
|
|
console.log("Examples:");
|
|
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 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 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 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("Workflow:");
|
|
console.log(" 1. Run 'awesome-copilot init' to create a configuration file");
|
|
console.log(" 2. Use 'awesome-copilot list' and 'awesome-copilot toggle' to manage enabled items");
|
|
console.log(" 3. Run 'awesome-copilot apply' to copy files to your project");
|
|
}
|
|
|
|
function showError(message) {
|
|
console.error(`❌ Error: ${message}`);
|
|
console.log("");
|
|
console.log("Run 'awesome-copilot help' for usage information.");
|
|
process.exit(1);
|
|
}
|
|
|
|
function handleListCommand(rawArgs) {
|
|
const { args, configPath } = extractConfigOption(rawArgs);
|
|
|
|
let sectionsToShow = CONFIG_SECTIONS;
|
|
if (args.length > 0) {
|
|
const requestedSection = validateSectionType(args[0]);
|
|
sectionsToShow = [requestedSection];
|
|
}
|
|
|
|
const { config } = loadConfig(configPath);
|
|
const sanitizedConfig = ensureConfigStructure(config);
|
|
|
|
// Import computeEffectiveItemStates
|
|
const { computeEffectiveItemStates } = require("./config-manager");
|
|
const effectiveStates = computeEffectiveItemStates(sanitizedConfig);
|
|
|
|
console.log(`📄 Configuration: ${configPath}`);
|
|
|
|
sectionsToShow.forEach(section => {
|
|
const availableItems = getAllAvailableItems(section);
|
|
|
|
// Count effectively enabled items
|
|
const effectivelyEnabled = Object.values(effectiveStates[section] || {})
|
|
.filter(state => state.enabled).length;
|
|
|
|
const { totalCharacters } = calculateSectionFootprint(section, sanitizedConfig[section]);
|
|
const headingParts = [
|
|
`${SECTION_METADATA[section].label} (${effectivelyEnabled}/${availableItems.length} enabled)`
|
|
];
|
|
|
|
if (totalCharacters > 0 && section !== "collections") {
|
|
headingParts.push(`~${formatNumber(totalCharacters)} chars`);
|
|
}
|
|
|
|
console.log(`\n${headingParts.join(", ")}`);
|
|
|
|
if (!availableItems.length) {
|
|
console.log(" (no items available)");
|
|
return;
|
|
}
|
|
|
|
// Show items with effective state and reason
|
|
if (section === "collections") {
|
|
// Collections show simple enabled/disabled
|
|
availableItems.forEach(itemName => {
|
|
const isEnabled = Boolean(sanitizedConfig[section]?.[itemName]);
|
|
console.log(` [${isEnabled ? "✓" : " "}] ${itemName}`);
|
|
});
|
|
} else {
|
|
// Other sections show effective state with reason
|
|
availableItems.forEach(itemName => {
|
|
const effectiveState = effectiveStates[section]?.[itemName];
|
|
if (effectiveState) {
|
|
const symbol = effectiveState.enabled ? "✓" : " ";
|
|
const reasonText = effectiveState.reason === "explicit"
|
|
? ` (${effectiveState.reason})`
|
|
: effectiveState.enabled ? ` (${effectiveState.reason})` : "";
|
|
console.log(` [${symbol}] ${itemName}${reasonText}`);
|
|
} else {
|
|
console.log(` [ ] ${itemName}`);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
console.log("\nUse 'awesome-copilot toggle' to enable or disable specific items.");
|
|
}
|
|
|
|
async function handleToggleCommand(rawArgs) {
|
|
const { args, configPath, flags } = extractToggleOptions(rawArgs);
|
|
|
|
if (args.length < 2) {
|
|
throw new Error("Usage: awesome-copilot toggle <section> <name|all> [on|off] [--all] [--apply] [--config <file>]");
|
|
}
|
|
|
|
const section = validateSectionType(args[0]);
|
|
let itemName = args[1];
|
|
const stateArg = args[2];
|
|
const desiredState = stateArg ? parseStateToken(stateArg) : null;
|
|
|
|
// Handle --all flag
|
|
if (flags.all) {
|
|
itemName = "all";
|
|
}
|
|
|
|
const availableItems = getAllAvailableItems(section);
|
|
const availableSet = new Set(availableItems);
|
|
if (!availableItems.length) {
|
|
throw new Error(`No ${SECTION_METADATA[section].label.toLowerCase()} available to toggle.`);
|
|
}
|
|
|
|
const { config, header } = loadConfig(configPath);
|
|
const configCopy = {
|
|
...config,
|
|
[section]: { ...config[section] }
|
|
};
|
|
const sectionState = configCopy[section];
|
|
|
|
// Special handling for collections to show delta summary
|
|
if (section === "collections") {
|
|
if (!availableSet.has(itemName)) {
|
|
const suggestion = findClosestMatch(itemName, availableItems);
|
|
if (suggestion) {
|
|
throw new Error(`Unknown ${SECTION_METADATA[section].singular} '${itemName}'. Did you mean '${suggestion}'?`);
|
|
}
|
|
throw new Error(`Unknown ${SECTION_METADATA[section].singular} '${itemName}'.`);
|
|
}
|
|
|
|
const currentState = Boolean(sectionState[itemName]);
|
|
const newState = desiredState === null ? !currentState : desiredState;
|
|
|
|
// Show delta summary for collections
|
|
if (currentState !== newState) {
|
|
const { computeEffectiveItemStates } = require("./config-manager");
|
|
|
|
// Compute effective states before change
|
|
const effectiveStatesBefore = computeEffectiveItemStates(configCopy);
|
|
|
|
// Simulate the change
|
|
configCopy[section][itemName] = newState;
|
|
const effectiveStatesAfter = computeEffectiveItemStates(configCopy);
|
|
|
|
// Calculate delta
|
|
const delta = { enabled: [], disabled: [] };
|
|
for (const sectionName of ["prompts", "instructions", "chatmodes"]) {
|
|
for (const item of getAllAvailableItems(sectionName)) {
|
|
const beforeState = effectiveStatesBefore[sectionName]?.[item]?.enabled || false;
|
|
const afterState = effectiveStatesAfter[sectionName]?.[item]?.enabled || false;
|
|
|
|
if (!beforeState && afterState) {
|
|
delta.enabled.push(`${sectionName}/${item}`);
|
|
} else if (beforeState && !afterState) {
|
|
delta.disabled.push(`${sectionName}/${item}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(`${newState ? "Enabled" : "Disabled"} collection '${itemName}'.`);
|
|
|
|
if (delta.enabled.length > 0 || delta.disabled.length > 0) {
|
|
console.log("\nDelta summary:");
|
|
if (delta.enabled.length > 0) {
|
|
console.log(` 📈 ${delta.enabled.length} items will be enabled:`);
|
|
delta.enabled.forEach(item => console.log(` + ${item}`));
|
|
}
|
|
if (delta.disabled.length > 0) {
|
|
console.log(` 📉 ${delta.disabled.length} items will be disabled:`);
|
|
delta.disabled.forEach(item => console.log(` - ${item}`));
|
|
}
|
|
} else {
|
|
console.log(" No effective changes (items may have explicit overrides)");
|
|
}
|
|
} else {
|
|
console.log(`Collection '${itemName}' is already ${currentState ? "enabled" : "disabled"}.`);
|
|
}
|
|
|
|
sectionState[itemName] = newState;
|
|
} else if (itemName === "all") {
|
|
if (desiredState === null) {
|
|
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 => {
|
|
sectionState[item] = desiredState;
|
|
});
|
|
console.log(`${desiredState ? "Enabled" : "Disabled"} all ${SECTION_METADATA[section].label.toLowerCase()}.`);
|
|
}
|
|
|
|
if (section === "instructions" && desiredState) {
|
|
console.log("⚠️ Enabling every instruction can exceed Copilot Agent's context window. Consider enabling only what you need.");
|
|
}
|
|
} else {
|
|
if (!availableSet.has(itemName)) {
|
|
const suggestion = findClosestMatch(itemName, availableItems);
|
|
if (suggestion) {
|
|
throw new Error(`Unknown ${SECTION_METADATA[section].singular} '${itemName}'. Did you mean '${suggestion}'?`);
|
|
}
|
|
throw new Error(`Unknown ${SECTION_METADATA[section].singular} '${itemName}'.`);
|
|
}
|
|
|
|
const currentState = Boolean(sectionState[itemName]);
|
|
const newState = desiredState === null ? !currentState : desiredState;
|
|
sectionState[itemName] = newState;
|
|
console.log(`${newState ? "Enabled" : "Disabled"} ${SECTION_METADATA[section].singular} '${itemName}'.`);
|
|
}
|
|
|
|
const sanitizedConfig = ensureConfigStructure(configCopy);
|
|
saveConfig(configPath, sanitizedConfig, header);
|
|
|
|
const enabledCount = countEnabledItems(sanitizedConfig[section]);
|
|
const totalAvailable = availableItems.length;
|
|
const { totalCharacters } = calculateSectionFootprint(section, sanitizedConfig[section]);
|
|
|
|
console.log(`\n${SECTION_METADATA[section].label}: ${enabledCount}/${totalAvailable} enabled.`);
|
|
if (totalCharacters > 0 && section !== "collections") {
|
|
console.log(`Estimated ${SECTION_METADATA[section].label.toLowerCase()} context size: ${formatNumber(totalCharacters)} characters.`);
|
|
}
|
|
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.");
|
|
}
|
|
}
|
|
|
|
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) {
|
|
const args = [...rawArgs];
|
|
let configPath = DEFAULT_CONFIG_PATH;
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
const arg = args[i];
|
|
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);
|
|
i -= 1;
|
|
}
|
|
}
|
|
|
|
if (args.length > 0) {
|
|
const potentialPath = args[args.length - 1];
|
|
if (isConfigFilePath(potentialPath)) {
|
|
configPath = potentialPath;
|
|
args.pop();
|
|
}
|
|
}
|
|
|
|
return { args, configPath };
|
|
}
|
|
|
|
function isConfigFilePath(value) {
|
|
if (typeof value !== "string") {
|
|
return false;
|
|
}
|
|
return value.endsWith(".yml") || value.endsWith(".yaml") || value.includes("/") || value.includes("\\");
|
|
}
|
|
|
|
function validateSectionType(input) {
|
|
const normalized = String(input || "").toLowerCase();
|
|
if (!SECTION_METADATA[normalized]) {
|
|
throw new Error(`Unknown section '${input}'. Expected one of: ${CONFIG_SECTIONS.join(", ")}.`);
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
function parseStateToken(token) {
|
|
const normalized = token.toLowerCase();
|
|
if (["on", "enable", "enabled", "true", "yes", "y"].includes(normalized)) {
|
|
return true;
|
|
}
|
|
if (["off", "disable", "disabled", "false", "no", "n"].includes(normalized)) {
|
|
return false;
|
|
}
|
|
throw new Error("State must be 'on' or 'off'.");
|
|
}
|
|
|
|
function calculateSectionFootprint(section, state = {}) {
|
|
const meta = SECTION_METADATA[section];
|
|
if (!meta || section === "collections") {
|
|
return { totalCharacters: 0 };
|
|
}
|
|
|
|
let totalCharacters = 0;
|
|
|
|
for (const [name, enabled] of Object.entries(state)) {
|
|
if (!enabled) continue;
|
|
|
|
const filePath = path.join(__dirname, meta.dir, `${name}${meta.ext}`);
|
|
try {
|
|
const stats = fs.statSync(filePath);
|
|
totalCharacters += stats.size;
|
|
} catch (error) {
|
|
// If the file no longer exists we skip it but continue gracefully.
|
|
}
|
|
}
|
|
|
|
return { totalCharacters };
|
|
}
|
|
|
|
function maybeWarnAboutContext(section, totalCharacters) {
|
|
const limit = CONTEXT_WARNING_CHAR_LIMIT[section];
|
|
if (!limit || totalCharacters <= 0) {
|
|
return;
|
|
}
|
|
|
|
if (totalCharacters >= limit) {
|
|
console.log(`⚠️ Warning: Estimated ${SECTION_METADATA[section].label.toLowerCase()} size ${formatNumber(totalCharacters)} characters exceeds the recommended limit of ${formatNumber(limit)} characters. Copilot Agent may truncate or crash.`);
|
|
} else if (totalCharacters >= limit * 0.8) {
|
|
console.log(`⚠️ Heads up: Estimated ${SECTION_METADATA[section].label.toLowerCase()} size ${formatNumber(totalCharacters)} characters is approaching the recommended limit (${formatNumber(limit)} characters).`);
|
|
}
|
|
}
|
|
|
|
function formatNumber(value) {
|
|
return numberFormatter.format(Math.round(value));
|
|
}
|
|
|
|
function findClosestMatch(target, candidates) {
|
|
const normalizedTarget = target.toLowerCase();
|
|
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() {
|
|
const args = process.argv.slice(2);
|
|
|
|
if (args.length === 0) {
|
|
showHelp();
|
|
return;
|
|
}
|
|
|
|
const command = args[0];
|
|
const commandArgs = args.slice(1);
|
|
|
|
if (!commands[command]) {
|
|
showError(`Unknown command: ${command}`);
|
|
}
|
|
|
|
try {
|
|
await commands[command].action(commandArgs);
|
|
} catch (error) {
|
|
showError(error.message);
|
|
}
|
|
}
|
|
|
|
if (require.main === module) {
|
|
main();
|
|
}
|
|
|
|
module.exports = { main };
|