feat: custom GitHub Copilot installer handler (#1606)
* feat: add custom GitHub Copilot installer handler Adds a dedicated GitHub Copilot handler that generates: - Agent files with .agent.md extension and enriched descriptions - Prompt files (.prompt.md) for workflows, tasks, and agent activators - copilot-instructions.md with project config and agent reference table Replaces the generic config-driven handler with a custom one that properly supports Copilot's agent/prompt file conventions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: escape YAML descriptions and preserve user copilot-instructions - Escape single quotes in YAML frontmatter descriptions across all prompt generators (createWorkflowPromptContent, createTechWriterPromptContent, createAgentActivatorPromptContent) to match createAgentContent behavior - Make copilot-instructions.md non-destructive using BMAD markers (<!-- BMAD:START --> / <!-- BMAD:END -->) to preserve user content - On cleanup, only remove content between markers; skip files without markers - Back up existing unmarked files before overwriting Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add error handling for CSV read/parse in loadAgentManifest and loadBmadHelp Wrap file read and csv.parse in try/catch blocks so malformed or unreadable CSV files gracefully degrade instead of aborting setup. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: use specific detection paths instead of .github configDir Set configDir to null and use detectionPaths with .github/copilot-instructions.md and .github/agents/ so the base detect() doesn't false-positive on every GitHub repo. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: add comments explaining hardcoded bmm/config.yaml in prompts Clarify that bmm/config.yaml is safe to hardcode in generated prompt content because these prompts are only created when bmm module data (bmad-help.csv, agent artifacts) exists. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: guard against missing workflow-file in bmad-help.csv entries Skip entries where workflow-file is empty/undefined to prevent workflowFile.endsWith() from throwing during prompt generation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: guard escapeYamlSingleQuote against undefined input Default to empty string when value is undefined/null to prevent replaceAll from throwing on missing CSV fields. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: preserve customised tool permissions across reinstalls Before this change, reinstalling would overwrite any user-customised tools arrays in agent and prompt frontmatter with the hardcoded default. Now the installer reads existing tool permissions from .agent.md and .prompt.md files before cleanup, and re-applies them to the regenerated files. Falls back to the default ['read', 'edit', 'search', 'execute'] for new files or files without prior customisation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: prevent cleanup from stripping copilot-instructions.md markers before generation The cleanup() method was removing the BMAD marker section from copilot-instructions.md, leaving user content without markers. generateCopilotInstructions() then treated the markerless file as legacy, backed it up, and overwrote user content. Fix: remove the copilot-instructions.md block from cleanup() entirely. generateCopilotInstructions() already handles marker-based replacement in a single read-modify-write pass that correctly preserves user content. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: update manager.js comments to include github-copilot.js Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: make agent capabilities data-driven via agent YAML metadata Replace the hardcoded getAgentCapabilities() map with a data-driven pipeline. Capabilities are now defined in each .agent.yaml source file, compiled into the XML output, extracted into agent-manifest.csv by the manifest generator, and read by the GitHub Copilot handler at install time. New agents automatically get their capabilities without code changes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: use this.bmadFolderName instead of hardcoded _bmad in template paths Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2754d66042
commit
36c21dbada
|
|
@ -5,6 +5,7 @@ agent:
|
||||||
title: Business Analyst
|
title: Business Analyst
|
||||||
icon: 📊
|
icon: 📊
|
||||||
module: bmm
|
module: bmm
|
||||||
|
capabilities: "market research, competitive analysis, requirements elicitation, domain expertise"
|
||||||
hasSidecar: false
|
hasSidecar: false
|
||||||
|
|
||||||
persona:
|
persona:
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ agent:
|
||||||
title: Architect
|
title: Architect
|
||||||
icon: 🏗️
|
icon: 🏗️
|
||||||
module: bmm
|
module: bmm
|
||||||
|
capabilities: "distributed systems, cloud infrastructure, API design, scalable patterns"
|
||||||
hasSidecar: false
|
hasSidecar: false
|
||||||
|
|
||||||
persona:
|
persona:
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ agent:
|
||||||
title: Developer Agent
|
title: Developer Agent
|
||||||
icon: 💻
|
icon: 💻
|
||||||
module: bmm
|
module: bmm
|
||||||
|
capabilities: "story execution, test-driven development, code implementation"
|
||||||
hasSidecar: false
|
hasSidecar: false
|
||||||
|
|
||||||
persona:
|
persona:
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ agent:
|
||||||
title: Product Manager
|
title: Product Manager
|
||||||
icon: 📋
|
icon: 📋
|
||||||
module: bmm
|
module: bmm
|
||||||
|
capabilities: "PRD creation, requirements discovery, stakeholder alignment, user interviews"
|
||||||
hasSidecar: false
|
hasSidecar: false
|
||||||
|
|
||||||
persona:
|
persona:
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ agent:
|
||||||
title: QA Engineer
|
title: QA Engineer
|
||||||
icon: 🧪
|
icon: 🧪
|
||||||
module: bmm
|
module: bmm
|
||||||
|
capabilities: "test automation, API testing, E2E testing, coverage analysis"
|
||||||
hasSidecar: false
|
hasSidecar: false
|
||||||
|
|
||||||
persona:
|
persona:
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ agent:
|
||||||
title: Quick Flow Solo Dev
|
title: Quick Flow Solo Dev
|
||||||
icon: 🚀
|
icon: 🚀
|
||||||
module: bmm
|
module: bmm
|
||||||
|
capabilities: "rapid spec creation, lean implementation, minimum ceremony"
|
||||||
hasSidecar: false
|
hasSidecar: false
|
||||||
|
|
||||||
persona:
|
persona:
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ agent:
|
||||||
title: Scrum Master
|
title: Scrum Master
|
||||||
icon: 🏃
|
icon: 🏃
|
||||||
module: bmm
|
module: bmm
|
||||||
|
capabilities: "sprint planning, story preparation, agile ceremonies, backlog management"
|
||||||
hasSidecar: false
|
hasSidecar: false
|
||||||
|
|
||||||
persona:
|
persona:
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ agent:
|
||||||
title: Technical Writer
|
title: Technical Writer
|
||||||
icon: 📚
|
icon: 📚
|
||||||
module: bmm
|
module: bmm
|
||||||
|
capabilities: "documentation, Mermaid diagrams, standards compliance, concept explanation"
|
||||||
hasSidecar: true
|
hasSidecar: true
|
||||||
|
|
||||||
persona:
|
persona:
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ agent:
|
||||||
title: UX Designer
|
title: UX Designer
|
||||||
icon: 🎨
|
icon: 🎨
|
||||||
module: bmm
|
module: bmm
|
||||||
|
capabilities: "user research, interaction design, UI patterns, experience strategy"
|
||||||
hasSidecar: false
|
hasSidecar: false
|
||||||
|
|
||||||
persona:
|
persona:
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ agent:
|
||||||
name: "BMad Master"
|
name: "BMad Master"
|
||||||
title: "BMad Master Executor, Knowledge Custodian, and Workflow Orchestrator"
|
title: "BMad Master Executor, Knowledge Custodian, and Workflow Orchestrator"
|
||||||
icon: "🧙"
|
icon: "🧙"
|
||||||
|
capabilities: "runtime resource management, workflow orchestration, task execution, knowledge custodian"
|
||||||
hasSidecar: false
|
hasSidecar: false
|
||||||
|
|
||||||
persona:
|
persona:
|
||||||
|
|
|
||||||
|
|
@ -321,6 +321,7 @@ class ManifestGenerator {
|
||||||
const nameMatch = content.match(/name="([^"]+)"/);
|
const nameMatch = content.match(/name="([^"]+)"/);
|
||||||
const titleMatch = content.match(/title="([^"]+)"/);
|
const titleMatch = content.match(/title="([^"]+)"/);
|
||||||
const iconMatch = content.match(/icon="([^"]+)"/);
|
const iconMatch = content.match(/icon="([^"]+)"/);
|
||||||
|
const capabilitiesMatch = content.match(/capabilities="([^"]+)"/);
|
||||||
|
|
||||||
// Extract persona fields
|
// Extract persona fields
|
||||||
const roleMatch = content.match(/<role>([^<]+)<\/role>/);
|
const roleMatch = content.match(/<role>([^<]+)<\/role>/);
|
||||||
|
|
@ -342,6 +343,7 @@ class ManifestGenerator {
|
||||||
displayName: nameMatch ? nameMatch[1] : agentName,
|
displayName: nameMatch ? nameMatch[1] : agentName,
|
||||||
title: titleMatch ? titleMatch[1] : '',
|
title: titleMatch ? titleMatch[1] : '',
|
||||||
icon: iconMatch ? iconMatch[1] : '',
|
icon: iconMatch ? iconMatch[1] : '',
|
||||||
|
capabilities: capabilitiesMatch ? this.cleanForCSV(capabilitiesMatch[1]) : '',
|
||||||
role: roleMatch ? this.cleanForCSV(roleMatch[1]) : '',
|
role: roleMatch ? this.cleanForCSV(roleMatch[1]) : '',
|
||||||
identity: identityMatch ? this.cleanForCSV(identityMatch[1]) : '',
|
identity: identityMatch ? this.cleanForCSV(identityMatch[1]) : '',
|
||||||
communicationStyle: styleMatch ? this.cleanForCSV(styleMatch[1]) : '',
|
communicationStyle: styleMatch ? this.cleanForCSV(styleMatch[1]) : '',
|
||||||
|
|
@ -784,7 +786,7 @@ class ManifestGenerator {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create CSV header with persona fields
|
// Create CSV header with persona fields
|
||||||
let csvContent = 'name,displayName,title,icon,role,identity,communicationStyle,principles,module,path\n';
|
let csvContent = 'name,displayName,title,icon,capabilities,role,identity,communicationStyle,principles,module,path\n';
|
||||||
|
|
||||||
// Combine existing and new agents, preferring new data for duplicates
|
// Combine existing and new agents, preferring new data for duplicates
|
||||||
const allAgents = new Map();
|
const allAgents = new Map();
|
||||||
|
|
@ -802,6 +804,7 @@ class ManifestGenerator {
|
||||||
displayName: agent.displayName,
|
displayName: agent.displayName,
|
||||||
title: agent.title,
|
title: agent.title,
|
||||||
icon: agent.icon,
|
icon: agent.icon,
|
||||||
|
capabilities: agent.capabilities,
|
||||||
role: agent.role,
|
role: agent.role,
|
||||||
identity: agent.identity,
|
identity: agent.identity,
|
||||||
communicationStyle: agent.communicationStyle,
|
communicationStyle: agent.communicationStyle,
|
||||||
|
|
@ -818,6 +821,7 @@ class ManifestGenerator {
|
||||||
escapeCsv(record.displayName),
|
escapeCsv(record.displayName),
|
||||||
escapeCsv(record.title),
|
escapeCsv(record.title),
|
||||||
escapeCsv(record.icon),
|
escapeCsv(record.icon),
|
||||||
|
escapeCsv(record.capabilities),
|
||||||
escapeCsv(record.role),
|
escapeCsv(record.role),
|
||||||
escapeCsv(record.identity),
|
escapeCsv(record.identity),
|
||||||
escapeCsv(record.communicationStyle),
|
escapeCsv(record.communicationStyle),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,655 @@
|
||||||
|
const path = require('node:path');
|
||||||
|
const { BaseIdeSetup } = require('./_base-ide');
|
||||||
|
const chalk = require('chalk');
|
||||||
|
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||||
|
const { BMAD_FOLDER_NAME, toDashPath } = require('./shared/path-utils');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const csv = require('csv-parse/sync');
|
||||||
|
const yaml = require('yaml');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GitHub Copilot setup handler
|
||||||
|
* Creates agents in .github/agents/, prompts in .github/prompts/,
|
||||||
|
* copilot-instructions.md, and configures VS Code settings
|
||||||
|
*/
|
||||||
|
class GitHubCopilotSetup extends BaseIdeSetup {
|
||||||
|
constructor() {
|
||||||
|
super('github-copilot', 'GitHub Copilot', false);
|
||||||
|
// Don't set configDir to '.github' — nearly every GitHub repo has that directory,
|
||||||
|
// which would cause the base detect() to false-positive. Use detectionPaths instead.
|
||||||
|
this.configDir = null;
|
||||||
|
this.githubDir = '.github';
|
||||||
|
this.agentsDir = 'agents';
|
||||||
|
this.promptsDir = 'prompts';
|
||||||
|
this.detectionPaths = ['.github/copilot-instructions.md', '.github/agents'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup GitHub Copilot configuration
|
||||||
|
* @param {string} projectDir - Project directory
|
||||||
|
* @param {string} bmadDir - BMAD installation directory
|
||||||
|
* @param {Object} options - Setup options
|
||||||
|
*/
|
||||||
|
async setup(projectDir, bmadDir, options = {}) {
|
||||||
|
console.log(chalk.cyan(`Setting up ${this.name}...`));
|
||||||
|
|
||||||
|
// Create .github/agents and .github/prompts directories
|
||||||
|
const githubDir = path.join(projectDir, this.githubDir);
|
||||||
|
const agentsDir = path.join(githubDir, this.agentsDir);
|
||||||
|
const promptsDir = path.join(githubDir, this.promptsDir);
|
||||||
|
await this.ensureDir(agentsDir);
|
||||||
|
await this.ensureDir(promptsDir);
|
||||||
|
|
||||||
|
// Preserve any customised tool permissions from existing files before cleanup
|
||||||
|
this.existingToolPermissions = await this.collectExistingToolPermissions(projectDir);
|
||||||
|
|
||||||
|
// Clean up any existing BMAD files before reinstalling
|
||||||
|
await this.cleanup(projectDir);
|
||||||
|
|
||||||
|
// Load agent manifest for enriched descriptions
|
||||||
|
const agentManifest = await this.loadAgentManifest(bmadDir);
|
||||||
|
|
||||||
|
// Generate agent launchers
|
||||||
|
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
||||||
|
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
||||||
|
|
||||||
|
// Create agent .agent.md files
|
||||||
|
let agentCount = 0;
|
||||||
|
for (const artifact of agentArtifacts) {
|
||||||
|
const agentMeta = agentManifest.get(artifact.name);
|
||||||
|
|
||||||
|
// Compute fileName first so we can look up any existing tool permissions
|
||||||
|
const dashName = toDashPath(artifact.relativePath);
|
||||||
|
const fileName = dashName.replace(/\.md$/, '.agent.md');
|
||||||
|
const toolsStr = this.getToolsForFile(fileName);
|
||||||
|
const agentContent = this.createAgentContent(artifact, agentMeta, toolsStr);
|
||||||
|
const targetPath = path.join(agentsDir, fileName);
|
||||||
|
await this.writeFile(targetPath, agentContent);
|
||||||
|
agentCount++;
|
||||||
|
|
||||||
|
console.log(chalk.green(` ✓ Created agent: ${fileName}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate prompt files from bmad-help.csv
|
||||||
|
const promptCount = await this.generatePromptFiles(projectDir, bmadDir, agentArtifacts, agentManifest);
|
||||||
|
|
||||||
|
// Generate copilot-instructions.md
|
||||||
|
await this.generateCopilotInstructions(projectDir, bmadDir, agentManifest);
|
||||||
|
|
||||||
|
console.log(chalk.green(`\n✓ ${this.name} configured:`));
|
||||||
|
console.log(chalk.dim(` - ${agentCount} agents created in .github/agents/`));
|
||||||
|
console.log(chalk.dim(` - ${promptCount} prompts created in .github/prompts/`));
|
||||||
|
console.log(chalk.dim(` - copilot-instructions.md generated`));
|
||||||
|
console.log(chalk.dim(` - Destination: .github/`));
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
results: {
|
||||||
|
agents: agentCount,
|
||||||
|
workflows: promptCount,
|
||||||
|
tasks: 0,
|
||||||
|
tools: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load agent manifest CSV into a Map keyed by agent name
|
||||||
|
* @param {string} bmadDir - BMAD installation directory
|
||||||
|
* @returns {Map} Agent metadata keyed by name
|
||||||
|
*/
|
||||||
|
async loadAgentManifest(bmadDir) {
|
||||||
|
const manifestPath = path.join(bmadDir, '_config', 'agent-manifest.csv');
|
||||||
|
const agents = new Map();
|
||||||
|
|
||||||
|
if (!(await fs.pathExists(manifestPath))) {
|
||||||
|
return agents;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const csvContent = await fs.readFile(manifestPath, 'utf8');
|
||||||
|
const records = csv.parse(csvContent, {
|
||||||
|
columns: true,
|
||||||
|
skip_empty_lines: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const record of records) {
|
||||||
|
agents.set(record.name, record);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Gracefully degrade if manifest is unreadable/malformed
|
||||||
|
}
|
||||||
|
|
||||||
|
return agents;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load bmad-help.csv to drive prompt generation
|
||||||
|
* @param {string} bmadDir - BMAD installation directory
|
||||||
|
* @returns {Array|null} Parsed CSV rows
|
||||||
|
*/
|
||||||
|
async loadBmadHelp(bmadDir) {
|
||||||
|
const helpPath = path.join(bmadDir, '_config', 'bmad-help.csv');
|
||||||
|
|
||||||
|
if (!(await fs.pathExists(helpPath))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const csvContent = await fs.readFile(helpPath, 'utf8');
|
||||||
|
return csv.parse(csvContent, {
|
||||||
|
columns: true,
|
||||||
|
skip_empty_lines: true,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Gracefully degrade if help CSV is unreadable/malformed
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create agent .agent.md content with enriched description
|
||||||
|
* @param {Object} artifact - Agent artifact from AgentCommandGenerator
|
||||||
|
* @param {Object|undefined} manifestEntry - Agent manifest entry with metadata
|
||||||
|
* @returns {string} Agent file content
|
||||||
|
*/
|
||||||
|
createAgentContent(artifact, manifestEntry, toolsStr) {
|
||||||
|
// Build enriched description from manifest metadata
|
||||||
|
let description;
|
||||||
|
if (manifestEntry) {
|
||||||
|
const persona = manifestEntry.displayName || artifact.name;
|
||||||
|
const title = manifestEntry.title || this.formatTitle(artifact.name);
|
||||||
|
const capabilities = manifestEntry.capabilities || 'agent capabilities';
|
||||||
|
description = `${persona} — ${title}: ${capabilities}`;
|
||||||
|
} else {
|
||||||
|
description = `Activates the ${this.formatTitle(artifact.name)} agent persona.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the agent file path for the activation block
|
||||||
|
const agentPath = artifact.agentPath || artifact.relativePath;
|
||||||
|
const agentFilePath = `{project-root}/${this.bmadFolderName}/${agentPath}`;
|
||||||
|
|
||||||
|
return `---
|
||||||
|
description: '${description.replaceAll("'", "''")}'
|
||||||
|
tools: ${toolsStr}
|
||||||
|
disable-model-invocation: true
|
||||||
|
---
|
||||||
|
|
||||||
|
You must fully embody this agent's persona and follow all activation instructions exactly as specified.
|
||||||
|
|
||||||
|
<agent-activation CRITICAL="TRUE">
|
||||||
|
1. LOAD the FULL agent file from ${agentFilePath}
|
||||||
|
2. READ its entire contents - this contains the complete agent persona, menu, and instructions
|
||||||
|
3. FOLLOW every step in the <activation> section precisely
|
||||||
|
4. DISPLAY the welcome/greeting as instructed
|
||||||
|
5. PRESENT the numbered menu
|
||||||
|
6. WAIT for user input before proceeding
|
||||||
|
</agent-activation>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate .prompt.md files for workflows, tasks, tech-writer commands, and agent activators
|
||||||
|
* @param {string} projectDir - Project directory
|
||||||
|
* @param {string} bmadDir - BMAD installation directory
|
||||||
|
* @param {Array} agentArtifacts - Agent artifacts for activator generation
|
||||||
|
* @param {Map} agentManifest - Agent manifest data
|
||||||
|
* @returns {number} Count of prompts generated
|
||||||
|
*/
|
||||||
|
async generatePromptFiles(projectDir, bmadDir, agentArtifacts, agentManifest) {
|
||||||
|
const promptsDir = path.join(projectDir, this.githubDir, this.promptsDir);
|
||||||
|
let promptCount = 0;
|
||||||
|
|
||||||
|
// Load bmad-help.csv to drive workflow/task prompt generation
|
||||||
|
const helpEntries = await this.loadBmadHelp(bmadDir);
|
||||||
|
|
||||||
|
if (helpEntries) {
|
||||||
|
for (const entry of helpEntries) {
|
||||||
|
const command = entry.command;
|
||||||
|
if (!command) continue; // Skip entries without a command (tech-writer commands have no command column)
|
||||||
|
|
||||||
|
const workflowFile = entry['workflow-file'];
|
||||||
|
if (!workflowFile) continue; // Skip entries with no workflow file path
|
||||||
|
const promptFileName = `${command}.prompt.md`;
|
||||||
|
const toolsStr = this.getToolsForFile(promptFileName);
|
||||||
|
const promptContent = this.createWorkflowPromptContent(entry, workflowFile, toolsStr);
|
||||||
|
const promptPath = path.join(promptsDir, promptFileName);
|
||||||
|
await this.writeFile(promptPath, promptContent);
|
||||||
|
promptCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate tech-writer command prompts (entries with no command column)
|
||||||
|
for (const entry of helpEntries) {
|
||||||
|
if (entry.command) continue; // Already handled above
|
||||||
|
const techWriterPrompt = this.createTechWriterPromptContent(entry);
|
||||||
|
if (techWriterPrompt) {
|
||||||
|
const promptFileName = `${techWriterPrompt.fileName}.prompt.md`;
|
||||||
|
const promptPath = path.join(promptsDir, promptFileName);
|
||||||
|
await this.writeFile(promptPath, techWriterPrompt.content);
|
||||||
|
promptCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate agent activator prompts (Pattern D)
|
||||||
|
for (const artifact of agentArtifacts) {
|
||||||
|
const agentMeta = agentManifest.get(artifact.name);
|
||||||
|
const fileName = `bmad-${artifact.name}.prompt.md`;
|
||||||
|
const toolsStr = this.getToolsForFile(fileName);
|
||||||
|
const promptContent = this.createAgentActivatorPromptContent(artifact, agentMeta, toolsStr);
|
||||||
|
const promptPath = path.join(promptsDir, fileName);
|
||||||
|
await this.writeFile(promptPath, promptContent);
|
||||||
|
promptCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return promptCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create prompt content for a workflow/task entry from bmad-help.csv
|
||||||
|
* Determines the pattern (A, B, or A for .xml tasks) based on file extension
|
||||||
|
* @param {Object} entry - bmad-help.csv row
|
||||||
|
* @param {string} workflowFile - Workflow file path
|
||||||
|
* @returns {string} Prompt file content
|
||||||
|
*/
|
||||||
|
createWorkflowPromptContent(entry, workflowFile, toolsStr) {
|
||||||
|
const description = this.escapeYamlSingleQuote(this.createPromptDescription(entry.name));
|
||||||
|
// bmm/config.yaml is safe to hardcode here: these prompts are only generated when
|
||||||
|
// bmad-help.csv exists (bmm module data), so bmm is guaranteed to be installed.
|
||||||
|
const configLine = `1. Load {project-root}/${this.bmadFolderName}/bmm/config.yaml and store ALL fields as session variables`;
|
||||||
|
|
||||||
|
let body;
|
||||||
|
if (workflowFile.endsWith('.yaml')) {
|
||||||
|
// Pattern B: YAML-based workflows — use workflow engine
|
||||||
|
body = `${configLine}
|
||||||
|
2. Load the workflow engine at {project-root}/${this.bmadFolderName}/core/tasks/workflow.xml
|
||||||
|
3. Load and execute the workflow configuration at {project-root}/${workflowFile} using the engine from step 2`;
|
||||||
|
} else if (workflowFile.endsWith('.xml')) {
|
||||||
|
// Pattern A variant: XML tasks — load and execute directly
|
||||||
|
body = `${configLine}
|
||||||
|
2. Load and execute the task at {project-root}/${workflowFile}`;
|
||||||
|
} else {
|
||||||
|
// Pattern A: MD workflows — load and follow directly
|
||||||
|
body = `${configLine}
|
||||||
|
2. Load and follow the workflow at {project-root}/${workflowFile}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `---
|
||||||
|
description: '${description}'
|
||||||
|
agent: 'agent'
|
||||||
|
tools: ${toolsStr}
|
||||||
|
---
|
||||||
|
|
||||||
|
${body}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a short 2-5 word description for a prompt from the entry name
|
||||||
|
* @param {string} name - Entry name from bmad-help.csv
|
||||||
|
* @returns {string} Short description
|
||||||
|
*/
|
||||||
|
createPromptDescription(name) {
|
||||||
|
const descriptionMap = {
|
||||||
|
'Brainstorm Project': 'Brainstorm ideas',
|
||||||
|
'Market Research': 'Market research',
|
||||||
|
'Domain Research': 'Domain research',
|
||||||
|
'Technical Research': 'Technical research',
|
||||||
|
'Create Brief': 'Create product brief',
|
||||||
|
'Create PRD': 'Create PRD',
|
||||||
|
'Validate PRD': 'Validate PRD',
|
||||||
|
'Edit PRD': 'Edit PRD',
|
||||||
|
'Create UX': 'Create UX design',
|
||||||
|
'Create Architecture': 'Create architecture',
|
||||||
|
'Create Epics and Stories': 'Create epics and stories',
|
||||||
|
'Check Implementation Readiness': 'Check implementation readiness',
|
||||||
|
'Sprint Planning': 'Sprint planning',
|
||||||
|
'Sprint Status': 'Sprint status',
|
||||||
|
'Create Story': 'Create story',
|
||||||
|
'Validate Story': 'Validate story',
|
||||||
|
'Dev Story': 'Dev story',
|
||||||
|
'QA Automation Test': 'QA automation',
|
||||||
|
'Code Review': 'Code review',
|
||||||
|
Retrospective: 'Retrospective',
|
||||||
|
'Document Project': 'Document project',
|
||||||
|
'Generate Project Context': 'Generate project context',
|
||||||
|
'Quick Spec': 'Quick spec',
|
||||||
|
'Quick Dev': 'Quick dev',
|
||||||
|
'Correct Course': 'Correct course',
|
||||||
|
Brainstorming: 'Brainstorm ideas',
|
||||||
|
'Party Mode': 'Party mode',
|
||||||
|
'bmad-help': 'BMAD help',
|
||||||
|
'Index Docs': 'Index documents',
|
||||||
|
'Shard Document': 'Shard document',
|
||||||
|
'Editorial Review - Prose': 'Editorial review prose',
|
||||||
|
'Editorial Review - Structure': 'Editorial review structure',
|
||||||
|
'Adversarial Review (General)': 'Adversarial review',
|
||||||
|
};
|
||||||
|
|
||||||
|
return descriptionMap[name] || name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create prompt content for tech-writer agent-only commands (Pattern C)
|
||||||
|
* @param {Object} entry - bmad-help.csv row
|
||||||
|
* @returns {Object|null} { fileName, content } or null if not a tech-writer command
|
||||||
|
*/
|
||||||
|
createTechWriterPromptContent(entry) {
|
||||||
|
if (entry['agent-name'] !== 'tech-writer') return null;
|
||||||
|
|
||||||
|
const techWriterCommands = {
|
||||||
|
'Write Document': { code: 'WD', file: 'bmad-bmm-write-document', description: 'Write document' },
|
||||||
|
'Update Standards': { code: 'US', file: 'bmad-bmm-update-standards', description: 'Update standards' },
|
||||||
|
'Mermaid Generate': { code: 'MG', file: 'bmad-bmm-mermaid-generate', description: 'Mermaid generate' },
|
||||||
|
'Validate Document': { code: 'VD', file: 'bmad-bmm-validate-document', description: 'Validate document' },
|
||||||
|
'Explain Concept': { code: 'EC', file: 'bmad-bmm-explain-concept', description: 'Explain concept' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const cmd = techWriterCommands[entry.name];
|
||||||
|
if (!cmd) return null;
|
||||||
|
|
||||||
|
const safeDescription = this.escapeYamlSingleQuote(cmd.description);
|
||||||
|
const toolsStr = this.getToolsForFile(`${cmd.file}.prompt.md`);
|
||||||
|
|
||||||
|
const content = `---
|
||||||
|
description: '${safeDescription}'
|
||||||
|
agent: 'agent'
|
||||||
|
tools: ${toolsStr}
|
||||||
|
---
|
||||||
|
|
||||||
|
1. Load {project-root}/${this.bmadFolderName}/bmm/config.yaml and store ALL fields as session variables
|
||||||
|
2. Load the full agent file from {project-root}/${this.bmadFolderName}/bmm/agents/tech-writer/tech-writer.md and activate the Paige (Technical Writer) persona
|
||||||
|
3. Execute the ${entry.name} menu command (${cmd.code})
|
||||||
|
`;
|
||||||
|
|
||||||
|
return { fileName: cmd.file, content };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create agent activator prompt content (Pattern D)
|
||||||
|
* @param {Object} artifact - Agent artifact
|
||||||
|
* @param {Object|undefined} manifestEntry - Agent manifest entry
|
||||||
|
* @returns {string} Prompt file content
|
||||||
|
*/
|
||||||
|
createAgentActivatorPromptContent(artifact, manifestEntry, toolsStr) {
|
||||||
|
let description;
|
||||||
|
if (manifestEntry) {
|
||||||
|
description = manifestEntry.title || this.formatTitle(artifact.name);
|
||||||
|
} else {
|
||||||
|
description = this.formatTitle(artifact.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const safeDescription = this.escapeYamlSingleQuote(description);
|
||||||
|
const agentPath = artifact.agentPath || artifact.relativePath;
|
||||||
|
const agentFilePath = `{project-root}/${this.bmadFolderName}/${agentPath}`;
|
||||||
|
|
||||||
|
// bmm/config.yaml is safe to hardcode: agent activators are only generated from
|
||||||
|
// bmm agent artifacts, so bmm is guaranteed to be installed.
|
||||||
|
return `---
|
||||||
|
description: '${safeDescription}'
|
||||||
|
agent: 'agent'
|
||||||
|
tools: ${toolsStr}
|
||||||
|
---
|
||||||
|
|
||||||
|
1. Load {project-root}/${this.bmadFolderName}/bmm/config.yaml and store ALL fields as session variables
|
||||||
|
2. Load the full agent file from ${agentFilePath}
|
||||||
|
3. Follow ALL activation instructions in the agent file
|
||||||
|
4. Display the welcome/greeting as instructed
|
||||||
|
5. Present the numbered menu
|
||||||
|
6. Wait for user input before proceeding
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate copilot-instructions.md from module config
|
||||||
|
* @param {string} projectDir - Project directory
|
||||||
|
* @param {string} bmadDir - BMAD installation directory
|
||||||
|
* @param {Map} agentManifest - Agent manifest data
|
||||||
|
*/
|
||||||
|
async generateCopilotInstructions(projectDir, bmadDir, agentManifest) {
|
||||||
|
const configVars = await this.loadModuleConfig(bmadDir);
|
||||||
|
|
||||||
|
// Build the agents table from the manifest
|
||||||
|
let agentsTable = '| Agent | Persona | Title | Capabilities |\n|---|---|---|---|\n';
|
||||||
|
const agentOrder = [
|
||||||
|
'bmad-master',
|
||||||
|
'analyst',
|
||||||
|
'architect',
|
||||||
|
'dev',
|
||||||
|
'pm',
|
||||||
|
'qa',
|
||||||
|
'quick-flow-solo-dev',
|
||||||
|
'sm',
|
||||||
|
'tech-writer',
|
||||||
|
'ux-designer',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const agentName of agentOrder) {
|
||||||
|
const meta = agentManifest.get(agentName);
|
||||||
|
if (meta) {
|
||||||
|
const capabilities = meta.capabilities || 'agent capabilities';
|
||||||
|
const cleanTitle = (meta.title || '').replaceAll('""', '"');
|
||||||
|
agentsTable += `| ${agentName} | ${meta.displayName} | ${cleanTitle} | ${capabilities} |\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bmad = this.bmadFolderName;
|
||||||
|
const bmadSection = `# BMAD Method — Project Instructions
|
||||||
|
|
||||||
|
## Project Configuration
|
||||||
|
|
||||||
|
- **Project**: ${configVars.project_name || '{{project_name}}'}
|
||||||
|
- **User**: ${configVars.user_name || '{{user_name}}'}
|
||||||
|
- **Communication Language**: ${configVars.communication_language || '{{communication_language}}'}
|
||||||
|
- **Document Output Language**: ${configVars.document_output_language || '{{document_output_language}}'}
|
||||||
|
- **User Skill Level**: ${configVars.user_skill_level || '{{user_skill_level}}'}
|
||||||
|
- **Output Folder**: ${configVars.output_folder || '{{output_folder}}'}
|
||||||
|
- **Planning Artifacts**: ${configVars.planning_artifacts || '{{planning_artifacts}}'}
|
||||||
|
- **Implementation Artifacts**: ${configVars.implementation_artifacts || '{{implementation_artifacts}}'}
|
||||||
|
- **Project Knowledge**: ${configVars.project_knowledge || '{{project_knowledge}}'}
|
||||||
|
|
||||||
|
## BMAD Runtime Structure
|
||||||
|
|
||||||
|
- **Agent definitions**: \`${bmad}/bmm/agents/\` (BMM module) and \`${bmad}/core/agents/\` (core)
|
||||||
|
- **Workflow definitions**: \`${bmad}/bmm/workflows/\` (organized by phase)
|
||||||
|
- **Core tasks**: \`${bmad}/core/tasks/\` (help, editorial review, indexing, sharding, adversarial review)
|
||||||
|
- **Core workflows**: \`${bmad}/core/workflows/\` (brainstorming, party-mode, advanced-elicitation)
|
||||||
|
- **Workflow engine**: \`${bmad}/core/tasks/workflow.xml\` (executes YAML-based workflows)
|
||||||
|
- **Module configuration**: \`${bmad}/bmm/config.yaml\`
|
||||||
|
- **Core configuration**: \`${bmad}/core/config.yaml\`
|
||||||
|
- **Agent manifest**: \`${bmad}/_config/agent-manifest.csv\`
|
||||||
|
- **Workflow manifest**: \`${bmad}/_config/workflow-manifest.csv\`
|
||||||
|
- **Help manifest**: \`${bmad}/_config/bmad-help.csv\`
|
||||||
|
- **Agent memory**: \`${bmad}/_memory/\`
|
||||||
|
|
||||||
|
## Key Conventions
|
||||||
|
|
||||||
|
- Always load \`${bmad}/bmm/config.yaml\` before any agent activation or workflow execution
|
||||||
|
- Store all config fields as session variables: \`{user_name}\`, \`{communication_language}\`, \`{output_folder}\`, \`{planning_artifacts}\`, \`{implementation_artifacts}\`, \`{project_knowledge}\`
|
||||||
|
- MD-based workflows execute directly — load and follow the \`.md\` file
|
||||||
|
- YAML-based workflows require the workflow engine — load \`workflow.xml\` first, then pass the \`.yaml\` config
|
||||||
|
- Follow step-based workflow execution: load steps JIT, never multiple at once
|
||||||
|
- Save outputs after EACH step when using the workflow engine
|
||||||
|
- The \`{project-root}\` variable resolves to the workspace root at runtime
|
||||||
|
|
||||||
|
## Available Agents
|
||||||
|
|
||||||
|
${agentsTable}
|
||||||
|
## Slash Commands
|
||||||
|
|
||||||
|
Type \`/bmad-\` in Copilot Chat to see all available BMAD workflows and agent activators. Agents are also available in the agents dropdown.`;
|
||||||
|
|
||||||
|
const instructionsPath = path.join(projectDir, this.githubDir, 'copilot-instructions.md');
|
||||||
|
const markerStart = '<!-- BMAD:START -->';
|
||||||
|
const markerEnd = '<!-- BMAD:END -->';
|
||||||
|
const markedContent = `${markerStart}\n${bmadSection}\n${markerEnd}`;
|
||||||
|
|
||||||
|
if (await fs.pathExists(instructionsPath)) {
|
||||||
|
const existing = await fs.readFile(instructionsPath, 'utf8');
|
||||||
|
const startIdx = existing.indexOf(markerStart);
|
||||||
|
const endIdx = existing.indexOf(markerEnd);
|
||||||
|
|
||||||
|
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
||||||
|
// Replace only the BMAD section between markers
|
||||||
|
const before = existing.slice(0, startIdx);
|
||||||
|
const after = existing.slice(endIdx + markerEnd.length);
|
||||||
|
const merged = `${before}${markedContent}${after}`;
|
||||||
|
await this.writeFile(instructionsPath, merged);
|
||||||
|
console.log(chalk.green(' ✓ Updated BMAD section in copilot-instructions.md'));
|
||||||
|
} else {
|
||||||
|
// Existing file without markers — back it up before overwriting
|
||||||
|
const backupPath = `${instructionsPath}.bak`;
|
||||||
|
await fs.copy(instructionsPath, backupPath);
|
||||||
|
console.log(chalk.yellow(` ⚠ Backed up existing copilot-instructions.md → copilot-instructions.md.bak`));
|
||||||
|
await this.writeFile(instructionsPath, `${markedContent}\n`);
|
||||||
|
console.log(chalk.green(' ✓ Generated copilot-instructions.md (with BMAD markers)'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No existing file — create fresh with markers
|
||||||
|
await this.writeFile(instructionsPath, `${markedContent}\n`);
|
||||||
|
console.log(chalk.green(' ✓ Generated copilot-instructions.md'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load module config.yaml for template variables
|
||||||
|
* @param {string} bmadDir - BMAD installation directory
|
||||||
|
* @returns {Object} Config variables
|
||||||
|
*/
|
||||||
|
async loadModuleConfig(bmadDir) {
|
||||||
|
const bmmConfigPath = path.join(bmadDir, 'bmm', 'config.yaml');
|
||||||
|
const coreConfigPath = path.join(bmadDir, 'core', 'config.yaml');
|
||||||
|
|
||||||
|
for (const configPath of [bmmConfigPath, coreConfigPath]) {
|
||||||
|
if (await fs.pathExists(configPath)) {
|
||||||
|
try {
|
||||||
|
const content = await fs.readFile(configPath, 'utf8');
|
||||||
|
return yaml.parse(content) || {};
|
||||||
|
} catch {
|
||||||
|
// Fall through to next config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape a string for use inside YAML single-quoted values.
|
||||||
|
* In YAML, the only escape inside single quotes is '' for a literal '.
|
||||||
|
* @param {string} value - Raw string
|
||||||
|
* @returns {string} Escaped string safe for YAML single-quoted context
|
||||||
|
*/
|
||||||
|
escapeYamlSingleQuote(value) {
|
||||||
|
return (value || '').replaceAll("'", "''");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scan existing agent and prompt files for customised tool permissions before cleanup.
|
||||||
|
* Returns a Map<filename, toolsArray> so permissions can be preserved across reinstalls.
|
||||||
|
* @param {string} projectDir - Project directory
|
||||||
|
* @returns {Map} Existing tool permissions keyed by filename
|
||||||
|
*/
|
||||||
|
async collectExistingToolPermissions(projectDir) {
|
||||||
|
const permissions = new Map();
|
||||||
|
const dirs = [
|
||||||
|
[path.join(projectDir, this.githubDir, this.agentsDir), /^bmad.*\.agent\.md$/],
|
||||||
|
[path.join(projectDir, this.githubDir, this.promptsDir), /^bmad-.*\.prompt\.md$/],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [dir, pattern] of dirs) {
|
||||||
|
if (!(await fs.pathExists(dir))) continue;
|
||||||
|
const files = await fs.readdir(dir);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
if (!pattern.test(file)) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = await fs.readFile(path.join(dir, file), 'utf8');
|
||||||
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
||||||
|
if (!fmMatch) continue;
|
||||||
|
|
||||||
|
const frontmatter = yaml.parse(fmMatch[1]);
|
||||||
|
if (frontmatter && Array.isArray(frontmatter.tools)) {
|
||||||
|
permissions.set(file, frontmatter.tools);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Skip unreadable files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the tools array string for a file, preserving any existing customisation.
|
||||||
|
* Falls back to the default tools if no prior customisation exists.
|
||||||
|
* @param {string} fileName - Target filename (e.g. 'bmad-agent-bmm-pm.agent.md')
|
||||||
|
* @returns {string} YAML inline array string
|
||||||
|
*/
|
||||||
|
getToolsForFile(fileName) {
|
||||||
|
const defaultTools = ['read', 'edit', 'search', 'execute'];
|
||||||
|
const tools = (this.existingToolPermissions && this.existingToolPermissions.get(fileName)) || defaultTools;
|
||||||
|
return '[' + tools.map((t) => `'${t}'`).join(', ') + ']';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format name as title
|
||||||
|
*/
|
||||||
|
formatTitle(name) {
|
||||||
|
return name
|
||||||
|
.split('-')
|
||||||
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup GitHub Copilot configuration - surgically remove only BMAD files
|
||||||
|
*/
|
||||||
|
async cleanup(projectDir) {
|
||||||
|
// Clean up agents directory
|
||||||
|
const agentsDir = path.join(projectDir, this.githubDir, this.agentsDir);
|
||||||
|
if (await fs.pathExists(agentsDir)) {
|
||||||
|
const files = await fs.readdir(agentsDir);
|
||||||
|
let removed = 0;
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.startsWith('bmad') && (file.endsWith('.agent.md') || file.endsWith('.md'))) {
|
||||||
|
await fs.remove(path.join(agentsDir, file));
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removed > 0) {
|
||||||
|
console.log(chalk.dim(` Cleaned up ${removed} existing BMAD agents`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up prompts directory
|
||||||
|
const promptsDir = path.join(projectDir, this.githubDir, this.promptsDir);
|
||||||
|
if (await fs.pathExists(promptsDir)) {
|
||||||
|
const files = await fs.readdir(promptsDir);
|
||||||
|
let removed = 0;
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.startsWith('bmad-') && file.endsWith('.prompt.md')) {
|
||||||
|
await fs.remove(path.join(promptsDir, file));
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removed > 0) {
|
||||||
|
console.log(chalk.dim(` Cleaned up ${removed} existing BMAD prompts`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: copilot-instructions.md is NOT cleaned up here.
|
||||||
|
// generateCopilotInstructions() handles marker-based replacement in a single
|
||||||
|
// read-modify-write pass, which correctly preserves user content outside the markers.
|
||||||
|
// Stripping markers here would cause generation to treat the file as legacy (no markers)
|
||||||
|
// and overwrite user content.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { GitHubCopilotSetup };
|
||||||
|
|
@ -8,7 +8,7 @@ const prompts = require('../../../lib/prompts');
|
||||||
* Dynamically discovers and loads IDE handlers
|
* Dynamically discovers and loads IDE handlers
|
||||||
*
|
*
|
||||||
* Loading strategy:
|
* Loading strategy:
|
||||||
* 1. Custom installer files (codex.js, kilo.js) - for platforms with unique installation logic
|
* 1. Custom installer files (codex.js, github-copilot.js, kilo.js) - for platforms with unique installation logic
|
||||||
* 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns
|
* 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns
|
||||||
*/
|
*/
|
||||||
class IdeManager {
|
class IdeManager {
|
||||||
|
|
@ -44,7 +44,7 @@ class IdeManager {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dynamically load all IDE handlers
|
* Dynamically load all IDE handlers
|
||||||
* 1. Load custom installer files first (codex.js, kilo.js)
|
* 1. Load custom installer files first (codex.js, github-copilot.js, kilo.js)
|
||||||
* 2. Load config-driven handlers from platform-codes.yaml
|
* 2. Load config-driven handlers from platform-codes.yaml
|
||||||
*/
|
*/
|
||||||
async loadHandlers() {
|
async loadHandlers() {
|
||||||
|
|
@ -61,7 +61,7 @@ class IdeManager {
|
||||||
*/
|
*/
|
||||||
async loadCustomInstallerFiles() {
|
async loadCustomInstallerFiles() {
|
||||||
const ideDir = __dirname;
|
const ideDir = __dirname;
|
||||||
const customFiles = ['codex.js', 'kilo.js'];
|
const customFiles = ['codex.js', 'github-copilot.js', 'kilo.js'];
|
||||||
|
|
||||||
for (const file of customFiles) {
|
for (const file of customFiles) {
|
||||||
const filePath = path.join(ideDir, file);
|
const filePath = path.join(ideDir, file);
|
||||||
|
|
|
||||||
|
|
@ -89,11 +89,7 @@ platforms:
|
||||||
preferred: false
|
preferred: false
|
||||||
category: ide
|
category: ide
|
||||||
description: "GitHub's AI pair programmer"
|
description: "GitHub's AI pair programmer"
|
||||||
installer:
|
# No installer config - uses custom github-copilot.js
|
||||||
targets:
|
|
||||||
- target_dir: .github/agents
|
|
||||||
template_type: copilot_agents
|
|
||||||
artifact_types: [agents]
|
|
||||||
|
|
||||||
iflow:
|
iflow:
|
||||||
name: "iFlow"
|
name: "iFlow"
|
||||||
|
|
|
||||||
|
|
@ -279,6 +279,9 @@ async function compileToXml(agentYaml, agentName = '', targetPath = '') {
|
||||||
`title="${meta.title || ''}"`,
|
`title="${meta.title || ''}"`,
|
||||||
`icon="${meta.icon || '🤖'}"`,
|
`icon="${meta.icon || '🤖'}"`,
|
||||||
];
|
];
|
||||||
|
if (meta.capabilities) {
|
||||||
|
agentAttrs.push(`capabilities="${escapeXml(meta.capabilities)}"`);
|
||||||
|
}
|
||||||
|
|
||||||
xml += `<agent ${agentAttrs.join(' ')}>\n`;
|
xml += `<agent ${agentAttrs.join(' ')}>\n`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -228,6 +228,7 @@ function buildMetadataSchema(expectedModule) {
|
||||||
title: createNonEmptyString('agent.metadata.title'),
|
title: createNonEmptyString('agent.metadata.title'),
|
||||||
icon: createNonEmptyString('agent.metadata.icon'),
|
icon: createNonEmptyString('agent.metadata.icon'),
|
||||||
module: createNonEmptyString('agent.metadata.module').optional(),
|
module: createNonEmptyString('agent.metadata.module').optional(),
|
||||||
|
capabilities: createNonEmptyString('agent.metadata.capabilities').optional(),
|
||||||
hasSidecar: z.boolean(),
|
hasSidecar: z.boolean(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue