refactor(installer): remove legacy workflow, task, and agent IDE generators (#2078)
* refactor(installer): remove legacy workflow, task, and agent IDE generators All platforms now use skill_format exclusively. The old WorkflowCommandGenerator, TaskToolCommandGenerator, and AgentCommandGenerator code paths in _config-driven.js were no-ops — collectSkills claims every directory before the legacy collectors run, making their manifests empty. Removed: - workflow-command-generator.js (deleted) - task-tool-command-generator.js (deleted) - writeAgentArtifacts, writeWorkflowArtifacts, writeTaskToolArtifacts - AgentCommandGenerator import from _config-driven.js - Legacy artifact_types/agents/workflows/tasks result fields Simplified installToTarget, installToMultipleTargets, printSummary, and IDE manager detail builder to skills-only. Updated test fixture to use SKILL.md format instead of old agent format. * fix(installer): address PR review findings from #2078 - Fix temp dir leak in test fixture cleanup (use path.dirname) - Fail loudly when skill_format missing instead of silent success - Add workflow.md to test fixture for verbatim-copy coverage 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
a2839cbee0
commit
1cb913523e
|
|
@ -49,34 +49,38 @@ function assert(condition, testName, errorMessage = '') {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createTestBmadFixture() {
|
async function createTestBmadFixture() {
|
||||||
const fixtureDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-fixture-'));
|
const fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-fixture-'));
|
||||||
|
const fixtureDir = path.join(fixtureRoot, '_bmad');
|
||||||
|
await fs.ensureDir(fixtureDir);
|
||||||
|
|
||||||
// Minimal workflow manifest (generators check for this)
|
// Skill manifest CSV — the sole source of truth for IDE skill installation
|
||||||
await fs.ensureDir(path.join(fixtureDir, '_config'));
|
await fs.ensureDir(path.join(fixtureDir, '_config'));
|
||||||
await fs.writeFile(path.join(fixtureDir, '_config', 'workflow-manifest.csv'), '');
|
await fs.writeFile(
|
||||||
|
path.join(fixtureDir, '_config', 'skill-manifest.csv'),
|
||||||
|
[
|
||||||
|
'canonicalId,name,description,module,path,install_to_bmad',
|
||||||
|
'"bmad-master","bmad-master","Minimal test agent fixture","core","_bmad/core/bmad-master/SKILL.md","true"',
|
||||||
|
'',
|
||||||
|
].join('\n'),
|
||||||
|
);
|
||||||
|
|
||||||
// Minimal compiled agent for core/agents (contains <agent tag and frontmatter)
|
// Minimal SKILL.md for the skill entry
|
||||||
const minimalAgent = [
|
const skillDir = path.join(fixtureDir, 'core', 'bmad-master');
|
||||||
|
await fs.ensureDir(skillDir);
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(skillDir, 'SKILL.md'),
|
||||||
|
[
|
||||||
'---',
|
'---',
|
||||||
'name: "test agent"',
|
'name: bmad-master',
|
||||||
'description: "Minimal test agent fixture"',
|
'description: Minimal test agent fixture',
|
||||||
'---',
|
'---',
|
||||||
'',
|
'',
|
||||||
|
'<!-- agent-activation -->',
|
||||||
'You are a test agent.',
|
'You are a test agent.',
|
||||||
'',
|
].join('\n'),
|
||||||
'<agent id="test-agent.agent.yaml" name="Test Agent" title="Test Agent">',
|
);
|
||||||
'<persona>Test persona</persona>',
|
await fs.writeFile(path.join(skillDir, 'bmad-skill-manifest.yaml'), 'SKILL.md:\n type: skill\n');
|
||||||
'</agent>',
|
await fs.writeFile(path.join(skillDir, 'workflow.md'), '# Test Workflow\nStep 1: Do the thing.\n');
|
||||||
].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;
|
return fixtureDir;
|
||||||
}
|
}
|
||||||
|
|
@ -253,7 +257,7 @@ async function runTests() {
|
||||||
assert(!(await fs.pathExists(path.join(tempProjectDir, '.windsurf', 'workflows'))), 'Windsurf setup removes legacy workflows dir');
|
assert(!(await fs.pathExists(path.join(tempProjectDir, '.windsurf', 'workflows'))), 'Windsurf setup removes legacy workflows dir');
|
||||||
|
|
||||||
await fs.remove(tempProjectDir);
|
await fs.remove(tempProjectDir);
|
||||||
await fs.remove(installedBmadDir);
|
await fs.remove(path.dirname(installedBmadDir));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
assert(false, 'Windsurf native skills migration test succeeds', error.message);
|
assert(false, 'Windsurf native skills migration test succeeds', error.message);
|
||||||
}
|
}
|
||||||
|
|
@ -301,7 +305,7 @@ async function runTests() {
|
||||||
assert(!(await fs.pathExists(path.join(tempProjectDir, '.kiro', 'steering'))), 'Kiro setup removes legacy steering dir');
|
assert(!(await fs.pathExists(path.join(tempProjectDir, '.kiro', 'steering'))), 'Kiro setup removes legacy steering dir');
|
||||||
|
|
||||||
await fs.remove(tempProjectDir);
|
await fs.remove(tempProjectDir);
|
||||||
await fs.remove(installedBmadDir);
|
await fs.remove(path.dirname(installedBmadDir));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
assert(false, 'Kiro native skills migration test succeeds', error.message);
|
assert(false, 'Kiro native skills migration test succeeds', error.message);
|
||||||
}
|
}
|
||||||
|
|
@ -349,7 +353,7 @@ async function runTests() {
|
||||||
assert(!(await fs.pathExists(path.join(tempProjectDir, '.agent', 'workflows'))), 'Antigravity setup removes legacy workflows dir');
|
assert(!(await fs.pathExists(path.join(tempProjectDir, '.agent', 'workflows'))), 'Antigravity setup removes legacy workflows dir');
|
||||||
|
|
||||||
await fs.remove(tempProjectDir);
|
await fs.remove(tempProjectDir);
|
||||||
await fs.remove(installedBmadDir);
|
await fs.remove(path.dirname(installedBmadDir));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
assert(false, 'Antigravity native skills migration test succeeds', error.message);
|
assert(false, 'Antigravity native skills migration test succeeds', error.message);
|
||||||
}
|
}
|
||||||
|
|
@ -402,7 +406,7 @@ async function runTests() {
|
||||||
assert(!(await fs.pathExists(path.join(tempProjectDir, '.augment', 'commands'))), 'Auggie setup removes legacy commands dir');
|
assert(!(await fs.pathExists(path.join(tempProjectDir, '.augment', 'commands'))), 'Auggie setup removes legacy commands dir');
|
||||||
|
|
||||||
await fs.remove(tempProjectDir);
|
await fs.remove(tempProjectDir);
|
||||||
await fs.remove(installedBmadDir);
|
await fs.remove(path.dirname(installedBmadDir));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
assert(false, 'Auggie native skills migration test succeeds', error.message);
|
assert(false, 'Auggie native skills migration test succeeds', error.message);
|
||||||
}
|
}
|
||||||
|
|
@ -468,7 +472,7 @@ async function runTests() {
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.remove(tempProjectDir);
|
await fs.remove(tempProjectDir);
|
||||||
await fs.remove(installedBmadDir);
|
await fs.remove(path.dirname(installedBmadDir));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
assert(false, 'OpenCode native skills migration test succeeds', error.message);
|
assert(false, 'OpenCode native skills migration test succeeds', error.message);
|
||||||
}
|
}
|
||||||
|
|
@ -522,7 +526,7 @@ async function runTests() {
|
||||||
assert(!(await fs.pathExists(legacyDir9)), 'Claude Code setup removes legacy commands dir');
|
assert(!(await fs.pathExists(legacyDir9)), 'Claude Code setup removes legacy commands dir');
|
||||||
|
|
||||||
await fs.remove(tempProjectDir9);
|
await fs.remove(tempProjectDir9);
|
||||||
await fs.remove(installedBmadDir9);
|
await fs.remove(path.dirname(installedBmadDir9));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
assert(false, 'Claude Code native skills migration test succeeds', error.message);
|
assert(false, 'Claude Code native skills migration test succeeds', error.message);
|
||||||
}
|
}
|
||||||
|
|
@ -561,7 +565,7 @@ async function runTests() {
|
||||||
);
|
);
|
||||||
|
|
||||||
await fs.remove(tempRoot10);
|
await fs.remove(tempRoot10);
|
||||||
await fs.remove(installedBmadDir10);
|
await fs.remove(path.dirname(installedBmadDir10));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
assert(false, 'Claude Code ancestor conflict protection test succeeds', error.message);
|
assert(false, 'Claude Code ancestor conflict protection test succeeds', error.message);
|
||||||
}
|
}
|
||||||
|
|
@ -615,7 +619,7 @@ async function runTests() {
|
||||||
assert(!(await fs.pathExists(legacyDir11)), 'Codex setup removes legacy prompts dir');
|
assert(!(await fs.pathExists(legacyDir11)), 'Codex setup removes legacy prompts dir');
|
||||||
|
|
||||||
await fs.remove(tempProjectDir11);
|
await fs.remove(tempProjectDir11);
|
||||||
await fs.remove(installedBmadDir11);
|
await fs.remove(path.dirname(installedBmadDir11));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
assert(false, 'Codex native skills migration test succeeds', error.message);
|
assert(false, 'Codex native skills migration test succeeds', error.message);
|
||||||
}
|
}
|
||||||
|
|
@ -651,7 +655,7 @@ async function runTests() {
|
||||||
assert(result12.handlerResult?.conflictDir === expectedConflictDir12, 'Codex ancestor rejection points at ancestor .agents/skills dir');
|
assert(result12.handlerResult?.conflictDir === expectedConflictDir12, 'Codex ancestor rejection points at ancestor .agents/skills dir');
|
||||||
|
|
||||||
await fs.remove(tempRoot12);
|
await fs.remove(tempRoot12);
|
||||||
await fs.remove(installedBmadDir12);
|
await fs.remove(path.dirname(installedBmadDir12));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
assert(false, 'Codex ancestor conflict protection test succeeds', error.message);
|
assert(false, 'Codex ancestor conflict protection test succeeds', error.message);
|
||||||
}
|
}
|
||||||
|
|
@ -705,7 +709,7 @@ async function runTests() {
|
||||||
assert(!(await fs.pathExists(legacyDir13c)), 'Cursor setup removes legacy commands dir');
|
assert(!(await fs.pathExists(legacyDir13c)), 'Cursor setup removes legacy commands dir');
|
||||||
|
|
||||||
await fs.remove(tempProjectDir13c);
|
await fs.remove(tempProjectDir13c);
|
||||||
await fs.remove(installedBmadDir13c);
|
await fs.remove(path.dirname(installedBmadDir13c));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
assert(false, 'Cursor native skills migration test succeeds', error.message);
|
assert(false, 'Cursor native skills migration test succeeds', error.message);
|
||||||
}
|
}
|
||||||
|
|
@ -770,7 +774,7 @@ async function runTests() {
|
||||||
assert(await fs.pathExists(skillFile13), 'Roo reinstall preserves SKILL.md output');
|
assert(await fs.pathExists(skillFile13), 'Roo reinstall preserves SKILL.md output');
|
||||||
|
|
||||||
await fs.remove(tempProjectDir13);
|
await fs.remove(tempProjectDir13);
|
||||||
await fs.remove(installedBmadDir13);
|
await fs.remove(path.dirname(installedBmadDir13));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
assert(false, 'Roo native skills migration test succeeds', error.message);
|
assert(false, 'Roo native skills migration test succeeds', error.message);
|
||||||
}
|
}
|
||||||
|
|
@ -809,7 +813,7 @@ async function runTests() {
|
||||||
);
|
);
|
||||||
|
|
||||||
await fs.remove(tempRoot);
|
await fs.remove(tempRoot);
|
||||||
await fs.remove(installedBmadDir);
|
await fs.remove(path.dirname(installedBmadDir));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
assert(false, 'OpenCode ancestor conflict protection test succeeds', error.message);
|
assert(false, 'OpenCode ancestor conflict protection test succeeds', error.message);
|
||||||
}
|
}
|
||||||
|
|
@ -895,7 +899,7 @@ async function runTests() {
|
||||||
);
|
);
|
||||||
|
|
||||||
await fs.remove(tempProjectDir17);
|
await fs.remove(tempProjectDir17);
|
||||||
await fs.remove(installedBmadDir17);
|
await fs.remove(path.dirname(installedBmadDir17));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
assert(false, 'GitHub Copilot native skills migration test succeeds', error.message);
|
assert(false, 'GitHub Copilot native skills migration test succeeds', error.message);
|
||||||
}
|
}
|
||||||
|
|
@ -957,7 +961,7 @@ async function runTests() {
|
||||||
assert(await fs.pathExists(skillFile18), 'Cline reinstall preserves SKILL.md output');
|
assert(await fs.pathExists(skillFile18), 'Cline reinstall preserves SKILL.md output');
|
||||||
|
|
||||||
await fs.remove(tempProjectDir18);
|
await fs.remove(tempProjectDir18);
|
||||||
await fs.remove(installedBmadDir18);
|
await fs.remove(path.dirname(installedBmadDir18));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
assert(false, 'Cline native skills migration test succeeds', error.message);
|
assert(false, 'Cline native skills migration test succeeds', error.message);
|
||||||
}
|
}
|
||||||
|
|
@ -1017,7 +1021,7 @@ async function runTests() {
|
||||||
assert(await fs.pathExists(skillFile19), 'CodeBuddy reinstall preserves SKILL.md output');
|
assert(await fs.pathExists(skillFile19), 'CodeBuddy reinstall preserves SKILL.md output');
|
||||||
|
|
||||||
await fs.remove(tempProjectDir19);
|
await fs.remove(tempProjectDir19);
|
||||||
await fs.remove(installedBmadDir19);
|
await fs.remove(path.dirname(installedBmadDir19));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
assert(false, 'CodeBuddy native skills migration test succeeds', error.message);
|
assert(false, 'CodeBuddy native skills migration test succeeds', error.message);
|
||||||
}
|
}
|
||||||
|
|
@ -1077,7 +1081,7 @@ async function runTests() {
|
||||||
assert(await fs.pathExists(skillFile20), 'Crush reinstall preserves SKILL.md output');
|
assert(await fs.pathExists(skillFile20), 'Crush reinstall preserves SKILL.md output');
|
||||||
|
|
||||||
await fs.remove(tempProjectDir20);
|
await fs.remove(tempProjectDir20);
|
||||||
await fs.remove(installedBmadDir20);
|
await fs.remove(path.dirname(installedBmadDir20));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
assert(false, 'Crush native skills migration test succeeds', error.message);
|
assert(false, 'Crush native skills migration test succeeds', error.message);
|
||||||
}
|
}
|
||||||
|
|
@ -1136,7 +1140,7 @@ async function runTests() {
|
||||||
assert(await fs.pathExists(skillFile21), 'Trae reinstall preserves SKILL.md output');
|
assert(await fs.pathExists(skillFile21), 'Trae reinstall preserves SKILL.md output');
|
||||||
|
|
||||||
await fs.remove(tempProjectDir21);
|
await fs.remove(tempProjectDir21);
|
||||||
await fs.remove(installedBmadDir21);
|
await fs.remove(path.dirname(installedBmadDir21));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
assert(false, 'Trae native skills migration test succeeds', error.message);
|
assert(false, 'Trae native skills migration test succeeds', error.message);
|
||||||
}
|
}
|
||||||
|
|
@ -1194,7 +1198,7 @@ async function runTests() {
|
||||||
);
|
);
|
||||||
|
|
||||||
await fs.remove(tempProjectDir22);
|
await fs.remove(tempProjectDir22);
|
||||||
await fs.remove(installedBmadDir22);
|
await fs.remove(path.dirname(installedBmadDir22));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
assert(false, 'KiloCoder suspended test succeeds', error.message);
|
assert(false, 'KiloCoder suspended test succeeds', error.message);
|
||||||
}
|
}
|
||||||
|
|
@ -1253,7 +1257,7 @@ async function runTests() {
|
||||||
assert(await fs.pathExists(skillFile23), 'Gemini reinstall preserves SKILL.md output');
|
assert(await fs.pathExists(skillFile23), 'Gemini reinstall preserves SKILL.md output');
|
||||||
|
|
||||||
await fs.remove(tempProjectDir23);
|
await fs.remove(tempProjectDir23);
|
||||||
await fs.remove(installedBmadDir23);
|
await fs.remove(path.dirname(installedBmadDir23));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
assert(false, 'Gemini native skills migration test succeeds', error.message);
|
assert(false, 'Gemini native skills migration test succeeds', error.message);
|
||||||
}
|
}
|
||||||
|
|
@ -1303,7 +1307,7 @@ async function runTests() {
|
||||||
assert(!(await fs.pathExists(path.join(tempProjectDir24, '.iflow', 'commands'))), 'iFlow setup removes legacy commands dir');
|
assert(!(await fs.pathExists(path.join(tempProjectDir24, '.iflow', 'commands'))), 'iFlow setup removes legacy commands dir');
|
||||||
|
|
||||||
await fs.remove(tempProjectDir24);
|
await fs.remove(tempProjectDir24);
|
||||||
await fs.remove(installedBmadDir24);
|
await fs.remove(path.dirname(installedBmadDir24));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
assert(false, 'iFlow native skills migration test succeeds', error.message);
|
assert(false, 'iFlow native skills migration test succeeds', error.message);
|
||||||
}
|
}
|
||||||
|
|
@ -1353,7 +1357,7 @@ async function runTests() {
|
||||||
assert(!(await fs.pathExists(path.join(tempProjectDir25, '.qwen', 'commands'))), 'QwenCoder setup removes legacy commands dir');
|
assert(!(await fs.pathExists(path.join(tempProjectDir25, '.qwen', 'commands'))), 'QwenCoder setup removes legacy commands dir');
|
||||||
|
|
||||||
await fs.remove(tempProjectDir25);
|
await fs.remove(tempProjectDir25);
|
||||||
await fs.remove(installedBmadDir25);
|
await fs.remove(path.dirname(installedBmadDir25));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
assert(false, 'QwenCoder native skills migration test succeeds', error.message);
|
assert(false, 'QwenCoder native skills migration test succeeds', error.message);
|
||||||
}
|
}
|
||||||
|
|
@ -1422,7 +1426,7 @@ async function runTests() {
|
||||||
assert(cleanedPrompts26.prompts[0].name === 'my-custom-prompt', 'Rovo Dev cleanup preserves non-BMAD entries in prompts.yml');
|
assert(cleanedPrompts26.prompts[0].name === 'my-custom-prompt', 'Rovo Dev cleanup preserves non-BMAD entries in prompts.yml');
|
||||||
|
|
||||||
await fs.remove(tempProjectDir26);
|
await fs.remove(tempProjectDir26);
|
||||||
await fs.remove(installedBmadDir26);
|
await fs.remove(path.dirname(installedBmadDir26));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
assert(false, 'Rovo Dev native skills migration test succeeds', error.message);
|
assert(false, 'Rovo Dev native skills migration test succeeds', error.message);
|
||||||
}
|
}
|
||||||
|
|
@ -1487,7 +1491,7 @@ async function runTests() {
|
||||||
assert(!(await fs.pathExists(regularSkillDir27)), 'Cleanup removes stale non-bmad-os skills');
|
assert(!(await fs.pathExists(regularSkillDir27)), 'Cleanup removes stale non-bmad-os skills');
|
||||||
|
|
||||||
await fs.remove(tempProjectDir27);
|
await fs.remove(tempProjectDir27);
|
||||||
await fs.remove(installedBmadDir27);
|
await fs.remove(path.dirname(installedBmadDir27));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
assert(false, 'bmad-os-* skill preservation test succeeds', error.message);
|
assert(false, 'bmad-os-* skill preservation test succeeds', error.message);
|
||||||
}
|
}
|
||||||
|
|
@ -1579,7 +1583,7 @@ async function runTests() {
|
||||||
assert(false, 'Pi native skills test succeeds', error.message);
|
assert(false, 'Pi native skills test succeeds', error.message);
|
||||||
} finally {
|
} finally {
|
||||||
if (tempProjectDir28) await fs.remove(tempProjectDir28).catch(() => {});
|
if (tempProjectDir28) await fs.remove(tempProjectDir28).catch(() => {});
|
||||||
if (installedBmadDir28) await fs.remove(installedBmadDir28).catch(() => {});
|
if (installedBmadDir28) await fs.remove(path.dirname(installedBmadDir28)).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
@ -1837,18 +1841,12 @@ async function runTests() {
|
||||||
});
|
});
|
||||||
|
|
||||||
assert(result.success === true, 'Antigravity setup succeeds with overlapping skill names');
|
assert(result.success === true, 'Antigravity setup succeeds with overlapping skill names');
|
||||||
assert(result.detail === '2 agents', 'Installer detail reports agents separately from skills');
|
assert(result.detail === '1 skills', 'Installer detail reports skill count');
|
||||||
assert(result.handlerResult.results.skillDirectories === 2, 'Result exposes unique skill directory count');
|
assert(result.handlerResult.results.skillDirectories === 1, 'Result exposes unique skill directory count');
|
||||||
assert(result.handlerResult.results.agents === 2, 'Result retains generated agent write count');
|
|
||||||
assert(result.handlerResult.results.workflows === 1, 'Result retains generated workflow count');
|
|
||||||
assert(result.handlerResult.results.skills === 1, 'Result retains verbatim skill count');
|
assert(result.handlerResult.results.skills === 1, 'Result retains verbatim skill count');
|
||||||
assert(
|
|
||||||
await fs.pathExists(path.join(collisionProjectDir, '.agent', 'skills', 'bmad-agent-bmad-master', 'SKILL.md')),
|
|
||||||
'Agent skill directory is created',
|
|
||||||
);
|
|
||||||
assert(
|
assert(
|
||||||
await fs.pathExists(path.join(collisionProjectDir, '.agent', 'skills', 'bmad-help', 'SKILL.md')),
|
await fs.pathExists(path.join(collisionProjectDir, '.agent', 'skills', 'bmad-help', 'SKILL.md')),
|
||||||
'Overlapping skill directory is created once',
|
'Skill directory is created from skill-manifest',
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
assert(false, 'Skill-format unique count test succeeds', error.message);
|
assert(false, 'Skill-format unique count test succeeds', error.message);
|
||||||
|
|
@ -1906,6 +1904,9 @@ async function runTests() {
|
||||||
const skillFile32 = path.join(tempProjectDir32, '.ona', 'skills', 'bmad-master', 'SKILL.md');
|
const skillFile32 = path.join(tempProjectDir32, '.ona', 'skills', 'bmad-master', 'SKILL.md');
|
||||||
assert(await fs.pathExists(skillFile32), 'Ona install writes SKILL.md directory output');
|
assert(await fs.pathExists(skillFile32), 'Ona install writes SKILL.md directory output');
|
||||||
|
|
||||||
|
const workflowFile32 = path.join(tempProjectDir32, '.ona', 'skills', 'bmad-master', 'workflow.md');
|
||||||
|
assert(await fs.pathExists(workflowFile32), 'Ona install copies non-SKILL.md files (workflow.md) verbatim');
|
||||||
|
|
||||||
// Parse YAML frontmatter between --- markers
|
// Parse YAML frontmatter between --- markers
|
||||||
const skillContent32 = await fs.readFile(skillFile32, 'utf8');
|
const skillContent32 = await fs.readFile(skillFile32, 'utf8');
|
||||||
const fmMatch32 = skillContent32.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
const fmMatch32 = skillContent32.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
||||||
|
|
@ -1944,7 +1945,7 @@ async function runTests() {
|
||||||
assert(false, 'Ona native skills test succeeds', error.message);
|
assert(false, 'Ona native skills test succeeds', error.message);
|
||||||
} finally {
|
} finally {
|
||||||
if (tempProjectDir32) await fs.remove(tempProjectDir32).catch(() => {});
|
if (tempProjectDir32) await fs.remove(tempProjectDir32).catch(() => {});
|
||||||
if (installedBmadDir32) await fs.remove(installedBmadDir32).catch(() => {});
|
if (installedBmadDir32) await fs.remove(path.dirname(installedBmadDir32)).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,6 @@ const fs = require('fs-extra');
|
||||||
const yaml = require('yaml');
|
const yaml = require('yaml');
|
||||||
const { BaseIdeSetup } = require('./_base-ide');
|
const { BaseIdeSetup } = require('./_base-ide');
|
||||||
const prompts = require('../../../lib/prompts');
|
const prompts = require('../../../lib/prompts');
|
||||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
|
||||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
|
||||||
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
|
|
||||||
const csv = require('csv-parse/sync');
|
const csv = require('csv-parse/sync');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -115,53 +112,20 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||||
* @returns {Promise<Object>} Installation result
|
* @returns {Promise<Object>} Installation result
|
||||||
*/
|
*/
|
||||||
async installToTarget(projectDir, bmadDir, config, options) {
|
async installToTarget(projectDir, bmadDir, config, options) {
|
||||||
const { target_dir, template_type, artifact_types } = config;
|
const { target_dir } = config;
|
||||||
|
|
||||||
// Skip targets with explicitly empty artifact_types and no verbatim skills
|
if (!config.skill_format) {
|
||||||
// This prevents creating empty directories when no artifacts will be written
|
return { success: false, reason: 'missing-skill-format', error: 'Installer config missing skill_format — cannot install skills' };
|
||||||
const skipStandardArtifacts = Array.isArray(artifact_types) && artifact_types.length === 0;
|
|
||||||
if (skipStandardArtifacts && !config.skill_format) {
|
|
||||||
return { success: true, results: { agents: 0, workflows: 0, tasks: 0, tools: 0, skills: 0 } };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetPath = path.join(projectDir, target_dir);
|
const targetPath = path.join(projectDir, target_dir);
|
||||||
await this.ensureDir(targetPath);
|
await this.ensureDir(targetPath);
|
||||||
|
|
||||||
const selectedModules = options.selectedModules || [];
|
this.skillWriteTracker = new Set();
|
||||||
const results = { agents: 0, workflows: 0, tasks: 0, tools: 0, skills: 0 };
|
const results = { skills: 0 };
|
||||||
this.skillWriteTracker = config.skill_format ? new Set() : null;
|
|
||||||
|
|
||||||
// Install standard artifacts (agents, workflows, tasks, tools)
|
|
||||||
if (!skipStandardArtifacts) {
|
|
||||||
// Install agents
|
|
||||||
if (!artifact_types || artifact_types.includes('agents')) {
|
|
||||||
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts } = await agentGen.collectAgentArtifacts(bmadDir, selectedModules);
|
|
||||||
results.agents = await this.writeAgentArtifacts(targetPath, artifacts, template_type, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install workflows
|
|
||||||
if (!artifact_types || artifact_types.includes('workflows')) {
|
|
||||||
const workflowGen = new WorkflowCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts } = await workflowGen.collectWorkflowArtifacts(bmadDir);
|
|
||||||
results.workflows = await this.writeWorkflowArtifacts(targetPath, artifacts, template_type, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install tasks and tools using template system (supports TOML for Gemini, MD for others)
|
|
||||||
if (!artifact_types || artifact_types.includes('tasks') || artifact_types.includes('tools')) {
|
|
||||||
const taskToolGen = new TaskToolCommandGenerator(this.bmadFolderName);
|
|
||||||
const { artifacts } = await taskToolGen.collectTaskToolArtifacts(bmadDir);
|
|
||||||
const taskToolResult = await this.writeTaskToolArtifacts(targetPath, artifacts, template_type, config);
|
|
||||||
results.tasks = taskToolResult.tasks || 0;
|
|
||||||
results.tools = taskToolResult.tools || 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install verbatim skills (type: skill)
|
|
||||||
if (config.skill_format) {
|
|
||||||
results.skills = await this.installVerbatimSkills(projectDir, bmadDir, targetPath, config);
|
results.skills = await this.installVerbatimSkills(projectDir, bmadDir, targetPath, config);
|
||||||
results.skillDirectories = this.skillWriteTracker ? this.skillWriteTracker.size : 0;
|
results.skillDirectories = this.skillWriteTracker.size;
|
||||||
}
|
|
||||||
|
|
||||||
await this.printSummary(results, target_dir, options);
|
await this.printSummary(results, target_dir, options);
|
||||||
this.skillWriteTracker = null;
|
this.skillWriteTracker = null;
|
||||||
|
|
@ -177,15 +141,11 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||||
* @returns {Promise<Object>} Installation result
|
* @returns {Promise<Object>} Installation result
|
||||||
*/
|
*/
|
||||||
async installToMultipleTargets(projectDir, bmadDir, targets, options) {
|
async installToMultipleTargets(projectDir, bmadDir, targets, options) {
|
||||||
const allResults = { agents: 0, workflows: 0, tasks: 0, tools: 0, skills: 0 };
|
const allResults = { skills: 0 };
|
||||||
|
|
||||||
for (const target of targets) {
|
for (const target of targets) {
|
||||||
const result = await this.installToTarget(projectDir, bmadDir, target, options);
|
const result = await this.installToTarget(projectDir, bmadDir, target, options);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
allResults.agents += result.results.agents || 0;
|
|
||||||
allResults.workflows += result.results.workflows || 0;
|
|
||||||
allResults.tasks += result.results.tasks || 0;
|
|
||||||
allResults.tools += result.results.tools || 0;
|
|
||||||
allResults.skills += result.results.skills || 0;
|
allResults.skills += result.results.skills || 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -193,118 +153,6 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
||||||
return { success: true, results: allResults };
|
return { success: true, results: allResults };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Write agent artifacts to target directory
|
|
||||||
* @param {string} targetPath - Target directory path
|
|
||||||
* @param {Array} artifacts - Agent artifacts
|
|
||||||
* @param {string} templateType - Template type to use
|
|
||||||
* @param {Object} config - Installation configuration
|
|
||||||
* @returns {Promise<number>} Count of artifacts written
|
|
||||||
*/
|
|
||||||
async writeAgentArtifacts(targetPath, artifacts, templateType, config = {}) {
|
|
||||||
// Try to load platform-specific template, fall back to default-agent
|
|
||||||
const { content: template, extension } = await this.loadTemplate(templateType, 'agent', config, 'default-agent');
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
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++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write workflow artifacts to target directory
|
|
||||||
* @param {string} targetPath - Target directory path
|
|
||||||
* @param {Array} artifacts - Workflow artifacts
|
|
||||||
* @param {string} templateType - Template type to use
|
|
||||||
* @param {Object} config - Installation configuration
|
|
||||||
* @returns {Promise<number>} Count of artifacts written
|
|
||||||
*/
|
|
||||||
async writeWorkflowArtifacts(targetPath, artifacts, templateType, config = {}) {
|
|
||||||
let count = 0;
|
|
||||||
|
|
||||||
for (const artifact of artifacts) {
|
|
||||||
if (artifact.type === 'workflow-command') {
|
|
||||||
const workflowTemplateType = config.md_workflow_template || `${templateType}-workflow`;
|
|
||||||
const { content: template, extension } = await this.loadTemplate(workflowTemplateType, '', config, 'default-workflow');
|
|
||||||
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++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write task/tool artifacts to target directory using templates
|
|
||||||
* @param {string} targetPath - Target directory path
|
|
||||||
* @param {Array} artifacts - Task/tool artifacts
|
|
||||||
* @param {string} templateType - Template type to use
|
|
||||||
* @param {Object} config - Installation configuration
|
|
||||||
* @returns {Promise<Object>} Counts of tasks and tools written
|
|
||||||
*/
|
|
||||||
async writeTaskToolArtifacts(targetPath, artifacts, templateType, config = {}) {
|
|
||||||
let taskCount = 0;
|
|
||||||
let toolCount = 0;
|
|
||||||
|
|
||||||
// Pre-load templates to avoid repeated file I/O in the loop
|
|
||||||
const taskTemplate = await this.loadTemplate(templateType, 'task', config, 'default-task');
|
|
||||||
const toolTemplate = await this.loadTemplate(templateType, 'tool', config, 'default-tool');
|
|
||||||
|
|
||||||
const { artifact_types } = config;
|
|
||||||
|
|
||||||
for (const artifact of artifacts) {
|
|
||||||
if (artifact.type !== 'task' && artifact.type !== 'tool') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip if the specific artifact type is not requested in config
|
|
||||||
if (artifact_types) {
|
|
||||||
if (artifact.type === 'task' && !artifact_types.includes('tasks')) continue;
|
|
||||||
if (artifact.type === 'tool' && !artifact_types.includes('tools')) continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use pre-loaded template based on artifact type
|
|
||||||
const { content: template, extension } = artifact.type === 'task' ? taskTemplate : toolTemplate;
|
|
||||||
|
|
||||||
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++;
|
|
||||||
} else {
|
|
||||||
toolCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { tasks: taskCount, tools: toolCount };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load template based on type and configuration
|
* Load template based on type and configuration
|
||||||
* @param {string} templateType - Template type (claude, windsurf, etc.)
|
* @param {string} templateType - Template type (claude, windsurf, etc.)
|
||||||
|
|
@ -711,13 +559,10 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
||||||
*/
|
*/
|
||||||
async printSummary(results, targetDir, options = {}) {
|
async printSummary(results, targetDir, options = {}) {
|
||||||
if (options.silent) return;
|
if (options.silent) return;
|
||||||
const parts = [];
|
const count = results.skillDirectories || results.skills || 0;
|
||||||
const totalDirs =
|
if (count > 0) {
|
||||||
results.skillDirectories || (results.workflows || 0) + (results.tasks || 0) + (results.tools || 0) + (results.skills || 0);
|
await prompts.log.success(`${this.name} configured: ${count} skills → ${targetDir}`);
|
||||||
const skillCount = totalDirs - (results.agents || 0);
|
}
|
||||||
if (skillCount > 0) parts.push(`${skillCount} skills`);
|
|
||||||
if (results.agents > 0) parts.push(`${results.agents} agents`);
|
|
||||||
await prompts.log.success(`${this.name} configured: ${parts.join(', ')} → ${targetDir}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -159,14 +159,9 @@ class IdeManager {
|
||||||
// Build detail string from handler-returned data
|
// Build detail string from handler-returned data
|
||||||
let detail = '';
|
let detail = '';
|
||||||
if (handlerResult && handlerResult.results) {
|
if (handlerResult && handlerResult.results) {
|
||||||
// Config-driven handlers return { success, results: { agents, workflows, tasks, tools } }
|
|
||||||
const r = handlerResult.results;
|
const r = handlerResult.results;
|
||||||
const parts = [];
|
const count = r.skillDirectories || r.skills || 0;
|
||||||
const totalDirs = r.skillDirectories || (r.workflows || 0) + (r.tasks || 0) + (r.tools || 0) + (r.skills || 0);
|
if (count > 0) detail = `${count} skills`;
|
||||||
const skillCount = totalDirs - (r.agents || 0);
|
|
||||||
if (skillCount > 0) parts.push(`${skillCount} skills`);
|
|
||||||
if (r.agents > 0) parts.push(`${r.agents} agents`);
|
|
||||||
detail = parts.join(', ');
|
|
||||||
}
|
}
|
||||||
// Propagate handler's success status (default true for backward compat)
|
// Propagate handler's success status (default true for backward compat)
|
||||||
const success = handlerResult?.success !== false;
|
const success = handlerResult?.success !== false;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates launcher command files for each agent
|
* Generates launcher command files for each agent
|
||||||
* Similar to WorkflowCommandGenerator but for agents
|
|
||||||
*/
|
*/
|
||||||
class AgentCommandGenerator {
|
class AgentCommandGenerator {
|
||||||
constructor(bmadFolderName = BMAD_FOLDER_NAME) {
|
constructor(bmadFolderName = BMAD_FOLDER_NAME) {
|
||||||
|
|
|
||||||
|
|
@ -1,368 +0,0 @@
|
||||||
const path = require('node:path');
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const csv = require('csv-parse/sync');
|
|
||||||
const { toColonName, toColonPath, toDashPath, BMAD_FOLDER_NAME } = require('./path-utils');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates command files for standalone tasks and tools
|
|
||||||
*/
|
|
||||||
class TaskToolCommandGenerator {
|
|
||||||
/**
|
|
||||||
* @param {string} bmadFolderName - Name of the BMAD folder for template rendering (default: '_bmad')
|
|
||||||
* Note: This parameter is accepted for API consistency with AgentCommandGenerator and
|
|
||||||
* WorkflowCommandGenerator, but is not used for path stripping. The manifest always stores
|
|
||||||
* filesystem paths with '_bmad/' prefix (the actual folder name), while bmadFolderName is
|
|
||||||
* used for template placeholder rendering ({{bmadFolderName}}).
|
|
||||||
*/
|
|
||||||
constructor(bmadFolderName = BMAD_FOLDER_NAME) {
|
|
||||||
this.bmadFolderName = bmadFolderName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Collect task and tool artifacts for IDE installation
|
|
||||||
* @param {string} bmadDir - BMAD installation directory
|
|
||||||
* @returns {Promise<Object>} Artifacts array with metadata
|
|
||||||
*/
|
|
||||||
async collectTaskToolArtifacts(bmadDir) {
|
|
||||||
const tasks = await this.loadTaskManifest(bmadDir);
|
|
||||||
const tools = await this.loadToolManifest(bmadDir);
|
|
||||||
|
|
||||||
// All tasks/tools in manifest are standalone (internal=true items are filtered during manifest generation)
|
|
||||||
const artifacts = [];
|
|
||||||
const bmadPrefix = `${BMAD_FOLDER_NAME}/`;
|
|
||||||
|
|
||||||
// Collect task artifacts
|
|
||||||
for (const task of tasks || []) {
|
|
||||||
let taskPath = (task.path || '').replaceAll('\\', '/');
|
|
||||||
// Convert absolute paths to relative paths
|
|
||||||
if (path.isAbsolute(taskPath)) {
|
|
||||||
taskPath = path.relative(bmadDir, taskPath).replaceAll('\\', '/');
|
|
||||||
}
|
|
||||||
// Remove _bmad/ prefix if present to get relative path within bmad folder
|
|
||||||
if (taskPath.startsWith(bmadPrefix)) {
|
|
||||||
taskPath = taskPath.slice(bmadPrefix.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
const taskExt = path.extname(taskPath) || '.md';
|
|
||||||
artifacts.push({
|
|
||||||
type: 'task',
|
|
||||||
name: task.name,
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect tool artifacts
|
|
||||||
for (const tool of tools || []) {
|
|
||||||
let toolPath = (tool.path || '').replaceAll('\\', '/');
|
|
||||||
// Convert absolute paths to relative paths
|
|
||||||
if (path.isAbsolute(toolPath)) {
|
|
||||||
toolPath = path.relative(bmadDir, toolPath).replaceAll('\\', '/');
|
|
||||||
}
|
|
||||||
// Remove _bmad/ prefix if present to get relative path within bmad folder
|
|
||||||
if (toolPath.startsWith(bmadPrefix)) {
|
|
||||||
toolPath = toolPath.slice(bmadPrefix.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
const toolExt = path.extname(toolPath) || '.md';
|
|
||||||
artifacts.push({
|
|
||||||
type: 'tool',
|
|
||||||
name: tool.name,
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
artifacts,
|
|
||||||
counts: {
|
|
||||||
tasks: (tasks || []).length,
|
|
||||||
tools: (tools || []).length,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate task and tool commands from manifest CSVs
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} bmadDir - BMAD installation directory
|
|
||||||
* @param {string} baseCommandsDir - Optional base commands directory (defaults to .claude/commands/bmad)
|
|
||||||
*/
|
|
||||||
async generateTaskToolCommands(projectDir, bmadDir, baseCommandsDir = null) {
|
|
||||||
const tasks = await this.loadTaskManifest(bmadDir);
|
|
||||||
const tools = await this.loadToolManifest(bmadDir);
|
|
||||||
|
|
||||||
// Base commands directory - use provided or default to Claude Code structure
|
|
||||||
const commandsDir = baseCommandsDir || path.join(projectDir, '.claude', 'commands', 'bmad');
|
|
||||||
|
|
||||||
let generatedCount = 0;
|
|
||||||
|
|
||||||
// Generate command files for tasks
|
|
||||||
for (const task of tasks || []) {
|
|
||||||
const moduleTasksDir = path.join(commandsDir, task.module, 'tasks');
|
|
||||||
await fs.ensureDir(moduleTasksDir);
|
|
||||||
|
|
||||||
const commandContent = this.generateCommandContent(task, 'task');
|
|
||||||
const commandPath = path.join(moduleTasksDir, `${task.name}.md`);
|
|
||||||
|
|
||||||
await fs.writeFile(commandPath, commandContent);
|
|
||||||
generatedCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate command files for tools
|
|
||||||
for (const tool of tools || []) {
|
|
||||||
const moduleToolsDir = path.join(commandsDir, tool.module, 'tools');
|
|
||||||
await fs.ensureDir(moduleToolsDir);
|
|
||||||
|
|
||||||
const commandContent = this.generateCommandContent(tool, 'tool');
|
|
||||||
const commandPath = path.join(moduleToolsDir, `${tool.name}.md`);
|
|
||||||
|
|
||||||
await fs.writeFile(commandPath, commandContent);
|
|
||||||
generatedCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
generated: generatedCount,
|
|
||||||
tasks: (tasks || []).length,
|
|
||||||
tools: (tools || []).length,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate command content for a task or tool
|
|
||||||
*/
|
|
||||||
generateCommandContent(item, type) {
|
|
||||||
const description = item.description || `Execute ${item.displayName || item.name}`;
|
|
||||||
|
|
||||||
// Convert path to use {project-root} placeholder
|
|
||||||
// Handle undefined/missing path by constructing from module and name
|
|
||||||
let itemPath = item.path;
|
|
||||||
if (!itemPath || typeof itemPath !== 'string') {
|
|
||||||
// Fallback: construct path from module and name if path is missing
|
|
||||||
const typePlural = type === 'task' ? 'tasks' : 'tools';
|
|
||||||
itemPath = `{project-root}/${this.bmadFolderName}/${item.module}/${typePlural}/${item.name}.md`;
|
|
||||||
} else {
|
|
||||||
// Normalize path separators to forward slashes
|
|
||||||
itemPath = itemPath.replaceAll('\\', '/');
|
|
||||||
|
|
||||||
// Extract relative path from absolute paths (Windows or Unix)
|
|
||||||
// Look for _bmad/ or bmad/ in the path and extract everything after it
|
|
||||||
// Match patterns like: /_bmad/core/tasks/... or /bmad/core/tasks/...
|
|
||||||
// Use [/\\] to handle both Unix forward slashes and Windows backslashes,
|
|
||||||
// and also paths without a leading separator (e.g., C:/_bmad/...)
|
|
||||||
const bmadMatch = itemPath.match(/[/\\]_bmad[/\\](.+)$/) || itemPath.match(/[/\\]bmad[/\\](.+)$/);
|
|
||||||
if (bmadMatch) {
|
|
||||||
// Found /_bmad/ or /bmad/ - use relative path after it
|
|
||||||
itemPath = `{project-root}/${this.bmadFolderName}/${bmadMatch[1]}`;
|
|
||||||
} else if (itemPath.startsWith(`${BMAD_FOLDER_NAME}/`)) {
|
|
||||||
// Relative path starting with _bmad/
|
|
||||||
itemPath = `{project-root}/${this.bmadFolderName}/${itemPath.slice(BMAD_FOLDER_NAME.length + 1)}`;
|
|
||||||
} else if (itemPath.startsWith('bmad/')) {
|
|
||||||
// Relative path starting with bmad/
|
|
||||||
itemPath = `{project-root}/${this.bmadFolderName}/${itemPath.slice(5)}`;
|
|
||||||
} else if (!itemPath.startsWith('{project-root}')) {
|
|
||||||
// For other relative paths, prefix with project root and bmad folder
|
|
||||||
itemPath = `{project-root}/${this.bmadFolderName}/${itemPath}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return `---
|
|
||||||
description: '${description.replaceAll("'", "''")}'
|
|
||||||
---
|
|
||||||
|
|
||||||
# ${item.displayName || item.name}
|
|
||||||
|
|
||||||
Read the entire ${type} file at: ${itemPath}
|
|
||||||
|
|
||||||
Follow all instructions in the ${type} file exactly as written.
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load task manifest CSV
|
|
||||||
*/
|
|
||||||
async loadTaskManifest(bmadDir) {
|
|
||||||
const manifestPath = path.join(bmadDir, '_config', 'task-manifest.csv');
|
|
||||||
|
|
||||||
if (!(await fs.pathExists(manifestPath))) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const csvContent = await fs.readFile(manifestPath, 'utf8');
|
|
||||||
return csv.parse(csvContent, {
|
|
||||||
columns: true,
|
|
||||||
skip_empty_lines: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load tool manifest CSV
|
|
||||||
*/
|
|
||||||
async loadToolManifest(bmadDir) {
|
|
||||||
const manifestPath = path.join(bmadDir, '_config', 'tool-manifest.csv');
|
|
||||||
|
|
||||||
if (!(await fs.pathExists(manifestPath))) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const csvContent = await fs.readFile(manifestPath, 'utf8');
|
|
||||||
return csv.parse(csvContent, {
|
|
||||||
columns: true,
|
|
||||||
skip_empty_lines: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate task and tool commands using underscore format (Windows-compatible)
|
|
||||||
* Creates flat files like: bmad_bmm_help.md
|
|
||||||
*
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} bmadDir - BMAD installation directory
|
|
||||||
* @param {string} baseCommandsDir - Base commands directory for the IDE
|
|
||||||
* @returns {Object} Generation results
|
|
||||||
*/
|
|
||||||
async generateColonTaskToolCommands(projectDir, bmadDir, baseCommandsDir) {
|
|
||||||
const tasks = await this.loadTaskManifest(bmadDir);
|
|
||||||
const tools = await this.loadToolManifest(bmadDir);
|
|
||||||
|
|
||||||
let generatedCount = 0;
|
|
||||||
|
|
||||||
// Generate command files for tasks
|
|
||||||
for (const task of tasks || []) {
|
|
||||||
const commandContent = this.generateCommandContent(task, 'task');
|
|
||||||
// Use underscore format: bmad_bmm_name.md
|
|
||||||
const flatName = toColonName(task.module, 'tasks', task.name);
|
|
||||||
const commandPath = path.join(baseCommandsDir, flatName);
|
|
||||||
await fs.ensureDir(path.dirname(commandPath));
|
|
||||||
await fs.writeFile(commandPath, commandContent);
|
|
||||||
generatedCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate command files for tools
|
|
||||||
for (const tool of tools || []) {
|
|
||||||
const commandContent = this.generateCommandContent(tool, 'tool');
|
|
||||||
// Use underscore format: bmad_bmm_name.md
|
|
||||||
const flatName = toColonName(tool.module, 'tools', tool.name);
|
|
||||||
const commandPath = path.join(baseCommandsDir, flatName);
|
|
||||||
await fs.ensureDir(path.dirname(commandPath));
|
|
||||||
await fs.writeFile(commandPath, commandContent);
|
|
||||||
generatedCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
generated: generatedCount,
|
|
||||||
tasks: (tasks || []).length,
|
|
||||||
tools: (tools || []).length,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate task and tool commands using underscore format (Windows-compatible)
|
|
||||||
* Creates flat files like: bmad_bmm_help.md
|
|
||||||
*
|
|
||||||
* @param {string} projectDir - Project directory
|
|
||||||
* @param {string} bmadDir - BMAD installation directory
|
|
||||||
* @param {string} baseCommandsDir - Base commands directory for the IDE
|
|
||||||
* @returns {Object} Generation results
|
|
||||||
*/
|
|
||||||
async generateDashTaskToolCommands(projectDir, bmadDir, baseCommandsDir) {
|
|
||||||
const tasks = await this.loadTaskManifest(bmadDir);
|
|
||||||
const tools = await this.loadToolManifest(bmadDir);
|
|
||||||
|
|
||||||
let generatedCount = 0;
|
|
||||||
|
|
||||||
// Generate command files for tasks
|
|
||||||
for (const task of tasks || []) {
|
|
||||||
const commandContent = this.generateCommandContent(task, 'task');
|
|
||||||
// Use dash format: bmad-bmm-name.md
|
|
||||||
const flatName = toDashPath(`${task.module}/tasks/${task.name}.md`);
|
|
||||||
const commandPath = path.join(baseCommandsDir, flatName);
|
|
||||||
await fs.ensureDir(path.dirname(commandPath));
|
|
||||||
await fs.writeFile(commandPath, commandContent);
|
|
||||||
generatedCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate command files for tools
|
|
||||||
for (const tool of tools || []) {
|
|
||||||
const commandContent = this.generateCommandContent(tool, 'tool');
|
|
||||||
// Use dash format: bmad-bmm-name.md
|
|
||||||
const flatName = toDashPath(`${tool.module}/tools/${tool.name}.md`);
|
|
||||||
const commandPath = path.join(baseCommandsDir, flatName);
|
|
||||||
await fs.ensureDir(path.dirname(commandPath));
|
|
||||||
await fs.writeFile(commandPath, commandContent);
|
|
||||||
generatedCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
generated: generatedCount,
|
|
||||||
tasks: (tasks || []).length,
|
|
||||||
tools: (tools || []).length,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write task/tool artifacts using underscore format (Windows-compatible)
|
|
||||||
* Creates flat files like: bmad_bmm_help.md
|
|
||||||
*
|
|
||||||
* @param {string} baseCommandsDir - Base commands directory for the IDE
|
|
||||||
* @param {Array} artifacts - Task/tool artifacts with relativePath
|
|
||||||
* @returns {number} Count of commands written
|
|
||||||
*/
|
|
||||||
async writeColonArtifacts(baseCommandsDir, artifacts) {
|
|
||||||
let writtenCount = 0;
|
|
||||||
|
|
||||||
for (const artifact of artifacts) {
|
|
||||||
if (artifact.type === 'task' || artifact.type === 'tool') {
|
|
||||||
const commandContent = this.generateCommandContent(artifact, artifact.type);
|
|
||||||
// Use underscore format: bmad_module_name.md
|
|
||||||
const flatName = toColonPath(artifact.relativePath);
|
|
||||||
const commandPath = path.join(baseCommandsDir, flatName);
|
|
||||||
await fs.ensureDir(path.dirname(commandPath));
|
|
||||||
await fs.writeFile(commandPath, commandContent);
|
|
||||||
writtenCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return writtenCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write task/tool artifacts using dash format (NEW STANDARD)
|
|
||||||
* Creates flat files like: bmad-bmm-help.md
|
|
||||||
*
|
|
||||||
* Note: Tasks/tools do NOT have bmad-agent- prefix - only agents do.
|
|
||||||
*
|
|
||||||
* @param {string} baseCommandsDir - Base commands directory for the IDE
|
|
||||||
* @param {Array} artifacts - Task/tool artifacts with relativePath
|
|
||||||
* @returns {number} Count of commands written
|
|
||||||
*/
|
|
||||||
async writeDashArtifacts(baseCommandsDir, artifacts) {
|
|
||||||
let writtenCount = 0;
|
|
||||||
|
|
||||||
for (const artifact of artifacts) {
|
|
||||||
if (artifact.type === 'task' || artifact.type === 'tool') {
|
|
||||||
const commandContent = this.generateCommandContent(artifact, artifact.type);
|
|
||||||
// Use dash format: bmad-module-name.md
|
|
||||||
const flatName = toDashPath(artifact.relativePath);
|
|
||||||
const commandPath = path.join(baseCommandsDir, flatName);
|
|
||||||
await fs.ensureDir(path.dirname(commandPath));
|
|
||||||
await fs.writeFile(commandPath, commandContent);
|
|
||||||
writtenCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return writtenCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { TaskToolCommandGenerator };
|
|
||||||
|
|
@ -1,179 +0,0 @@
|
||||||
const path = require('node:path');
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const csv = require('csv-parse/sync');
|
|
||||||
const { BMAD_FOLDER_NAME } = require('./path-utils');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates command files for each workflow in the manifest
|
|
||||||
*/
|
|
||||||
class WorkflowCommandGenerator {
|
|
||||||
constructor(bmadFolderName = BMAD_FOLDER_NAME) {
|
|
||||||
this.bmadFolderName = bmadFolderName;
|
|
||||||
}
|
|
||||||
|
|
||||||
async collectWorkflowArtifacts(bmadDir) {
|
|
||||||
const workflows = await this.loadWorkflowManifest(bmadDir);
|
|
||||||
|
|
||||||
if (!workflows) {
|
|
||||||
return { artifacts: [], counts: { commands: 0, launchers: 0 } };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ALL workflows now generate commands - no standalone filtering
|
|
||||||
const allWorkflows = workflows;
|
|
||||||
|
|
||||||
const artifacts = [];
|
|
||||||
|
|
||||||
for (const workflow of allWorkflows) {
|
|
||||||
// Calculate the relative workflow path (e.g., bmm/workflows/4-implementation/sprint-planning/workflow.md)
|
|
||||||
let workflowRelPath = workflow.path || '';
|
|
||||||
// Normalize path separators for cross-platform compatibility
|
|
||||||
workflowRelPath = workflowRelPath.replaceAll('\\', '/');
|
|
||||||
// Remove _bmad/ prefix if present to get relative path from project root
|
|
||||||
// Handle both absolute paths (/path/to/_bmad/...) and relative paths (_bmad/...)
|
|
||||||
if (workflowRelPath.includes('_bmad/')) {
|
|
||||||
const parts = workflowRelPath.split(/_bmad\//);
|
|
||||||
if (parts.length > 1) {
|
|
||||||
workflowRelPath = parts.slice(1).join('/');
|
|
||||||
}
|
|
||||||
} else if (workflowRelPath.includes('/src/')) {
|
|
||||||
// Normalize source paths (e.g. .../src/bmm/...) to relative module path (e.g. bmm/...)
|
|
||||||
const match = workflowRelPath.match(/\/src\/([^/]+)\/(.+)/);
|
|
||||||
if (match) {
|
|
||||||
workflowRelPath = `${match[1]}/${match[2]}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
artifacts.push({
|
|
||||||
type: 'workflow-command',
|
|
||||||
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
|
|
||||||
sourcePath: workflow.path,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupedWorkflows = this.groupWorkflowsByModule(allWorkflows);
|
|
||||||
for (const [module, launcherContent] of Object.entries(this.buildModuleWorkflowLaunchers(groupedWorkflows))) {
|
|
||||||
artifacts.push({
|
|
||||||
type: 'workflow-launcher',
|
|
||||||
module,
|
|
||||||
relativePath: path.join(module, 'workflows', 'README.md'),
|
|
||||||
content: launcherContent,
|
|
||||||
sourcePath: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
artifacts,
|
|
||||||
counts: {
|
|
||||||
commands: allWorkflows.length,
|
|
||||||
launchers: Object.keys(groupedWorkflows).length,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create workflow launcher files for each module
|
|
||||||
*/
|
|
||||||
async createModuleWorkflowLaunchers(baseCommandsDir, workflowsByModule) {
|
|
||||||
for (const [module, moduleWorkflows] of Object.entries(workflowsByModule)) {
|
|
||||||
const content = this.buildLauncherContent(module, moduleWorkflows);
|
|
||||||
const moduleWorkflowsDir = path.join(baseCommandsDir, module, 'workflows');
|
|
||||||
await fs.ensureDir(moduleWorkflowsDir);
|
|
||||||
const launcherPath = path.join(moduleWorkflowsDir, 'README.md');
|
|
||||||
await fs.writeFile(launcherPath, content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
groupWorkflowsByModule(workflows) {
|
|
||||||
const workflowsByModule = {};
|
|
||||||
|
|
||||||
for (const workflow of workflows) {
|
|
||||||
if (!workflowsByModule[workflow.module]) {
|
|
||||||
workflowsByModule[workflow.module] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
workflowsByModule[workflow.module].push({
|
|
||||||
...workflow,
|
|
||||||
displayPath: this.transformWorkflowPath(workflow.path),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return workflowsByModule;
|
|
||||||
}
|
|
||||||
|
|
||||||
buildModuleWorkflowLaunchers(groupedWorkflows) {
|
|
||||||
const launchers = {};
|
|
||||||
|
|
||||||
for (const [module, moduleWorkflows] of Object.entries(groupedWorkflows)) {
|
|
||||||
launchers[module] = this.buildLauncherContent(module, moduleWorkflows);
|
|
||||||
}
|
|
||||||
|
|
||||||
return launchers;
|
|
||||||
}
|
|
||||||
|
|
||||||
buildLauncherContent(module, moduleWorkflows) {
|
|
||||||
let content = `# ${module.toUpperCase()} Workflows
|
|
||||||
|
|
||||||
## Available Workflows in ${module}
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
for (const workflow of moduleWorkflows) {
|
|
||||||
content += `**${workflow.name}**\n`;
|
|
||||||
content += `- Path: \`${workflow.displayPath}\`\n`;
|
|
||||||
content += `- ${workflow.description}\n\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
content += `
|
|
||||||
## Execution
|
|
||||||
|
|
||||||
When running any workflow:
|
|
||||||
1. LOAD the workflow.md file at the path shown above
|
|
||||||
2. READ its entire contents and follow its directions exactly
|
|
||||||
3. Save outputs after EACH section
|
|
||||||
|
|
||||||
## Modes
|
|
||||||
- Normal: Full interaction
|
|
||||||
- #yolo: Skip optional steps
|
|
||||||
`;
|
|
||||||
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
transformWorkflowPath(workflowPath) {
|
|
||||||
let transformed = workflowPath;
|
|
||||||
|
|
||||||
if (workflowPath.includes('/src/bmm-skills/')) {
|
|
||||||
const match = workflowPath.match(/\/src\/bmm-skills\/(.+)/);
|
|
||||||
if (match) {
|
|
||||||
transformed = `{project-root}/${this.bmadFolderName}/bmm/${match[1]}`;
|
|
||||||
}
|
|
||||||
} else if (workflowPath.includes('/src/core-skills/')) {
|
|
||||||
const match = workflowPath.match(/\/src\/core-skills\/(.+)/);
|
|
||||||
if (match) {
|
|
||||||
transformed = `{project-root}/${this.bmadFolderName}/core/${match[1]}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return transformed;
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadWorkflowManifest(bmadDir) {
|
|
||||||
const manifestPath = path.join(bmadDir, '_config', 'workflow-manifest.csv');
|
|
||||||
|
|
||||||
if (!(await fs.pathExists(manifestPath))) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const csvContent = await fs.readFile(manifestPath, 'utf8');
|
|
||||||
return csv.parse(csvContent, {
|
|
||||||
columns: true,
|
|
||||||
skip_empty_lines: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { WorkflowCommandGenerator };
|
|
||||||
Loading…
Reference in New Issue