Compare commits
No commits in common. "434e7efab6a40e501af2ea1beccddaca931158cd" and "09ce8559f28da9961cc02eb78f46dcf130565a66" have entirely different histories.
434e7efab6
...
09ce8559f2
|
|
@ -1,39 +0,0 @@
|
|||
analyst.agent.yaml:
|
||||
canonicalId: bmad-analyst
|
||||
type: agent
|
||||
description: "Business Analyst for market research, competitive analysis, and requirements elicitation"
|
||||
|
||||
architect.agent.yaml:
|
||||
canonicalId: bmad-architect
|
||||
type: agent
|
||||
description: "Architect for distributed systems, cloud infrastructure, and API design"
|
||||
|
||||
dev.agent.yaml:
|
||||
canonicalId: bmad-dev
|
||||
type: agent
|
||||
description: "Developer Agent for story execution, test-driven development, and code implementation"
|
||||
|
||||
pm.agent.yaml:
|
||||
canonicalId: bmad-pm
|
||||
type: agent
|
||||
description: "Product Manager for PRD creation, requirements discovery, and stakeholder alignment"
|
||||
|
||||
qa.agent.yaml:
|
||||
canonicalId: bmad-qa
|
||||
type: agent
|
||||
description: "QA Engineer for test automation, API testing, and E2E testing"
|
||||
|
||||
quick-flow-solo-dev.agent.yaml:
|
||||
canonicalId: bmad-quick-flow-solo-dev
|
||||
type: agent
|
||||
description: "Quick Flow Solo Dev for rapid spec creation and lean implementation"
|
||||
|
||||
sm.agent.yaml:
|
||||
canonicalId: bmad-sm
|
||||
type: agent
|
||||
description: "Scrum Master for sprint planning, story preparation, and agile ceremonies"
|
||||
|
||||
ux-designer.agent.yaml:
|
||||
canonicalId: bmad-ux-designer
|
||||
type: agent
|
||||
description: "UX Designer for user research, interaction design, and UI patterns"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
canonicalId: bmad-tech-writer
|
||||
type: agent
|
||||
description: "Technical Writer for documentation, Mermaid diagrams, and standards compliance"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
canonicalId: bmad-create-product-brief
|
||||
type: workflow
|
||||
description: "Create product brief through collaborative discovery"
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
workflow-domain-research.md:
|
||||
canonicalId: bmad-bmm-domain-research
|
||||
type: workflow
|
||||
description: "Conduct domain and industry research"
|
||||
|
||||
workflow-market-research.md:
|
||||
canonicalId: bmad-bmm-market-research
|
||||
type: workflow
|
||||
description: "Conduct market research on competition and customers"
|
||||
|
||||
workflow-technical-research.md:
|
||||
canonicalId: bmad-bmm-technical-research
|
||||
type: workflow
|
||||
description: "Conduct technical research on technologies and architecture"
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
workflow-create-prd.md:
|
||||
canonicalId: bmad-bmm-create-prd
|
||||
type: workflow
|
||||
description: "Create a PRD from scratch"
|
||||
|
||||
workflow-edit-prd.md:
|
||||
canonicalId: bmad-bmm-edit-prd
|
||||
type: workflow
|
||||
description: "Edit an existing PRD"
|
||||
|
||||
workflow-validate-prd.md:
|
||||
canonicalId: bmad-bmm-validate-prd
|
||||
type: workflow
|
||||
description: "Validate a PRD against standards"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
canonicalId: bmad-create-ux-design
|
||||
type: workflow
|
||||
description: "Plan UX patterns and design specifications"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
canonicalId: bmad-check-implementation-readiness
|
||||
type: workflow
|
||||
description: "Validate PRD, UX, Architecture and Epics specs are complete"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
canonicalId: bmad-create-architecture
|
||||
type: workflow
|
||||
description: "Create architecture solution design decisions for AI agent consistency"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
canonicalId: bmad-create-epics-and-stories
|
||||
type: workflow
|
||||
description: "Break requirements into epics and user stories"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
canonicalId: bmad-code-review
|
||||
type: workflow
|
||||
description: "Perform adversarial code review finding specific issues"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
canonicalId: bmad-correct-course
|
||||
type: workflow
|
||||
description: "Manage significant changes during sprint execution"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
canonicalId: bmad-create-story
|
||||
type: workflow
|
||||
description: "Creates a dedicated story file with all the context needed for implementation"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
canonicalId: bmad-dev-story
|
||||
type: workflow
|
||||
description: "Execute story implementation following a context-filled story spec file"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
canonicalId: bmad-retrospective
|
||||
type: workflow
|
||||
description: "Post-epic review to extract lessons and assess success"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
canonicalId: bmad-sprint-planning
|
||||
type: workflow
|
||||
description: "Generate sprint status tracking from epics"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
canonicalId: bmad-sprint-status
|
||||
type: workflow
|
||||
description: "Summarize sprint status and surface risks"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
canonicalId: bmad-quick-dev-new-preview
|
||||
type: workflow
|
||||
description: "Unified quick flow - clarify intent, plan, implement, review, present"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
canonicalId: bmad-quick-dev
|
||||
type: workflow
|
||||
description: "Implement a Quick Tech Spec for small changes or features"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
canonicalId: bmad-quick-spec
|
||||
type: workflow
|
||||
description: "Very quick process to create implementation-ready quick specs for small changes or features"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
canonicalId: bmad-document-project
|
||||
type: workflow
|
||||
description: "Document brownfield projects for AI context"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
canonicalId: bmad-generate-project-context
|
||||
type: workflow
|
||||
description: "Create project-context.md with AI rules"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
canonicalId: bmad-qa-generate-e2e-tests
|
||||
type: workflow
|
||||
description: "Generate end-to-end automated tests for existing features"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
canonicalId: bmad-master
|
||||
type: agent
|
||||
description: "BMad Master Executor, Knowledge Custodian, and Workflow Orchestrator"
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
editorial-review-prose.xml:
|
||||
canonicalId: bmad-editorial-review-prose
|
||||
type: task
|
||||
description: "Clinical copy-editor that reviews text for communication issues"
|
||||
|
||||
editorial-review-structure.xml:
|
||||
canonicalId: bmad-editorial-review-structure
|
||||
type: task
|
||||
description: "Structural editor that proposes cuts, reorganization, and simplification while preserving comprehension"
|
||||
|
||||
help.md:
|
||||
canonicalId: bmad-help
|
||||
type: task
|
||||
description: "Analyzes what is done and the users query and offers advice on what to do next"
|
||||
|
||||
index-docs.xml:
|
||||
canonicalId: bmad-index-docs
|
||||
type: task
|
||||
description: "Generates or updates an index.md to reference all docs in the folder"
|
||||
|
||||
review-adversarial-general.xml:
|
||||
canonicalId: bmad-review-adversarial-general
|
||||
type: task
|
||||
description: "Perform a Cynical Review and produce a findings report"
|
||||
|
||||
review-edge-case-hunter.xml:
|
||||
canonicalId: bmad-review-edge-case-hunter
|
||||
type: task
|
||||
description: "Walk every branching path and boundary condition in content, report only unhandled edge cases"
|
||||
|
||||
shard-doc.xml:
|
||||
canonicalId: bmad-shard-doc
|
||||
type: task
|
||||
description: "Splits large markdown documents into smaller, organized files based on sections"
|
||||
|
||||
workflow.xml:
|
||||
canonicalId: bmad-workflow
|
||||
type: task
|
||||
description: "Execute given workflow by loading its configuration and following instructions"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
canonicalId: bmad-brainstorming
|
||||
type: workflow
|
||||
description: "Facilitate interactive brainstorming sessions using diverse creative techniques and ideation methods"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
canonicalId: bmad-party-mode
|
||||
type: workflow
|
||||
description: "Orchestrates group discussions between all installed BMAD agents"
|
||||
|
|
@ -12,12 +12,9 @@
|
|||
*/
|
||||
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const fs = require('fs-extra');
|
||||
const { YamlXmlBuilder } = require('../tools/cli/lib/yaml-xml-builder');
|
||||
const { ManifestGenerator } = require('../tools/cli/installers/lib/core/manifest-generator');
|
||||
const { IdeManager } = require('../tools/cli/installers/lib/ide/manager');
|
||||
const { clearCache, loadPlatformCodes } = require('../tools/cli/installers/lib/ide/platform-codes');
|
||||
|
||||
// ANSI colors
|
||||
const colors = {
|
||||
|
|
@ -48,39 +45,6 @@ function assert(condition, testName, errorMessage = '') {
|
|||
}
|
||||
}
|
||||
|
||||
async function createTestBmadFixture() {
|
||||
const fixtureDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-fixture-'));
|
||||
|
||||
// Minimal workflow manifest (generators check for this)
|
||||
await fs.ensureDir(path.join(fixtureDir, '_config'));
|
||||
await fs.writeFile(path.join(fixtureDir, '_config', 'workflow-manifest.csv'), '');
|
||||
|
||||
// Minimal compiled agent for core/agents (contains <agent tag and frontmatter)
|
||||
const minimalAgent = [
|
||||
'---',
|
||||
'name: "test agent"',
|
||||
'description: "Minimal test agent fixture"',
|
||||
'---',
|
||||
'',
|
||||
'You are a test agent.',
|
||||
'',
|
||||
'<agent id="test-agent.agent.yaml" name="Test Agent" title="Test Agent">',
|
||||
'<persona>Test persona</persona>',
|
||||
'</agent>',
|
||||
].join('\n');
|
||||
|
||||
await fs.ensureDir(path.join(fixtureDir, 'core', 'agents'));
|
||||
await fs.writeFile(path.join(fixtureDir, 'core', 'agents', 'bmad-master.md'), minimalAgent);
|
||||
// Skill manifest so the installer uses 'bmad-master' as the canonical skill name
|
||||
await fs.writeFile(path.join(fixtureDir, 'core', 'agents', 'bmad-skill-manifest.yaml'), 'bmad-master.md:\n canonicalId: bmad-master\n');
|
||||
|
||||
// Minimal compiled agent for bmm module (tests use selectedModules: ['bmm'])
|
||||
await fs.ensureDir(path.join(fixtureDir, 'bmm', 'agents'));
|
||||
await fs.writeFile(path.join(fixtureDir, 'bmm', 'agents', 'test-bmm-agent.md'), minimalAgent);
|
||||
|
||||
return fixtureDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Suite
|
||||
*/
|
||||
|
|
@ -194,311 +158,9 @@ async function runTests() {
|
|||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Test 4: Windsurf Native Skills Install
|
||||
// Test 5: QA Agent Compilation
|
||||
// ============================================================
|
||||
console.log(`${colors.yellow}Test Suite 4: Windsurf Native Skills${colors.reset}\n`);
|
||||
|
||||
try {
|
||||
clearCache();
|
||||
const platformCodes = await loadPlatformCodes();
|
||||
const windsurfInstaller = platformCodes.platforms.windsurf?.installer;
|
||||
|
||||
assert(windsurfInstaller?.target_dir === '.windsurf/skills', 'Windsurf target_dir uses native skills path');
|
||||
|
||||
assert(windsurfInstaller?.skill_format === true, 'Windsurf installer enables native skill output');
|
||||
|
||||
assert(
|
||||
Array.isArray(windsurfInstaller?.legacy_targets) && windsurfInstaller.legacy_targets.includes('.windsurf/workflows'),
|
||||
'Windsurf installer cleans legacy workflow output',
|
||||
);
|
||||
|
||||
const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-windsurf-test-'));
|
||||
const installedBmadDir = await createTestBmadFixture();
|
||||
const legacyDir = path.join(tempProjectDir, '.windsurf', 'workflows', 'bmad-legacy-dir');
|
||||
await fs.ensureDir(legacyDir);
|
||||
await fs.writeFile(path.join(tempProjectDir, '.windsurf', 'workflows', 'bmad-legacy.md'), 'legacy\n');
|
||||
await fs.writeFile(path.join(legacyDir, 'SKILL.md'), 'legacy\n');
|
||||
|
||||
const ideManager = new IdeManager();
|
||||
await ideManager.ensureInitialized();
|
||||
const result = await ideManager.setup('windsurf', tempProjectDir, installedBmadDir, {
|
||||
silent: true,
|
||||
selectedModules: ['bmm'],
|
||||
});
|
||||
|
||||
assert(result.success === true, 'Windsurf setup succeeds against temp project');
|
||||
|
||||
const skillFile = path.join(tempProjectDir, '.windsurf', 'skills', 'bmad-master', 'SKILL.md');
|
||||
assert(await fs.pathExists(skillFile), 'Windsurf install writes SKILL.md directory output');
|
||||
|
||||
assert(!(await fs.pathExists(path.join(tempProjectDir, '.windsurf', 'workflows'))), 'Windsurf setup removes legacy workflows dir');
|
||||
|
||||
await fs.remove(tempProjectDir);
|
||||
await fs.remove(installedBmadDir);
|
||||
} catch (error) {
|
||||
assert(false, 'Windsurf native skills migration test succeeds', error.message);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Test 5: Kiro Native Skills Install
|
||||
// ============================================================
|
||||
console.log(`${colors.yellow}Test Suite 5: Kiro Native Skills${colors.reset}\n`);
|
||||
|
||||
try {
|
||||
clearCache();
|
||||
const platformCodes = await loadPlatformCodes();
|
||||
const kiroInstaller = platformCodes.platforms.kiro?.installer;
|
||||
|
||||
assert(kiroInstaller?.target_dir === '.kiro/skills', 'Kiro target_dir uses native skills path');
|
||||
|
||||
assert(kiroInstaller?.skill_format === true, 'Kiro installer enables native skill output');
|
||||
|
||||
assert(
|
||||
Array.isArray(kiroInstaller?.legacy_targets) && kiroInstaller.legacy_targets.includes('.kiro/steering'),
|
||||
'Kiro installer cleans legacy steering output',
|
||||
);
|
||||
|
||||
const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-kiro-test-'));
|
||||
const installedBmadDir = await createTestBmadFixture();
|
||||
const legacyDir = path.join(tempProjectDir, '.kiro', 'steering', 'bmad-legacy-dir');
|
||||
await fs.ensureDir(legacyDir);
|
||||
await fs.writeFile(path.join(tempProjectDir, '.kiro', 'steering', 'bmad-legacy.md'), 'legacy\n');
|
||||
await fs.writeFile(path.join(legacyDir, 'SKILL.md'), 'legacy\n');
|
||||
|
||||
const ideManager = new IdeManager();
|
||||
await ideManager.ensureInitialized();
|
||||
const result = await ideManager.setup('kiro', tempProjectDir, installedBmadDir, {
|
||||
silent: true,
|
||||
selectedModules: ['bmm'],
|
||||
});
|
||||
|
||||
assert(result.success === true, 'Kiro setup succeeds against temp project');
|
||||
|
||||
const skillFile = path.join(tempProjectDir, '.kiro', 'skills', 'bmad-master', 'SKILL.md');
|
||||
assert(await fs.pathExists(skillFile), 'Kiro install writes SKILL.md directory output');
|
||||
|
||||
assert(!(await fs.pathExists(path.join(tempProjectDir, '.kiro', 'steering'))), 'Kiro setup removes legacy steering dir');
|
||||
|
||||
await fs.remove(tempProjectDir);
|
||||
await fs.remove(installedBmadDir);
|
||||
} catch (error) {
|
||||
assert(false, 'Kiro native skills migration test succeeds', error.message);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Test 6: Antigravity Native Skills Install
|
||||
// ============================================================
|
||||
console.log(`${colors.yellow}Test Suite 6: Antigravity Native Skills${colors.reset}\n`);
|
||||
|
||||
try {
|
||||
clearCache();
|
||||
const platformCodes = await loadPlatformCodes();
|
||||
const antigravityInstaller = platformCodes.platforms.antigravity?.installer;
|
||||
|
||||
assert(antigravityInstaller?.target_dir === '.agent/skills', 'Antigravity target_dir uses native skills path');
|
||||
|
||||
assert(antigravityInstaller?.skill_format === true, 'Antigravity installer enables native skill output');
|
||||
|
||||
assert(
|
||||
Array.isArray(antigravityInstaller?.legacy_targets) && antigravityInstaller.legacy_targets.includes('.agent/workflows'),
|
||||
'Antigravity installer cleans legacy workflow output',
|
||||
);
|
||||
|
||||
const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-antigravity-test-'));
|
||||
const installedBmadDir = await createTestBmadFixture();
|
||||
const legacyDir = path.join(tempProjectDir, '.agent', 'workflows', 'bmad-legacy-dir');
|
||||
await fs.ensureDir(legacyDir);
|
||||
await fs.writeFile(path.join(tempProjectDir, '.agent', 'workflows', 'bmad-legacy.md'), 'legacy\n');
|
||||
await fs.writeFile(path.join(legacyDir, 'SKILL.md'), 'legacy\n');
|
||||
|
||||
const ideManager = new IdeManager();
|
||||
await ideManager.ensureInitialized();
|
||||
const result = await ideManager.setup('antigravity', tempProjectDir, installedBmadDir, {
|
||||
silent: true,
|
||||
selectedModules: ['bmm'],
|
||||
});
|
||||
|
||||
assert(result.success === true, 'Antigravity setup succeeds against temp project');
|
||||
|
||||
const skillFile = path.join(tempProjectDir, '.agent', 'skills', 'bmad-master', 'SKILL.md');
|
||||
assert(await fs.pathExists(skillFile), 'Antigravity install writes SKILL.md directory output');
|
||||
|
||||
assert(!(await fs.pathExists(path.join(tempProjectDir, '.agent', 'workflows'))), 'Antigravity setup removes legacy workflows dir');
|
||||
|
||||
await fs.remove(tempProjectDir);
|
||||
await fs.remove(installedBmadDir);
|
||||
} catch (error) {
|
||||
assert(false, 'Antigravity native skills migration test succeeds', error.message);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Test 7: Auggie Native Skills Install
|
||||
// ============================================================
|
||||
console.log(`${colors.yellow}Test Suite 7: Auggie Native Skills${colors.reset}\n`);
|
||||
|
||||
try {
|
||||
clearCache();
|
||||
const platformCodes = await loadPlatformCodes();
|
||||
const auggieInstaller = platformCodes.platforms.auggie?.installer;
|
||||
|
||||
assert(auggieInstaller?.target_dir === '.augment/skills', 'Auggie target_dir uses native skills path');
|
||||
|
||||
assert(auggieInstaller?.skill_format === true, 'Auggie installer enables native skill output');
|
||||
|
||||
assert(
|
||||
Array.isArray(auggieInstaller?.legacy_targets) && auggieInstaller.legacy_targets.includes('.augment/commands'),
|
||||
'Auggie installer cleans legacy command output',
|
||||
);
|
||||
|
||||
assert(
|
||||
auggieInstaller?.ancestor_conflict_check !== true,
|
||||
'Auggie installer does not enable ancestor conflict checks without verified inheritance',
|
||||
);
|
||||
|
||||
const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-auggie-test-'));
|
||||
const installedBmadDir = await createTestBmadFixture();
|
||||
const legacyDir = path.join(tempProjectDir, '.augment', 'commands', 'bmad-legacy-dir');
|
||||
await fs.ensureDir(legacyDir);
|
||||
await fs.writeFile(path.join(tempProjectDir, '.augment', 'commands', 'bmad-legacy.md'), 'legacy\n');
|
||||
await fs.writeFile(path.join(legacyDir, 'SKILL.md'), 'legacy\n');
|
||||
|
||||
const ideManager = new IdeManager();
|
||||
await ideManager.ensureInitialized();
|
||||
const result = await ideManager.setup('auggie', tempProjectDir, installedBmadDir, {
|
||||
silent: true,
|
||||
selectedModules: ['bmm'],
|
||||
});
|
||||
|
||||
assert(result.success === true, 'Auggie setup succeeds against temp project');
|
||||
|
||||
const skillFile = path.join(tempProjectDir, '.augment', 'skills', 'bmad-master', 'SKILL.md');
|
||||
assert(await fs.pathExists(skillFile), 'Auggie install writes SKILL.md directory output');
|
||||
|
||||
assert(!(await fs.pathExists(path.join(tempProjectDir, '.augment', 'commands'))), 'Auggie setup removes legacy commands dir');
|
||||
|
||||
await fs.remove(tempProjectDir);
|
||||
await fs.remove(installedBmadDir);
|
||||
} catch (error) {
|
||||
assert(false, 'Auggie native skills migration test succeeds', error.message);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Test 8: OpenCode Native Skills Install
|
||||
// ============================================================
|
||||
console.log(`${colors.yellow}Test Suite 8: OpenCode Native Skills${colors.reset}\n`);
|
||||
|
||||
try {
|
||||
clearCache();
|
||||
const platformCodes = await loadPlatformCodes();
|
||||
const opencodeInstaller = platformCodes.platforms.opencode?.installer;
|
||||
|
||||
assert(opencodeInstaller?.target_dir === '.opencode/skills', 'OpenCode target_dir uses native skills path');
|
||||
|
||||
assert(opencodeInstaller?.skill_format === true, 'OpenCode installer enables native skill output');
|
||||
|
||||
assert(opencodeInstaller?.ancestor_conflict_check === true, 'OpenCode installer enables ancestor conflict checks');
|
||||
|
||||
assert(
|
||||
Array.isArray(opencodeInstaller?.legacy_targets) &&
|
||||
['.opencode/agents', '.opencode/commands', '.opencode/agent', '.opencode/command'].every((legacyTarget) =>
|
||||
opencodeInstaller.legacy_targets.includes(legacyTarget),
|
||||
),
|
||||
'OpenCode installer cleans split legacy agent and command output',
|
||||
);
|
||||
|
||||
const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-opencode-test-'));
|
||||
const installedBmadDir = await createTestBmadFixture();
|
||||
const legacyDirs = [
|
||||
path.join(tempProjectDir, '.opencode', 'agents', 'bmad-legacy-agent'),
|
||||
path.join(tempProjectDir, '.opencode', 'commands', 'bmad-legacy-command'),
|
||||
path.join(tempProjectDir, '.opencode', 'agent', 'bmad-legacy-agent-singular'),
|
||||
path.join(tempProjectDir, '.opencode', 'command', 'bmad-legacy-command-singular'),
|
||||
];
|
||||
|
||||
for (const legacyDir of legacyDirs) {
|
||||
await fs.ensureDir(legacyDir);
|
||||
await fs.writeFile(path.join(legacyDir, 'SKILL.md'), 'legacy\n');
|
||||
await fs.writeFile(path.join(path.dirname(legacyDir), `${path.basename(legacyDir)}.md`), 'legacy\n');
|
||||
}
|
||||
|
||||
const ideManager = new IdeManager();
|
||||
await ideManager.ensureInitialized();
|
||||
const result = await ideManager.setup('opencode', tempProjectDir, installedBmadDir, {
|
||||
silent: true,
|
||||
selectedModules: ['bmm'],
|
||||
});
|
||||
|
||||
assert(result.success === true, 'OpenCode setup succeeds against temp project');
|
||||
|
||||
const skillFile = path.join(tempProjectDir, '.opencode', 'skills', 'bmad-master', 'SKILL.md');
|
||||
assert(await fs.pathExists(skillFile), 'OpenCode install writes SKILL.md directory output');
|
||||
|
||||
for (const legacyDir of ['agents', 'commands', 'agent', 'command']) {
|
||||
assert(
|
||||
!(await fs.pathExists(path.join(tempProjectDir, '.opencode', legacyDir))),
|
||||
`OpenCode setup removes legacy .opencode/${legacyDir} dir`,
|
||||
);
|
||||
}
|
||||
|
||||
await fs.remove(tempProjectDir);
|
||||
await fs.remove(installedBmadDir);
|
||||
} catch (error) {
|
||||
assert(false, 'OpenCode native skills migration test succeeds', error.message);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Test 9: OpenCode Ancestor Conflict
|
||||
// ============================================================
|
||||
console.log(`${colors.yellow}Test Suite 9: OpenCode Ancestor Conflict${colors.reset}\n`);
|
||||
|
||||
try {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-opencode-ancestor-test-'));
|
||||
const parentProjectDir = path.join(tempRoot, 'parent');
|
||||
const childProjectDir = path.join(parentProjectDir, 'child');
|
||||
const installedBmadDir = await createTestBmadFixture();
|
||||
|
||||
await fs.ensureDir(path.join(parentProjectDir, '.git'));
|
||||
await fs.ensureDir(path.join(parentProjectDir, '.opencode', 'skills', 'bmad-existing'));
|
||||
await fs.ensureDir(childProjectDir);
|
||||
await fs.writeFile(path.join(parentProjectDir, '.opencode', 'skills', 'bmad-existing', 'SKILL.md'), 'legacy\n');
|
||||
|
||||
const ideManager = new IdeManager();
|
||||
await ideManager.ensureInitialized();
|
||||
const result = await ideManager.setup('opencode', childProjectDir, installedBmadDir, {
|
||||
silent: true,
|
||||
selectedModules: ['bmm'],
|
||||
});
|
||||
const expectedConflictDir = await fs.realpath(path.join(parentProjectDir, '.opencode', 'skills'));
|
||||
|
||||
assert(result.success === false, 'OpenCode setup refuses install when ancestor skills already exist');
|
||||
assert(result.handlerResult?.reason === 'ancestor-conflict', 'OpenCode ancestor rejection reports ancestor-conflict reason');
|
||||
assert(
|
||||
result.handlerResult?.conflictDir === expectedConflictDir,
|
||||
'OpenCode ancestor rejection points at ancestor .opencode/skills dir',
|
||||
);
|
||||
|
||||
await fs.remove(tempRoot);
|
||||
await fs.remove(installedBmadDir);
|
||||
} catch (error) {
|
||||
assert(false, 'OpenCode ancestor conflict protection test succeeds', error.message);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Test 10: QA Agent Compilation
|
||||
// ============================================================
|
||||
console.log(`${colors.yellow}Test Suite 10: QA Agent Compilation${colors.reset}\n`);
|
||||
console.log(`${colors.yellow}Test Suite 5: QA Agent Compilation${colors.reset}\n`);
|
||||
|
||||
try {
|
||||
const builder = new YamlXmlBuilder();
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ const crypto = require('node:crypto');
|
|||
const csv = require('csv-parse/sync');
|
||||
const { getSourcePath, getModulePath } = require('../../../lib/project-root');
|
||||
const prompts = require('../../../lib/prompts');
|
||||
const { loadSkillManifest: loadSkillManifestShared, getCanonicalId: getCanonicalIdShared } = require('../ide/shared/skill-manifest');
|
||||
|
||||
// Load package.json for version info
|
||||
const packageJson = require('../../../../../package.json');
|
||||
|
|
@ -24,16 +23,6 @@ class ManifestGenerator {
|
|||
this.selectedIdes = [];
|
||||
}
|
||||
|
||||
/** Delegate to shared skill-manifest module */
|
||||
async loadSkillManifest(dirPath) {
|
||||
return loadSkillManifestShared(dirPath);
|
||||
}
|
||||
|
||||
/** Delegate to shared skill-manifest module */
|
||||
getCanonicalId(manifest, filename) {
|
||||
return getCanonicalIdShared(manifest, filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean text for CSV output by normalizing whitespace.
|
||||
* Note: Quote escaping is handled by escapeCsv() at write time.
|
||||
|
|
@ -161,8 +150,6 @@ class ManifestGenerator {
|
|||
// Recursively find workflow.yaml files
|
||||
const findWorkflows = async (dir, relativePath = '') => {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
// Load skill manifest for this directory (if present)
|
||||
const skillManifest = await this.loadSkillManifest(dir);
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
|
@ -234,7 +221,6 @@ class ManifestGenerator {
|
|||
description: this.cleanForCSV(workflow.description),
|
||||
module: moduleName,
|
||||
path: installPath,
|
||||
canonicalId: this.getCanonicalId(skillManifest, entry.name),
|
||||
});
|
||||
|
||||
// Add to files list
|
||||
|
|
@ -308,8 +294,6 @@ class ManifestGenerator {
|
|||
async getAgentsFromDir(dirPath, moduleName, relativePath = '') {
|
||||
const agents = [];
|
||||
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
||||
// Load skill manifest for this directory (if present)
|
||||
const skillManifest = await this.loadSkillManifest(dirPath);
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dirPath, entry.name);
|
||||
|
|
@ -365,7 +349,6 @@ class ManifestGenerator {
|
|||
principles: principlesMatch ? this.cleanForCSV(principlesMatch[1]) : '',
|
||||
module: moduleName,
|
||||
path: installPath,
|
||||
canonicalId: this.getCanonicalId(skillManifest, entry.name),
|
||||
});
|
||||
|
||||
// Add to files list
|
||||
|
|
@ -405,8 +388,6 @@ class ManifestGenerator {
|
|||
async getTasksFromDir(dirPath, moduleName) {
|
||||
const tasks = [];
|
||||
const files = await fs.readdir(dirPath);
|
||||
// Load skill manifest for this directory (if present)
|
||||
const skillManifest = await this.loadSkillManifest(dirPath);
|
||||
|
||||
for (const file of files) {
|
||||
// Check for both .xml and .md files
|
||||
|
|
@ -466,7 +447,6 @@ class ManifestGenerator {
|
|||
module: moduleName,
|
||||
path: installPath,
|
||||
standalone: standalone,
|
||||
canonicalId: this.getCanonicalId(skillManifest, file),
|
||||
});
|
||||
|
||||
// Add to files list
|
||||
|
|
@ -506,8 +486,6 @@ class ManifestGenerator {
|
|||
async getToolsFromDir(dirPath, moduleName) {
|
||||
const tools = [];
|
||||
const files = await fs.readdir(dirPath);
|
||||
// Load skill manifest for this directory (if present)
|
||||
const skillManifest = await this.loadSkillManifest(dirPath);
|
||||
|
||||
for (const file of files) {
|
||||
// Check for both .xml and .md files
|
||||
|
|
@ -567,7 +545,6 @@ class ManifestGenerator {
|
|||
module: moduleName,
|
||||
path: installPath,
|
||||
standalone: standalone,
|
||||
canonicalId: this.getCanonicalId(skillManifest, file),
|
||||
});
|
||||
|
||||
// Add to files list
|
||||
|
|
@ -758,8 +735,8 @@ class ManifestGenerator {
|
|||
const csvPath = path.join(cfgDir, 'workflow-manifest.csv');
|
||||
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
|
||||
|
||||
// Create CSV header - standalone column removed, canonicalId added as optional column
|
||||
let csv = 'name,description,module,path,canonicalId\n';
|
||||
// Create CSV header - standalone column removed, everything is canonicalized to 4 columns
|
||||
let csv = 'name,description,module,path\n';
|
||||
|
||||
// Build workflows map from discovered workflows only
|
||||
// Old entries are NOT preserved - the manifest reflects what actually exists on disk
|
||||
|
|
@ -773,19 +750,12 @@ class ManifestGenerator {
|
|||
description: workflow.description,
|
||||
module: workflow.module,
|
||||
path: workflow.path,
|
||||
canonicalId: workflow.canonicalId || '',
|
||||
});
|
||||
}
|
||||
|
||||
// Write all workflows
|
||||
for (const [, value] of allWorkflows) {
|
||||
const row = [
|
||||
escapeCsv(value.name),
|
||||
escapeCsv(value.description),
|
||||
escapeCsv(value.module),
|
||||
escapeCsv(value.path),
|
||||
escapeCsv(value.canonicalId),
|
||||
].join(',');
|
||||
const row = [escapeCsv(value.name), escapeCsv(value.description), escapeCsv(value.module), escapeCsv(value.path)].join(',');
|
||||
csv += row + '\n';
|
||||
}
|
||||
|
||||
|
|
@ -814,8 +784,8 @@ class ManifestGenerator {
|
|||
}
|
||||
}
|
||||
|
||||
// Create CSV header with persona fields and canonicalId
|
||||
let csvContent = 'name,displayName,title,icon,capabilities,role,identity,communicationStyle,principles,module,path,canonicalId\n';
|
||||
// Create CSV header with persona fields
|
||||
let csvContent = 'name,displayName,title,icon,capabilities,role,identity,communicationStyle,principles,module,path\n';
|
||||
|
||||
// Combine existing and new agents, preferring new data for duplicates
|
||||
const allAgents = new Map();
|
||||
|
|
@ -840,7 +810,6 @@ class ManifestGenerator {
|
|||
principles: agent.principles,
|
||||
module: agent.module,
|
||||
path: agent.path,
|
||||
canonicalId: agent.canonicalId || '',
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -858,7 +827,6 @@ class ManifestGenerator {
|
|||
escapeCsv(record.principles),
|
||||
escapeCsv(record.module),
|
||||
escapeCsv(record.path),
|
||||
escapeCsv(record.canonicalId),
|
||||
].join(',');
|
||||
csvContent += row + '\n';
|
||||
}
|
||||
|
|
@ -888,8 +856,8 @@ class ManifestGenerator {
|
|||
}
|
||||
}
|
||||
|
||||
// Create CSV header with standalone and canonicalId columns
|
||||
let csvContent = 'name,displayName,description,module,path,standalone,canonicalId\n';
|
||||
// Create CSV header with standalone column
|
||||
let csvContent = 'name,displayName,description,module,path,standalone\n';
|
||||
|
||||
// Combine existing and new tasks
|
||||
const allTasks = new Map();
|
||||
|
|
@ -909,7 +877,6 @@ class ManifestGenerator {
|
|||
module: task.module,
|
||||
path: task.path,
|
||||
standalone: task.standalone,
|
||||
canonicalId: task.canonicalId || '',
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -922,7 +889,6 @@ class ManifestGenerator {
|
|||
escapeCsv(record.module),
|
||||
escapeCsv(record.path),
|
||||
escapeCsv(record.standalone),
|
||||
escapeCsv(record.canonicalId),
|
||||
].join(',');
|
||||
csvContent += row + '\n';
|
||||
}
|
||||
|
|
@ -952,8 +918,8 @@ class ManifestGenerator {
|
|||
}
|
||||
}
|
||||
|
||||
// Create CSV header with standalone and canonicalId columns
|
||||
let csvContent = 'name,displayName,description,module,path,standalone,canonicalId\n';
|
||||
// Create CSV header with standalone column
|
||||
let csvContent = 'name,displayName,description,module,path,standalone\n';
|
||||
|
||||
// Combine existing and new tools
|
||||
const allTools = new Map();
|
||||
|
|
@ -973,7 +939,6 @@ class ManifestGenerator {
|
|||
module: tool.module,
|
||||
path: tool.path,
|
||||
standalone: tool.standalone,
|
||||
canonicalId: tool.canonicalId || '',
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -986,7 +951,6 @@ class ManifestGenerator {
|
|||
escapeCsv(record.module),
|
||||
escapeCsv(record.path),
|
||||
escapeCsv(record.standalone),
|
||||
escapeCsv(record.canonicalId),
|
||||
].join(',');
|
||||
csvContent += row + '\n';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const yaml = require('yaml');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const prompts = require('../../../lib/prompts');
|
||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||
|
|
@ -26,34 +24,6 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
|||
super(platformCode, platformConfig.name, platformConfig.preferred);
|
||||
this.platformConfig = platformConfig;
|
||||
this.installerConfig = platformConfig.installer || null;
|
||||
|
||||
// Set configDir from target_dir so base-class detect() works
|
||||
if (this.installerConfig?.target_dir) {
|
||||
this.configDir = this.installerConfig.target_dir;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect whether this IDE already has configuration in the project.
|
||||
* For skill_format platforms, checks for bmad-prefixed entries in target_dir
|
||||
* (matching old codex.js behavior) instead of just checking directory existence.
|
||||
* @param {string} projectDir - Project directory
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async detect(projectDir) {
|
||||
if (this.installerConfig?.skill_format && this.configDir) {
|
||||
const dir = path.join(projectDir || process.cwd(), this.configDir);
|
||||
if (await fs.pathExists(dir)) {
|
||||
try {
|
||||
const entries = await fs.readdir(dir);
|
||||
return entries.some((e) => typeof e === 'string' && e.startsWith('bmad'));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return super.detect(projectDir);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -69,8 +39,8 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
|||
const conflict = await this.findAncestorConflict(projectDir);
|
||||
if (conflict) {
|
||||
await prompts.log.error(
|
||||
`Found existing BMAD skills in ancestor installation: ${conflict}\n` +
|
||||
` ${this.name} inherits skills from parent directories, so this would cause duplicates.\n` +
|
||||
`Found existing BMAD commands in ancestor installation: ${conflict}\n` +
|
||||
` ${this.name} inherits commands from parent directories, so this would cause duplicates.\n` +
|
||||
` Please remove the BMAD files from that directory first:\n` +
|
||||
` rm -rf "${conflict}"/bmad*`,
|
||||
);
|
||||
|
|
@ -195,13 +165,8 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
|||
for (const artifact of artifacts) {
|
||||
const content = this.renderTemplate(template, artifact);
|
||||
const filename = this.generateFilename(artifact, 'agent', extension);
|
||||
|
||||
if (config.skill_format) {
|
||||
await this.writeSkillFile(targetPath, artifact, content);
|
||||
} else {
|
||||
const filePath = path.join(targetPath, filename);
|
||||
await this.writeFile(filePath, content);
|
||||
}
|
||||
count++;
|
||||
}
|
||||
|
||||
|
|
@ -233,13 +198,8 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
|||
const { content: template, extension } = await this.loadTemplate(workflowTemplateType, '', config, finalTemplateType);
|
||||
const content = this.renderTemplate(template, artifact);
|
||||
const filename = this.generateFilename(artifact, 'workflow', extension);
|
||||
|
||||
if (config.skill_format) {
|
||||
await this.writeSkillFile(targetPath, artifact, content);
|
||||
} else {
|
||||
const filePath = path.join(targetPath, filename);
|
||||
await this.writeFile(filePath, content);
|
||||
}
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
|
@ -281,13 +241,8 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
|||
|
||||
const content = this.renderTemplate(template, artifact);
|
||||
const filename = this.generateFilename(artifact, artifact.type, extension);
|
||||
|
||||
if (config.skill_format) {
|
||||
await this.writeSkillFile(targetPath, artifact, content);
|
||||
} else {
|
||||
const filePath = path.join(targetPath, filename);
|
||||
await this.writeFile(filePath, content);
|
||||
}
|
||||
|
||||
if (artifact.type === 'task') {
|
||||
taskCount++;
|
||||
|
|
@ -454,146 +409,22 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
|||
// No default
|
||||
}
|
||||
|
||||
// Replace _bmad placeholder with actual folder name BEFORE inserting paths,
|
||||
// so that paths containing '_bmad' are not corrupted by the blanket replacement.
|
||||
let rendered = template.replaceAll('_bmad', this.bmadFolderName);
|
||||
|
||||
// Replace {{bmadFolderName}} placeholder if present
|
||||
rendered = rendered.replaceAll('{{bmadFolderName}}', this.bmadFolderName);
|
||||
|
||||
rendered = rendered
|
||||
let rendered = template
|
||||
.replaceAll('{{name}}', artifact.name || '')
|
||||
.replaceAll('{{module}}', artifact.module || 'core')
|
||||
.replaceAll('{{path}}', pathToUse)
|
||||
.replaceAll('{{description}}', artifact.description || `${artifact.name} ${artifact.type || ''}`)
|
||||
.replaceAll('{{workflow_path}}', pathToUse);
|
||||
|
||||
// Replace _bmad placeholder with actual folder name
|
||||
rendered = rendered.replaceAll('_bmad', this.bmadFolderName);
|
||||
|
||||
// Replace {{bmadFolderName}} placeholder if present
|
||||
rendered = rendered.replaceAll('{{bmadFolderName}}', this.bmadFolderName);
|
||||
|
||||
return rendered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write artifact as a skill directory with SKILL.md inside.
|
||||
* Writes artifact as a skill directory with SKILL.md inside.
|
||||
* @param {string} targetPath - Base skills directory
|
||||
* @param {Object} artifact - Artifact data
|
||||
* @param {string} content - Rendered template content
|
||||
*/
|
||||
async writeSkillFile(targetPath, artifact, content) {
|
||||
const { resolveSkillName } = require('./shared/path-utils');
|
||||
|
||||
// Get the skill name (prefers canonicalId, falls back to path-derived) and remove .md
|
||||
const flatName = resolveSkillName(artifact);
|
||||
const skillName = path.basename(flatName.replace(/\.md$/, ''));
|
||||
|
||||
if (!skillName) {
|
||||
throw new Error(`Cannot derive skill name for artifact: ${artifact.relativePath || JSON.stringify(artifact)}`);
|
||||
}
|
||||
|
||||
// Create skill directory
|
||||
const skillDir = path.join(targetPath, skillName);
|
||||
await this.ensureDir(skillDir);
|
||||
|
||||
// Transform content: rewrite frontmatter for skills format
|
||||
const skillContent = this.transformToSkillFormat(content, skillName);
|
||||
|
||||
await this.writeFile(path.join(skillDir, 'SKILL.md'), skillContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform artifact content to Agent Skills format.
|
||||
* Rewrites frontmatter to contain only unquoted name and description.
|
||||
* @param {string} content - Original content with YAML frontmatter
|
||||
* @param {string} skillName - Skill name (must match directory name)
|
||||
* @returns {string} Transformed content
|
||||
*/
|
||||
transformToSkillFormat(content, skillName) {
|
||||
// Normalize line endings
|
||||
content = content.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
|
||||
|
||||
// Parse frontmatter
|
||||
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
||||
if (!fmMatch) {
|
||||
// No frontmatter -- wrap with minimal frontmatter
|
||||
const fm = yaml.stringify({ name: skillName, description: skillName }).trimEnd();
|
||||
return `---\n${fm}\n---\n\n${content}`;
|
||||
}
|
||||
|
||||
const frontmatter = fmMatch[1];
|
||||
const body = fmMatch[2];
|
||||
|
||||
// Parse frontmatter with yaml library to extract description
|
||||
let description;
|
||||
try {
|
||||
const parsed = yaml.parse(frontmatter);
|
||||
const rawDesc = parsed?.description;
|
||||
description = typeof rawDesc === 'string' && rawDesc ? rawDesc : `${skillName} skill`;
|
||||
} catch {
|
||||
description = `${skillName} skill`;
|
||||
}
|
||||
|
||||
// Build new frontmatter with only name and description, unquoted
|
||||
const newFrontmatter = yaml.stringify({ name: skillName, description: String(description) }, { lineWidth: 0 }).trimEnd();
|
||||
return `---\n${newFrontmatter}\n---\n${body}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a custom agent launcher.
|
||||
* For skill_format platforms, produces <skillDir>/SKILL.md.
|
||||
* For flat platforms, produces a single file in target_dir.
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
||||
* @param {Object} metadata - Agent metadata
|
||||
* @returns {Object|null} Info about created file/skill
|
||||
*/
|
||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
||||
if (!this.installerConfig?.target_dir) return null;
|
||||
|
||||
const { customAgentDashName } = require('./shared/path-utils');
|
||||
const targetPath = path.join(projectDir, this.installerConfig.target_dir);
|
||||
await this.ensureDir(targetPath);
|
||||
|
||||
// Build artifact to reuse existing template rendering.
|
||||
// The default-agent template already includes the _bmad/ prefix before {{path}},
|
||||
// but agentPath is relative to project root (e.g. "_bmad/custom/agents/fred.md").
|
||||
// Strip the bmadFolderName prefix so the template doesn't produce a double path.
|
||||
const bmadPrefix = this.bmadFolderName + '/';
|
||||
const normalizedPath = agentPath.startsWith(bmadPrefix) ? agentPath.slice(bmadPrefix.length) : agentPath;
|
||||
|
||||
const artifact = {
|
||||
type: 'agent-launcher',
|
||||
name: agentName,
|
||||
description: metadata?.description || `${agentName} agent`,
|
||||
agentPath: normalizedPath,
|
||||
relativePath: normalizedPath,
|
||||
module: 'custom',
|
||||
};
|
||||
|
||||
const { content: template } = await this.loadTemplate(
|
||||
this.installerConfig.template_type || 'default',
|
||||
'agent',
|
||||
this.installerConfig,
|
||||
'default-agent',
|
||||
);
|
||||
const content = this.renderTemplate(template, artifact);
|
||||
|
||||
if (this.installerConfig.skill_format) {
|
||||
const skillName = customAgentDashName(agentName).replace(/\.md$/, '');
|
||||
const skillDir = path.join(targetPath, skillName);
|
||||
await this.ensureDir(skillDir);
|
||||
const skillContent = this.transformToSkillFormat(content, skillName);
|
||||
const skillPath = path.join(skillDir, 'SKILL.md');
|
||||
await this.writeFile(skillPath, skillContent);
|
||||
return { path: path.relative(projectDir, skillPath), command: `$${skillName}` };
|
||||
}
|
||||
|
||||
// Flat file output
|
||||
const filename = customAgentDashName(agentName);
|
||||
const filePath = path.join(targetPath, filename);
|
||||
await this.writeFile(filePath, content);
|
||||
return { path: path.relative(projectDir, filePath), command: agentName };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate filename for artifact
|
||||
* @param {Object} artifact - Artifact data
|
||||
|
|
@ -602,11 +433,10 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
|||
* @returns {string} Generated filename
|
||||
*/
|
||||
generateFilename(artifact, artifactType, extension = '.md') {
|
||||
const { resolveSkillName } = require('./shared/path-utils');
|
||||
const { toDashPath } = require('./shared/path-utils');
|
||||
|
||||
// Reuse central logic to ensure consistent naming conventions
|
||||
// Prefers canonicalId from manifest when available, falls back to path-derived name
|
||||
const standardName = resolveSkillName(artifact);
|
||||
const standardName = toDashPath(artifact.relativePath);
|
||||
|
||||
// Clean up potential double extensions from source files (e.g. .yaml.md, .xml.md -> .md)
|
||||
// This handles any extensions that might slip through toDashPath()
|
||||
|
|
@ -646,14 +476,10 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
|||
if (this.installerConfig?.legacy_targets) {
|
||||
if (!options.silent) await prompts.log.message(' Migrating legacy directories...');
|
||||
for (const legacyDir of this.installerConfig.legacy_targets) {
|
||||
if (this.isGlobalPath(legacyDir)) {
|
||||
await this.warnGlobalLegacy(legacyDir, options);
|
||||
} else {
|
||||
await this.cleanupTarget(projectDir, legacyDir, options);
|
||||
await this.removeEmptyParents(projectDir, legacyDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean all target directories
|
||||
if (this.installerConfig?.targets) {
|
||||
|
|
@ -675,41 +501,6 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a path is global (starts with ~ or is absolute)
|
||||
* @param {string} p - Path to check
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isGlobalPath(p) {
|
||||
return p.startsWith('~') || path.isAbsolute(p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Warn about stale BMAD files in a global legacy directory (never auto-deletes)
|
||||
* @param {string} legacyDir - Legacy directory path (may start with ~)
|
||||
* @param {Object} options - Options (silent, etc.)
|
||||
*/
|
||||
async warnGlobalLegacy(legacyDir, options = {}) {
|
||||
try {
|
||||
const expanded = legacyDir.startsWith('~/')
|
||||
? path.join(os.homedir(), legacyDir.slice(2))
|
||||
: legacyDir === '~'
|
||||
? os.homedir()
|
||||
: legacyDir;
|
||||
|
||||
if (!(await fs.pathExists(expanded))) return;
|
||||
|
||||
const entries = await fs.readdir(expanded);
|
||||
const bmadFiles = entries.filter((e) => typeof e === 'string' && e.startsWith('bmad'));
|
||||
|
||||
if (bmadFiles.length > 0 && !options.silent) {
|
||||
await prompts.log.warn(`Found ${bmadFiles.length} stale BMAD file(s) in ${expanded}. Remove manually: rm ${expanded}/bmad-*`);
|
||||
}
|
||||
} catch {
|
||||
// Errors reading global paths are silently ignored
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup a specific target directory
|
||||
* @param {string} projectDir - Project directory
|
||||
|
|
|
|||
|
|
@ -0,0 +1,440 @@
|
|||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const fs = require('fs-extra');
|
||||
const yaml = require('yaml');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
|
||||
const { getTasksFromBmad } = require('./shared/bmad-artifacts');
|
||||
const { toDashPath, customAgentDashName } = require('./shared/path-utils');
|
||||
const prompts = require('../../../lib/prompts');
|
||||
|
||||
/**
|
||||
* Codex setup handler (CLI mode)
|
||||
*/
|
||||
class CodexSetup extends BaseIdeSetup {
|
||||
constructor() {
|
||||
super('codex', 'Codex', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup Codex configuration
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} bmadDir - BMAD installation directory
|
||||
* @param {Object} options - Setup options
|
||||
*/
|
||||
async setup(projectDir, bmadDir, options = {}) {
|
||||
if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`);
|
||||
|
||||
// Always use CLI mode
|
||||
const mode = 'cli';
|
||||
|
||||
const { artifacts, counts } = await this.collectClaudeArtifacts(projectDir, bmadDir, options);
|
||||
|
||||
// Clean up old .codex/prompts locations (both global and project)
|
||||
const oldGlobalDir = this.getOldCodexPromptDir(null, 'global');
|
||||
await this.clearOldBmadFiles(oldGlobalDir, options);
|
||||
const oldProjectDir = this.getOldCodexPromptDir(projectDir, 'project');
|
||||
await this.clearOldBmadFiles(oldProjectDir, options);
|
||||
|
||||
// Install to .agents/skills
|
||||
const destDir = this.getCodexSkillsDir(projectDir);
|
||||
await fs.ensureDir(destDir);
|
||||
await this.clearOldBmadSkills(destDir, options);
|
||||
|
||||
// Collect and write agent skills
|
||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
|
||||
const agentCount = await this.writeSkillArtifacts(destDir, agentArtifacts, 'agent-launcher');
|
||||
|
||||
// Collect and write task skills
|
||||
const tasks = await getTasksFromBmad(bmadDir, options.selectedModules || []);
|
||||
const taskArtifacts = [];
|
||||
for (const task of tasks) {
|
||||
const content = await this.readAndProcessWithProject(
|
||||
task.path,
|
||||
{
|
||||
module: task.module,
|
||||
name: task.name,
|
||||
},
|
||||
projectDir,
|
||||
);
|
||||
taskArtifacts.push({
|
||||
type: 'task',
|
||||
name: task.name,
|
||||
displayName: task.name,
|
||||
module: task.module,
|
||||
path: task.path,
|
||||
sourcePath: task.path,
|
||||
relativePath: path.join(task.module, 'tasks', `${task.name}.md`),
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
const ttGen = new TaskToolCommandGenerator(this.bmadFolderName);
|
||||
const taskSkillArtifacts = taskArtifacts.map((artifact) => ({
|
||||
...artifact,
|
||||
content: ttGen.generateCommandContent(artifact, artifact.type),
|
||||
}));
|
||||
const tasksWritten = await this.writeSkillArtifacts(destDir, taskSkillArtifacts, 'task');
|
||||
|
||||
// Collect and write workflow skills
|
||||
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: workflowArtifacts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
|
||||
const workflowCount = await this.writeSkillArtifacts(destDir, workflowArtifacts, 'workflow-command');
|
||||
|
||||
const written = agentCount + workflowCount + tasksWritten;
|
||||
|
||||
if (!options.silent) {
|
||||
await prompts.log.success(
|
||||
`${this.name} configured: ${counts.agents} agents, ${counts.workflows} workflows, ${counts.tasks} tasks, ${written} skills → ${destDir}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
mode,
|
||||
artifacts,
|
||||
counts,
|
||||
destination: destDir,
|
||||
written,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect Codex installation by checking for BMAD skills
|
||||
*/
|
||||
async detect(projectDir) {
|
||||
const dir = this.getCodexSkillsDir(projectDir || process.cwd());
|
||||
|
||||
if (await fs.pathExists(dir)) {
|
||||
try {
|
||||
const entries = await fs.readdir(dir);
|
||||
if (entries && entries.some((entry) => entry && typeof entry === 'string' && entry.startsWith('bmad'))) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect Claude-style artifacts for Codex export.
|
||||
* Returns the normalized artifact list for further processing.
|
||||
*/
|
||||
async collectClaudeArtifacts(projectDir, bmadDir, options = {}) {
|
||||
const selectedModules = options.selectedModules || [];
|
||||
const artifacts = [];
|
||||
|
||||
// Generate agent launchers
|
||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, selectedModules);
|
||||
|
||||
for (const artifact of agentArtifacts) {
|
||||
artifacts.push({
|
||||
type: 'agent',
|
||||
module: artifact.module,
|
||||
sourcePath: artifact.sourcePath,
|
||||
relativePath: artifact.relativePath,
|
||||
content: artifact.content,
|
||||
});
|
||||
}
|
||||
|
||||
const tasks = await getTasksFromBmad(bmadDir, selectedModules);
|
||||
for (const task of tasks) {
|
||||
const content = await this.readAndProcessWithProject(
|
||||
task.path,
|
||||
{
|
||||
module: task.module,
|
||||
name: task.name,
|
||||
},
|
||||
projectDir,
|
||||
);
|
||||
|
||||
artifacts.push({
|
||||
type: 'task',
|
||||
name: task.name,
|
||||
displayName: task.name,
|
||||
module: task.module,
|
||||
path: task.path,
|
||||
sourcePath: task.path,
|
||||
relativePath: path.join(task.module, 'tasks', `${task.name}.md`),
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
|
||||
const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
|
||||
artifacts.push(...workflowArtifacts);
|
||||
|
||||
return {
|
||||
artifacts,
|
||||
counts: {
|
||||
agents: agentArtifacts.length,
|
||||
tasks: tasks.length,
|
||||
workflows: workflowCounts.commands,
|
||||
workflowLaunchers: workflowCounts.launchers,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
getCodexSkillsDir(projectDir) {
|
||||
if (!projectDir) {
|
||||
throw new Error('projectDir is required for project-scoped skill installation');
|
||||
}
|
||||
return path.join(projectDir, '.agents', 'skills');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the old .codex/prompts directory for cleanup purposes
|
||||
*/
|
||||
getOldCodexPromptDir(projectDir = null, location = 'global') {
|
||||
if (location === 'project' && projectDir) {
|
||||
return path.join(projectDir, '.codex', 'prompts');
|
||||
}
|
||||
return path.join(os.homedir(), '.codex', 'prompts');
|
||||
}
|
||||
|
||||
/**
|
||||
* Write artifacts as Agent Skills (agentskills.io format).
|
||||
* Each artifact becomes a directory containing SKILL.md.
|
||||
* @param {string} destDir - Base skills directory
|
||||
* @param {Array} artifacts - Artifacts to write
|
||||
* @param {string} artifactType - Type filter (e.g., 'agent-launcher', 'workflow-command', 'task')
|
||||
* @returns {number} Number of skills written
|
||||
*/
|
||||
async writeSkillArtifacts(destDir, artifacts, artifactType) {
|
||||
let writtenCount = 0;
|
||||
|
||||
for (const artifact of artifacts) {
|
||||
// Filter by type if the artifact has a type field
|
||||
if (artifact.type && artifact.type !== artifactType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the dash-format name (e.g., bmad-bmm-create-prd.md) and remove .md
|
||||
const flatName = toDashPath(artifact.relativePath);
|
||||
const skillName = flatName.replace(/\.md$/, '');
|
||||
|
||||
// Create skill directory
|
||||
const skillDir = path.join(destDir, skillName);
|
||||
await fs.ensureDir(skillDir);
|
||||
|
||||
// Transform content: rewrite frontmatter for skills format
|
||||
const skillContent = this.transformToSkillFormat(artifact.content, skillName);
|
||||
|
||||
// Write SKILL.md with platform-native line endings
|
||||
const platformContent = skillContent.replaceAll('\n', os.EOL);
|
||||
await fs.writeFile(path.join(skillDir, 'SKILL.md'), platformContent, 'utf8');
|
||||
writtenCount++;
|
||||
}
|
||||
|
||||
return writtenCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform artifact content from Codex prompt format to Agent Skills format.
|
||||
* Removes disable-model-invocation, ensures name matches directory.
|
||||
* @param {string} content - Original content with YAML frontmatter
|
||||
* @param {string} skillName - Skill name (must match directory name)
|
||||
* @returns {string} Transformed content
|
||||
*/
|
||||
transformToSkillFormat(content, skillName) {
|
||||
// Normalize line endings so body matches rebuilt frontmatter (both LF)
|
||||
content = content.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
|
||||
|
||||
// Parse frontmatter
|
||||
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
||||
if (!fmMatch) {
|
||||
// No frontmatter -- wrap with minimal frontmatter
|
||||
const fm = yaml.stringify({ name: skillName, description: skillName }).trimEnd();
|
||||
return `---\n${fm}\n---\n\n${content}`;
|
||||
}
|
||||
|
||||
const frontmatter = fmMatch[1];
|
||||
const body = fmMatch[2];
|
||||
|
||||
// Parse frontmatter with yaml library to handle all quoting variants
|
||||
let description;
|
||||
try {
|
||||
const parsed = yaml.parse(frontmatter);
|
||||
description = parsed?.description || `${skillName} skill`;
|
||||
} catch {
|
||||
description = `${skillName} skill`;
|
||||
}
|
||||
|
||||
// Build new frontmatter with only skills-spec fields, let yaml handle quoting
|
||||
const newFrontmatter = yaml.stringify({ name: skillName, description }, { lineWidth: 0 }).trimEnd();
|
||||
return `---\n${newFrontmatter}\n---\n${body}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove existing BMAD skill directories from the skills directory.
|
||||
*/
|
||||
async clearOldBmadSkills(destDir, options = {}) {
|
||||
if (!(await fs.pathExists(destDir))) {
|
||||
return;
|
||||
}
|
||||
|
||||
let entries;
|
||||
try {
|
||||
entries = await fs.readdir(destDir);
|
||||
} catch (error) {
|
||||
if (!options.silent) await prompts.log.warn(`Warning: Could not read directory ${destDir}: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!entries || !Array.isArray(entries)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry || typeof entry !== 'string') {
|
||||
continue;
|
||||
}
|
||||
if (!entry.startsWith('bmad')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entryPath = path.join(destDir, entry);
|
||||
try {
|
||||
await fs.remove(entryPath);
|
||||
} catch (error) {
|
||||
if (!options.silent) {
|
||||
await prompts.log.message(` Skipping ${entry}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean old BMAD files from legacy .codex/prompts directories.
|
||||
*/
|
||||
async clearOldBmadFiles(destDir, options = {}) {
|
||||
if (!(await fs.pathExists(destDir))) {
|
||||
return;
|
||||
}
|
||||
|
||||
let entries;
|
||||
try {
|
||||
entries = await fs.readdir(destDir);
|
||||
} catch (error) {
|
||||
// Directory exists but can't be read - skip cleanup
|
||||
if (!options.silent) await prompts.log.warn(`Warning: Could not read directory ${destDir}: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!entries || !Array.isArray(entries)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
// Skip non-strings or undefined entries
|
||||
if (!entry || typeof entry !== 'string') {
|
||||
continue;
|
||||
}
|
||||
if (!entry.startsWith('bmad')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entryPath = path.join(destDir, entry);
|
||||
try {
|
||||
await fs.remove(entryPath);
|
||||
} catch (error) {
|
||||
if (!options.silent) {
|
||||
await prompts.log.message(` Skipping ${entry}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async readAndProcessWithProject(filePath, metadata, projectDir) {
|
||||
const rawContent = await fs.readFile(filePath, 'utf8');
|
||||
const content = rawContent.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
|
||||
return super.processContent(content, metadata, projectDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get instructions for project-specific installation
|
||||
* @param {string} projectDir - Optional project directory
|
||||
* @param {string} destDir - Optional destination directory
|
||||
* @returns {string} Instructions text
|
||||
*/
|
||||
getProjectSpecificInstructions(projectDir = null, destDir = null) {
|
||||
const lines = [
|
||||
'Project-Specific Codex Configuration',
|
||||
'',
|
||||
`Skills installed to: ${destDir || '<project>/.agents/skills'}`,
|
||||
'',
|
||||
'Codex automatically discovers skills in .agents/skills/ at and above the current directory and in your home directory.',
|
||||
'No additional configuration is needed.',
|
||||
];
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup Codex configuration - cleans both new .agents/skills and old .codex/prompts
|
||||
*/
|
||||
async cleanup(projectDir = null) {
|
||||
// Clean old .codex/prompts locations
|
||||
const oldGlobalDir = this.getOldCodexPromptDir(null, 'global');
|
||||
await this.clearOldBmadFiles(oldGlobalDir);
|
||||
|
||||
if (projectDir) {
|
||||
const oldProjectDir = this.getOldCodexPromptDir(projectDir, 'project');
|
||||
await this.clearOldBmadFiles(oldProjectDir);
|
||||
|
||||
// Clean new .agents/skills location
|
||||
const destDir = this.getCodexSkillsDir(projectDir);
|
||||
await this.clearOldBmadSkills(destDir);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a custom agent launcher for Codex as an Agent Skill
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
|
||||
* @param {string} agentPath - Path to compiled agent (relative to project root)
|
||||
* @param {Object} metadata - Agent metadata
|
||||
* @returns {Object|null} Info about created skill
|
||||
*/
|
||||
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
|
||||
const destDir = this.getCodexSkillsDir(projectDir);
|
||||
|
||||
// Skill name from the dash name (without .md)
|
||||
const skillName = customAgentDashName(agentName).replace(/\.md$/, '');
|
||||
const skillDir = path.join(destDir, skillName);
|
||||
await fs.ensureDir(skillDir);
|
||||
|
||||
const description = metadata?.description || `${agentName} agent`;
|
||||
const fm = yaml.stringify({ name: skillName, description }).trimEnd();
|
||||
const skillContent =
|
||||
`---\n${fm}\n---\n` +
|
||||
"\nYou must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.\n" +
|
||||
'\n<agent-activation CRITICAL="TRUE">\n' +
|
||||
`1. LOAD the FULL agent file from @${agentPath}\n` +
|
||||
'2. READ its entire contents - this contains the complete agent persona, menu, and instructions\n' +
|
||||
'3. FOLLOW every step in the <activation> section precisely\n' +
|
||||
'4. DISPLAY the welcome/greeting as instructed\n' +
|
||||
'5. PRESENT the numbered menu\n' +
|
||||
'6. WAIT for user input before proceeding\n' +
|
||||
'</agent-activation>\n';
|
||||
|
||||
// Write with platform-native line endings
|
||||
const platformContent = skillContent.replaceAll('\n', os.EOL);
|
||||
const skillPath = path.join(skillDir, 'SKILL.md');
|
||||
await fs.writeFile(skillPath, platformContent, 'utf8');
|
||||
|
||||
return {
|
||||
path: path.relative(projectDir, skillPath),
|
||||
command: `$${skillName}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { CodexSetup };
|
||||
|
|
@ -8,7 +8,7 @@ const prompts = require('../../../lib/prompts');
|
|||
* Dynamically discovers and loads IDE handlers
|
||||
*
|
||||
* Loading strategy:
|
||||
* 1. Custom installer files (github-copilot.js, kilo.js, rovodev.js) - for platforms with unique installation logic
|
||||
* 1. Custom installer files (codex.js, github-copilot.js, kilo.js, rovodev.js) - for platforms with unique installation logic
|
||||
* 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns
|
||||
*/
|
||||
class IdeManager {
|
||||
|
|
@ -44,7 +44,7 @@ class IdeManager {
|
|||
|
||||
/**
|
||||
* Dynamically load all IDE handlers
|
||||
* 1. Load custom installer files first (github-copilot.js, kilo.js, rovodev.js)
|
||||
* 1. Load custom installer files first (codex.js, github-copilot.js, kilo.js, rovodev.js)
|
||||
* 2. Load config-driven handlers from platform-codes.yaml
|
||||
*/
|
||||
async loadHandlers() {
|
||||
|
|
@ -58,11 +58,10 @@ class IdeManager {
|
|||
/**
|
||||
* Load custom installer files (unique installation logic)
|
||||
* These files have special installation patterns that don't fit the config-driven model
|
||||
* Note: codex was migrated to config-driven (platform-codes.yaml) and no longer needs a custom installer
|
||||
*/
|
||||
async loadCustomInstallerFiles() {
|
||||
const ideDir = __dirname;
|
||||
const customFiles = ['github-copilot.js', 'kilo.js', 'rovodev.js'];
|
||||
const customFiles = ['codex.js', 'github-copilot.js', 'kilo.js', 'rovodev.js'];
|
||||
|
||||
for (const file of customFiles) {
|
||||
const filePath = path.join(ideDir, file);
|
||||
|
|
@ -190,6 +189,14 @@ class IdeManager {
|
|||
if (r.tasks > 0) parts.push(`${r.tasks} tasks`);
|
||||
if (r.tools > 0) parts.push(`${r.tools} tools`);
|
||||
detail = parts.join(', ');
|
||||
} else if (handlerResult && handlerResult.counts) {
|
||||
// Codex handler returns { success, counts: { agents, workflows, tasks }, written }
|
||||
const c = handlerResult.counts;
|
||||
const parts = [];
|
||||
if (c.agents > 0) parts.push(`${c.agents} agents`);
|
||||
if (c.workflows > 0) parts.push(`${c.workflows} workflows`);
|
||||
if (c.tasks > 0) parts.push(`${c.tasks} tasks`);
|
||||
detail = parts.join(', ');
|
||||
} else if (handlerResult && handlerResult.modes !== undefined) {
|
||||
// Kilo handler returns { success, modes, workflows, tasks, tools }
|
||||
const parts = [];
|
||||
|
|
|
|||
|
|
@ -20,11 +20,8 @@ platforms:
|
|||
category: ide
|
||||
description: "Google's AI development environment"
|
||||
installer:
|
||||
legacy_targets:
|
||||
- .agent/workflows
|
||||
target_dir: .agent/skills
|
||||
target_dir: .agent/workflows
|
||||
template_type: antigravity
|
||||
skill_format: true
|
||||
|
||||
auggie:
|
||||
name: "Auggie"
|
||||
|
|
@ -32,11 +29,8 @@ platforms:
|
|||
category: cli
|
||||
description: "AI development tool"
|
||||
installer:
|
||||
legacy_targets:
|
||||
- .augment/commands
|
||||
target_dir: .augment/skills
|
||||
target_dir: .augment/commands
|
||||
template_type: default
|
||||
skill_format: true
|
||||
|
||||
claude-code:
|
||||
name: "Claude Code"
|
||||
|
|
@ -44,11 +38,8 @@ platforms:
|
|||
category: cli
|
||||
description: "Anthropic's official CLI for Claude"
|
||||
installer:
|
||||
legacy_targets:
|
||||
- .claude/commands
|
||||
target_dir: .claude/skills
|
||||
target_dir: .claude/commands
|
||||
template_type: default
|
||||
skill_format: true
|
||||
ancestor_conflict_check: true
|
||||
|
||||
cline:
|
||||
|
|
@ -65,15 +56,7 @@ platforms:
|
|||
preferred: false
|
||||
category: cli
|
||||
description: "OpenAI Codex integration"
|
||||
installer:
|
||||
legacy_targets:
|
||||
- .codex/prompts
|
||||
- ~/.codex/prompts
|
||||
target_dir: .agents/skills
|
||||
template_type: default
|
||||
skill_format: true
|
||||
ancestor_conflict_check: true
|
||||
artifact_types: [agents, workflows, tasks]
|
||||
# No installer config - uses custom codex.js
|
||||
|
||||
codebuddy:
|
||||
name: "CodeBuddy"
|
||||
|
|
@ -99,11 +82,8 @@ platforms:
|
|||
category: ide
|
||||
description: "AI-first code editor"
|
||||
installer:
|
||||
legacy_targets:
|
||||
- .cursor/commands
|
||||
target_dir: .cursor/skills
|
||||
target_dir: .cursor/commands
|
||||
template_type: default
|
||||
skill_format: true
|
||||
|
||||
gemini:
|
||||
name: "Gemini CLI"
|
||||
|
|
@ -143,11 +123,8 @@ platforms:
|
|||
category: ide
|
||||
description: "Amazon's AI-powered IDE"
|
||||
installer:
|
||||
legacy_targets:
|
||||
- .kiro/steering
|
||||
target_dir: .kiro/skills
|
||||
target_dir: .kiro/steering
|
||||
template_type: kiro
|
||||
skill_format: true
|
||||
|
||||
opencode:
|
||||
name: "OpenCode"
|
||||
|
|
@ -156,14 +133,15 @@ platforms:
|
|||
description: "OpenCode terminal coding assistant"
|
||||
installer:
|
||||
legacy_targets:
|
||||
- .opencode/agents
|
||||
- .opencode/commands
|
||||
- .opencode/agent
|
||||
- .opencode/command
|
||||
target_dir: .opencode/skills
|
||||
targets:
|
||||
- target_dir: .opencode/agents
|
||||
template_type: opencode
|
||||
skill_format: true
|
||||
ancestor_conflict_check: true
|
||||
artifact_types: [agents]
|
||||
- target_dir: .opencode/commands
|
||||
template_type: opencode
|
||||
artifact_types: [workflows, tasks, tools]
|
||||
|
||||
qwen:
|
||||
name: "QwenCoder"
|
||||
|
|
@ -205,11 +183,8 @@ platforms:
|
|||
category: ide
|
||||
description: "AI-powered IDE with cascade flows"
|
||||
installer:
|
||||
legacy_targets:
|
||||
- .windsurf/workflows
|
||||
target_dir: .windsurf/skills
|
||||
target_dir: .windsurf/workflows
|
||||
template_type: windsurf
|
||||
skill_format: true
|
||||
|
||||
# ============================================================================
|
||||
# Installer Config Schema
|
||||
|
|
@ -228,11 +203,9 @@ platforms:
|
|||
# artifact_types: [agents, workflows, tasks, tools]
|
||||
# artifact_types: array (optional) # Filter which artifacts to install (default: all)
|
||||
# skip_existing: boolean (optional) # Skip files that already exist (default: false)
|
||||
# skill_format: boolean (optional) # Use directory-per-skill output: <name>/SKILL.md
|
||||
# # with clean frontmatter (name + description, unquoted)
|
||||
# ancestor_conflict_check: boolean (optional) # Refuse install when ancestor dir has BMAD files
|
||||
# # in the same target_dir (for IDEs that inherit
|
||||
# # skills from parent directories)
|
||||
# # commands from parent directories)
|
||||
|
||||
# ============================================================================
|
||||
# Platform Categories
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ class AgentCommandGenerator {
|
|||
name: agent.name,
|
||||
description: agent.description || `${agent.name} agent`,
|
||||
module: agent.module,
|
||||
canonicalId: agent.canonicalId || '',
|
||||
relativePath: path.join(agent.module, 'agents', agentPathInModule), // For command filename
|
||||
agentPath: agentRelPath, // Relative path to actual agent file
|
||||
content: launcherContent,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const { loadSkillManifest, getCanonicalId } = require('./skill-manifest');
|
||||
|
||||
/**
|
||||
* Helpers for gathering BMAD agents/tasks from the installed tree.
|
||||
|
|
@ -35,7 +34,6 @@ async function getAgentsFromBmad(bmadDir, selectedModules = []) {
|
|||
|
||||
const agentDirPath = path.join(standaloneAgentsDir, agentDir.name);
|
||||
const agentFiles = await fs.readdir(agentDirPath);
|
||||
const skillManifest = await loadSkillManifest(agentDirPath);
|
||||
|
||||
for (const file of agentFiles) {
|
||||
if (!file.endsWith('.md')) continue;
|
||||
|
|
@ -50,7 +48,6 @@ async function getAgentsFromBmad(bmadDir, selectedModules = []) {
|
|||
path: filePath,
|
||||
name: file.replace('.md', ''),
|
||||
module: 'standalone', // Mark as standalone agent
|
||||
canonicalId: getCanonicalId(skillManifest, file),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -87,7 +84,6 @@ async function getAgentsFromDir(dirPath, moduleName, relativePath = '') {
|
|||
}
|
||||
|
||||
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
||||
const skillManifest = await loadSkillManifest(dirPath);
|
||||
|
||||
for (const entry of entries) {
|
||||
// Skip if entry.name is undefined or not a string
|
||||
|
|
@ -128,7 +124,6 @@ async function getAgentsFromDir(dirPath, moduleName, relativePath = '') {
|
|||
name: entry.name.replace('.md', ''),
|
||||
module: moduleName,
|
||||
relativePath: newRelativePath, // Keep the .md extension for the full path
|
||||
canonicalId: getCanonicalId(skillManifest, entry.name),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -144,7 +139,6 @@ async function getTasksFromDir(dirPath, moduleName) {
|
|||
}
|
||||
|
||||
const files = await fs.readdir(dirPath);
|
||||
const skillManifest = await loadSkillManifest(dirPath);
|
||||
|
||||
for (const file of files) {
|
||||
// Include both .md and .xml task files
|
||||
|
|
@ -166,7 +160,6 @@ async function getTasksFromDir(dirPath, moduleName) {
|
|||
path: filePath,
|
||||
name: file.replace(ext, ''),
|
||||
module: moduleName,
|
||||
canonicalId: getCanonicalId(skillManifest, file),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -264,21 +264,6 @@ function parseUnderscoreName(filename) {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the skill name for an artifact.
|
||||
* Prefers canonicalId from a bmad-skill-manifest.yaml sidecar when available,
|
||||
* falling back to the path-derived name from toDashPath().
|
||||
*
|
||||
* @param {Object} artifact - Artifact object (must have relativePath; may have canonicalId)
|
||||
* @returns {string} Filename like 'bmad-create-prd.md' or 'bmad-agent-bmm-pm.md'
|
||||
*/
|
||||
function resolveSkillName(artifact) {
|
||||
if (artifact.canonicalId) {
|
||||
return `${artifact.canonicalId}.md`;
|
||||
}
|
||||
return toDashPath(artifact.relativePath);
|
||||
}
|
||||
|
||||
// Backward compatibility aliases (colon format was same as underscore)
|
||||
const toColonName = toUnderscoreName;
|
||||
const toColonPath = toUnderscorePath;
|
||||
|
|
@ -290,7 +275,6 @@ module.exports = {
|
|||
// New standard (dash-based)
|
||||
toDashName,
|
||||
toDashPath,
|
||||
resolveSkillName,
|
||||
customAgentDashName,
|
||||
isDashFormat,
|
||||
parseDashName,
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
const path = require('node:path');
|
||||
const fs = require('fs-extra');
|
||||
const yaml = require('yaml');
|
||||
|
||||
/**
|
||||
* Load bmad-skill-manifest.yaml from a directory.
|
||||
* Single-entry manifests (canonicalId at top level) apply to all files in the directory.
|
||||
* Multi-entry manifests are keyed by source filename.
|
||||
* @param {string} dirPath - Directory to check for bmad-skill-manifest.yaml
|
||||
* @returns {Object|null} Parsed manifest or null
|
||||
*/
|
||||
async function loadSkillManifest(dirPath) {
|
||||
const manifestPath = path.join(dirPath, 'bmad-skill-manifest.yaml');
|
||||
try {
|
||||
if (!(await fs.pathExists(manifestPath))) return null;
|
||||
const content = await fs.readFile(manifestPath, 'utf8');
|
||||
const parsed = yaml.parse(content);
|
||||
if (!parsed || typeof parsed !== 'object') return null;
|
||||
if (parsed.canonicalId) return { __single: parsed };
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
console.warn(`Warning: Failed to parse bmad-skill-manifest.yaml in ${dirPath}: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the canonicalId for a specific file from a loaded skill manifest.
|
||||
* @param {Object|null} manifest - Loaded manifest (from loadSkillManifest)
|
||||
* @param {string} filename - Source filename to look up (e.g., 'pm.md', 'help.md', 'pm.agent.yaml')
|
||||
* @returns {string} canonicalId or empty string
|
||||
*/
|
||||
function getCanonicalId(manifest, filename) {
|
||||
if (!manifest) return '';
|
||||
// Single-entry manifest applies to all files in the directory
|
||||
if (manifest.__single) return manifest.__single.canonicalId || '';
|
||||
// Multi-entry: look up by filename directly
|
||||
if (manifest[filename]) return manifest[filename].canonicalId || '';
|
||||
// Fallback: try alternate extensions for compiled files
|
||||
const baseName = filename.replace(/\.(md|xml)$/i, '');
|
||||
const agentKey = `${baseName}.agent.yaml`;
|
||||
if (manifest[agentKey]) return manifest[agentKey].canonicalId || '';
|
||||
const xmlKey = `${baseName}.xml`;
|
||||
if (manifest[xmlKey]) return manifest[xmlKey].canonicalId || '';
|
||||
return '';
|
||||
}
|
||||
|
||||
module.exports = { loadSkillManifest, getCanonicalId };
|
||||
|
|
@ -50,7 +50,6 @@ class TaskToolCommandGenerator {
|
|||
displayName: task.displayName || task.name,
|
||||
description: task.description || `Execute ${task.displayName || task.name}`,
|
||||
module: task.module,
|
||||
canonicalId: task.canonicalId || '',
|
||||
// Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows)
|
||||
relativePath: `${task.module}/tasks/${task.name}${taskExt}`,
|
||||
path: taskPath,
|
||||
|
|
@ -76,7 +75,6 @@ class TaskToolCommandGenerator {
|
|||
displayName: tool.displayName || tool.name,
|
||||
description: tool.description || `Execute ${tool.displayName || tool.name}`,
|
||||
module: tool.module,
|
||||
canonicalId: tool.canonicalId || '',
|
||||
// Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows)
|
||||
relativePath: `${tool.module}/tools/${tool.name}${toolExt}`,
|
||||
path: toolPath,
|
||||
|
|
|
|||
|
|
@ -93,7 +93,6 @@ class WorkflowCommandGenerator {
|
|||
name: workflow.name,
|
||||
description: workflow.description || `${workflow.name} workflow`,
|
||||
module: workflow.module,
|
||||
canonicalId: workflow.canonicalId || '',
|
||||
relativePath: path.join(workflow.module, 'workflows', `${workflow.name}.md`),
|
||||
workflowPath: workflowRelPath, // Relative path to actual workflow file
|
||||
content: commandContent,
|
||||
|
|
|
|||
|
|
@ -1,215 +0,0 @@
|
|||
# Native Skills Migration Checklist
|
||||
|
||||
Branch: `refactor/all-is-skills`
|
||||
|
||||
Scope: migrate the BMAD-supported platforms that fully support the Agent Skills standard from legacy installer outputs to native skills output.
|
||||
|
||||
Current branch status:
|
||||
|
||||
- `Claude Code` has already been moved to `.claude/skills`
|
||||
- `Codex CLI` has already been moved to `.agents/skills`
|
||||
|
||||
This checklist now includes those completed platforms plus the remaining full-support platforms.
|
||||
|
||||
## Claude Code
|
||||
|
||||
Support assumption: full Agent Skills support. BMAD has already migrated from `.claude/commands` to `.claude/skills`.
|
||||
|
||||
- [ ] Confirm current implementation still matches Claude Code skills expectations
|
||||
- [ ] Confirm legacy cleanup for `.claude/commands`
|
||||
- [ ] Test fresh install
|
||||
- [ ] Test reinstall/upgrade from legacy command output
|
||||
- [ ] Confirm ancestor conflict protection
|
||||
- [ ] Implement/extend automated tests as needed
|
||||
- [ ] Commit any follow-up fixes if required
|
||||
|
||||
## Codex CLI
|
||||
|
||||
Support assumption: full Agent Skills support. BMAD has already migrated from `.codex/prompts` to `.agents/skills`.
|
||||
|
||||
- [ ] Confirm current implementation still matches Codex CLI skills expectations
|
||||
- [ ] Confirm legacy cleanup for project and global `.codex/prompts`
|
||||
- [ ] Test fresh install
|
||||
- [ ] Test reinstall/upgrade from legacy prompt output
|
||||
- [x] Confirm ancestor conflict protection because Codex inherits parent-directory `.agents/skills`
|
||||
- [ ] Implement/extend automated tests as needed
|
||||
- [ ] Commit any follow-up fixes if required
|
||||
|
||||
## Cursor
|
||||
|
||||
Support assumption: full Agent Skills support. BMAD currently installs legacy command files to `.cursor/commands`; target should move to a native skills directory.
|
||||
|
||||
- [x] Confirm current Cursor skills path and that BMAD should target `.cursor/skills`
|
||||
- [x] Implement installer migration to native skills output
|
||||
- [x] Add legacy cleanup for `.cursor/commands`
|
||||
- [x] Test fresh install
|
||||
- [x] Test reinstall/upgrade from legacy command output
|
||||
- [x] Confirm no ancestor conflict protection is needed because a child workspace surfaced child `.cursor/skills` entries but not a parent-only skill during manual verification
|
||||
- [ ] Implement/extend automated tests
|
||||
- [x] Commit
|
||||
|
||||
## Windsurf
|
||||
|
||||
Support assumption: full Agent Skills support. Windsurf docs confirm workspace skills at `.windsurf/skills` and global skills at `~/.codeium/windsurf/skills`. BMAD has now migrated from `.windsurf/workflows` to `.windsurf/skills`. Manual verification also confirmed that Windsurf custom skills are triggered via `@skill-name`, not slash commands.
|
||||
|
||||
- [x] Confirm Windsurf native skills directory as `.windsurf/skills`
|
||||
- [x] Implement installer migration to native skills output
|
||||
- [x] Add legacy cleanup for `.windsurf/workflows`
|
||||
- [x] Test fresh install
|
||||
- [x] Test reinstall/upgrade from legacy workflow output
|
||||
- [x] Confirm no ancestor conflict protection is needed because manual Windsurf verification showed child-local `@` skills loaded while a parent-only skill was not inherited
|
||||
- [x] Implement/extend automated tests
|
||||
- [x] Commit
|
||||
|
||||
## Cline
|
||||
|
||||
Support assumption: full Agent Skills support. BMAD currently installs workflow files to `.clinerules/workflows`; target should move to the platform's native skills directory.
|
||||
|
||||
- [ ] Confirm current Cline skills path and whether `.cline/skills` is the correct BMAD target
|
||||
- [ ] Implement installer migration to native skills output
|
||||
- [ ] Add legacy cleanup for `.clinerules/workflows`
|
||||
- [ ] Test fresh install
|
||||
- [ ] Test reinstall/upgrade from legacy workflow output
|
||||
- [ ] Confirm ancestor conflict protection where applicable
|
||||
- [ ] Implement/extend automated tests
|
||||
- [ ] Commit
|
||||
|
||||
## Google Antigravity
|
||||
|
||||
Support assumption: full Agent Skills support. Antigravity docs confirm workspace skills at `.agent/skills/<skill-folder>/` and global skills at `~/.gemini/antigravity/skills/<skill-folder>/`. BMAD has now migrated from `.agent/workflows` to `.agent/skills`.
|
||||
|
||||
- [x] Confirm Antigravity native skills path and project/global precedence
|
||||
- [x] Implement installer migration to native skills output
|
||||
- [x] Add legacy cleanup for `.agent/workflows`
|
||||
- [x] Test fresh install
|
||||
- [x] Test reinstall/upgrade from legacy workflow output
|
||||
- [x] Confirm no ancestor conflict protection is needed because manual Antigravity verification in `/tmp/antigravity-ancestor-repro/parent/child` showed only the child-local `child-only` skill, with no inherited parent `.agent/skills` entry
|
||||
- [x] Implement/extend automated tests
|
||||
- [x] Commit
|
||||
|
||||
## Auggie
|
||||
|
||||
Support assumption: full Agent Skills support. BMAD currently installs commands to `.augment/commands`; target should move to `.augment/skills`.
|
||||
|
||||
- [x] Confirm Auggie native skills path and compatibility loading from `.claude/skills` and `.agents/skills` via Augment docs plus local `auggie --print` repros
|
||||
- [x] Implement installer migration to native skills output
|
||||
- [x] Add legacy cleanup for `.augment/commands`
|
||||
- [x] Test fresh install
|
||||
- [x] Test reinstall/upgrade from legacy command output
|
||||
- [x] Confirm no ancestor conflict protection is needed because local `auggie --workspace-root` repro showed child-local `.augment/skills` loading `child-only` but not parent `parent-only`
|
||||
- [x] Implement/extend automated tests
|
||||
- [ ] Commit
|
||||
|
||||
## CodeBuddy
|
||||
|
||||
Support assumption: full Agent Skills support. BMAD currently installs commands to `.codebuddy/commands`; target should move to `.codebuddy/skills`.
|
||||
|
||||
- [ ] Confirm CodeBuddy native skills path and any naming/frontmatter requirements
|
||||
- [ ] Implement installer migration to native skills output
|
||||
- [ ] Add legacy cleanup for `.codebuddy/commands`
|
||||
- [ ] Test fresh install
|
||||
- [ ] Test reinstall/upgrade from legacy command output
|
||||
- [ ] Confirm ancestor conflict protection where applicable
|
||||
- [ ] Implement/extend automated tests
|
||||
- [ ] Commit
|
||||
|
||||
## Crush
|
||||
|
||||
Support assumption: full Agent Skills support. BMAD currently installs commands to `.crush/commands`; target should move to the platform's native skills location.
|
||||
|
||||
- [ ] Confirm Crush project-local versus global skills path and BMAD's preferred install target
|
||||
- [ ] Implement installer migration to native skills output
|
||||
- [ ] Add legacy cleanup for `.crush/commands`
|
||||
- [ ] Test fresh install
|
||||
- [ ] Test reinstall/upgrade from legacy command output
|
||||
- [ ] Confirm ancestor conflict protection where applicable
|
||||
- [ ] Implement/extend automated tests
|
||||
- [ ] Commit
|
||||
|
||||
## Kiro
|
||||
|
||||
Support assumption: full Agent Skills support. Kiro docs confirm project skills at `.kiro/skills/<skill-name>/SKILL.md` and describe steering as a separate rules mechanism, not a required compatibility layer. BMAD has now migrated from `.kiro/steering` to `.kiro/skills`. Manual app verification also confirmed that Kiro can surface skills in Slash when the relevant UI setting is enabled, and that it does not inherit ancestor `.kiro/skills` directories.
|
||||
|
||||
- [x] Confirm Kiro skills path and verify BMAD should stop writing steering artifacts for this migration
|
||||
- [x] Implement installer migration to native skills output
|
||||
- [x] Add legacy cleanup for `.kiro/steering`
|
||||
- [x] Test fresh install
|
||||
- [x] Test reinstall/upgrade from legacy steering output
|
||||
- [x] Confirm no ancestor conflict protection is needed because manual Kiro verification showed Slash-visible skills from the current workspace only, with no ancestor `.kiro/skills` inheritance
|
||||
- [x] Implement/extend automated tests
|
||||
- [x] Commit
|
||||
|
||||
## OpenCode
|
||||
|
||||
Support assumption: full Agent Skills support. BMAD currently splits output between `.opencode/agents` and `.opencode/commands`; target should consolidate to `.opencode/skills`.
|
||||
|
||||
- [x] Confirm OpenCode native skills path and compatibility loading from `.claude/skills` and `.agents/skills` in OpenCode docs and with local `opencode run` repros
|
||||
- [x] Implement installer migration from multi-target legacy output to single native skills target
|
||||
- [x] Add legacy cleanup for `.opencode/agents`, `.opencode/commands`, `.opencode/agent`, and `.opencode/command`
|
||||
- [x] Test fresh install
|
||||
- [x] Test reinstall/upgrade from split legacy output
|
||||
- [x] Confirm ancestor conflict protection is required because local `opencode run` repros loaded both child-local `child-only` and ancestor `parent-only`, matching the docs that project-local skill discovery walks upward to the git worktree
|
||||
- [x] Implement/extend automated tests
|
||||
- [ ] Commit
|
||||
|
||||
## Roo Code
|
||||
|
||||
Support assumption: full Agent Skills support. BMAD currently installs commands to `.roo/commands`; target should move to `.roo/skills` or the correct mode-aware skill directories.
|
||||
|
||||
- [ ] Confirm Roo native skills path and whether BMAD should use generic or mode-specific skill directories
|
||||
- [ ] Implement installer migration to native skills output
|
||||
- [ ] Add legacy cleanup for `.roo/commands`
|
||||
- [ ] Test fresh install
|
||||
- [ ] Test reinstall/upgrade from legacy command output
|
||||
- [ ] Confirm ancestor conflict protection where applicable
|
||||
- [ ] Implement/extend automated tests
|
||||
- [ ] Commit
|
||||
|
||||
## Trae
|
||||
|
||||
Support assumption: full Agent Skills support. BMAD currently installs rule files to `.trae/rules`; target should move to the platform's native skills directory.
|
||||
|
||||
- [ ] Confirm Trae native skills path and whether the current `.trae/rules` path is still required for compatibility
|
||||
- [ ] Implement installer migration to native skills output
|
||||
- [ ] Add legacy cleanup for `.trae/rules`
|
||||
- [ ] Test fresh install
|
||||
- [ ] Test reinstall/upgrade from legacy rules output
|
||||
- [ ] Confirm ancestor conflict protection where applicable
|
||||
- [ ] Implement/extend automated tests
|
||||
- [ ] Commit
|
||||
|
||||
## GitHub Copilot
|
||||
|
||||
Support assumption: full Agent Skills support. BMAD currently uses a custom installer that generates `.github/agents`, `.github/prompts`, and `.github/copilot-instructions.md`; target should move to `.github/skills`.
|
||||
|
||||
- [ ] Confirm GitHub Copilot native skills path and whether `.github/agents` remains necessary as a compatibility layer
|
||||
- [ ] Design the migration away from the custom prompt/agent installer model
|
||||
- [ ] Implement native skills output, ideally with shared config-driven code where practical
|
||||
- [ ] Add legacy cleanup for `.github/agents`, `.github/prompts`, and any BMAD-owned Copilot instruction file behavior that should be retired
|
||||
- [ ] Test fresh install
|
||||
- [ ] Test reinstall/upgrade from legacy custom installer output
|
||||
- [ ] Confirm ancestor conflict protection where applicable
|
||||
- [ ] Implement/extend automated tests
|
||||
- [ ] Commit
|
||||
|
||||
## KiloCoder
|
||||
|
||||
Support assumption: full Agent Skills support. BMAD currently uses a custom installer that writes `.kilocodemodes` and `.kilocode/workflows`; target should move to native skills output.
|
||||
|
||||
- [ ] Confirm KiloCoder native skills path and whether `.kilocodemodes` should be removed entirely or retained temporarily for compatibility
|
||||
- [ ] Design the migration away from modes plus workflow markdown
|
||||
- [ ] Implement native skills output
|
||||
- [ ] Add legacy cleanup for `.kilocode/workflows` and BMAD-owned entries in `.kilocodemodes`
|
||||
- [ ] Test fresh install
|
||||
- [ ] Test reinstall/upgrade from legacy custom installer output
|
||||
- [ ] Confirm ancestor conflict protection where applicable
|
||||
- [ ] Implement/extend automated tests
|
||||
- [ ] Commit
|
||||
|
||||
## Summary Gates
|
||||
|
||||
- [ ] All full-support BMAD platforms install `SKILL.md` directory-based output
|
||||
- [ ] No full-support platform still emits BMAD command/workflow/rule files as its primary install format
|
||||
- [ ] Legacy cleanup paths are defined for every migrated platform
|
||||
- [ ] Automated coverage exists for config-driven and custom-installer migrations
|
||||
- [ ] Installer docs and migration notes updated after code changes land
|
||||
Loading…
Reference in New Issue