feat(installer): expand to 42 platforms with shared target_dir coordination (#2313)
* refactor(installer): replace legacy_targets auto-cleanup with upgrade warnings Removes the legacy_targets YAML field and its install-time auto-migration of pre-v6.1.0 directories (.claude/commands, .opencode/agents, etc.). On install, surface a warning instead: read manifest version and scan 24 known legacy paths, then print rm -rf commands the user can run themselves. Also deletes orphan tools/platform-codes.yaml (never loaded by any code) and fixes a stale URL in the cs translation. * feat(installer): consolidate to .agents/skills and add global_target_dir for all platforms Updates platform-codes.yaml against verified primary docs for all 24 supported platforms. 14 platforms (auggie, codex, crush, cursor, gemini, github-copilot, kilo, kimi-code, opencode, pi, roo, rovo-dev, windsurf) move their project target_dir to the cross-tool .agents/skills/ standard. Junie moves from the broken .agents/skills/ to its own .junie/skills/ per JetBrains docs. Adds global_target_dir to every platform: 11 share ~/.agents/skills/, Crush uses XDG ~/.config/agents/skills/, Codex global stays ~/.codex/skills/, the rest are tool-specific. Ona and Trae omit global (no documented home path). Note: installer logic does not yet dedupe writes for platforms sharing a target_dir — users installing multiple .agents/skills/ tools together will overwrite the same files (harmless on install, but uninstalling one clears the dir for the others). Coordination logic is the next step. * feat(installer): add 18 new platforms, dedup shared target_dir, ownership-aware cleanup Adds 18 platforms from the verified Vercel list (adal, amp, bob, command-code, cortex, droid, firebender, goose, kode, mistral-vibe, mux, neovate, openclaw, openhands, pochi, replit, warp, zencoder). Marks codex and github-copilot as preferred alongside claude-code and cursor. Coordination for platforms sharing a target_dir: - IdeManager.setupBatch dedups skill writes when multiple selected platforms point at the same target_dir (e.g. .agents/skills/). The first platform writes, peers skip the redundant wipe-and-rewrite. Result reports the same count and target dir for every member so the install summary is consistent. - IdeManager.cleanupByList accepts remainingIdes; when removing one platform from a shared dir while another co-installed platform still owns it, the target_dir wipe is skipped. Platform-specific hooks (copilot markers, kilo modes, rovodev prompts) still run. - _setupIdes uses setupBatch; _removeDeselectedIdes passes remainingIdes so partial reconfigure preserves shared skills. Skill ownership now uses skill-manifest.csv canonicalIds, not the bmad- prefix. This unblocks custom modules that ship skills with non-bmad names (e.g. fred-cool-skill). Affected sites: - _config-driven.detect: reads canonicalIds from the project's bmadDir - _config-driven.findAncestorConflict: reads canonicalIds from the ancestor's own bmadDir, falling back to the prefix only when no manifest exists - legacy-warnings.findStaleLegacyDirs: same canonicalId-based detection Migration warnings: LEGACY_SKILL_PATHS adds 12 skill dirs that moved to the .agents/skills/ standard (cursor, gemini, github-copilot, kimi, opencode, pi, roo, rovodev, windsurf, plus their globals). Users with stale skills in those locations get a one-line warning with the rm command per dir. New shared helper tools/installer/ide/shared/installed-skills.js exposes getInstalledCanonicalIds(bmadDir) and isBmadOwnedEntry(entry, canonicalIds). Tests: 9 new assertions across two suites covering dedup, partial uninstall preservation, and custom-module skill detection. All 286 tests pass. * fix(installer): setupBatch must not claim a shared target_dir on failure If the first platform's setup throws or returns success: false, the dedup map previously still recorded the claim with skillCount: 0, causing every peer sharing the target_dir to skip its install — leaving the dir empty/broken behind a cascade of misleading "shares with X" rows. Now the claim is only recorded when the install succeeded and wrote skills. On failure, the next peer becomes the new first writer and recovers. Adds Suite 40b regression test that monkey-patches cursor.setup to throw and verifies gemini still populates the shared dir. * fix(installer): address PR #2313 review findings Three issues raised by augmentcode and coderabbit bot reviewers: 1. _removeDeselectedIdes silently swallowed cleanup failures after the refactor to cleanupByList. The old per-IDE try/catch logged a warning; the new path discarded the result array. Now logs a warning per failed ide so failures stay visible. 2. The legacy-dir cleanup hint printed `rm -rf "<path>"/bmad*` which both matched bmad-os-* utility skills the user should keep AND missed the custom-module skills (e.g. fred-cool-skill) that the new canonical-id detection now finds. Findings now carry the exact entry names from the scan, and the warning prints one precise rm line per entry. 3. warnPreNativeSkillsLegacy did unguarded fs reads at install start. A permission/IO error would have aborted the whole install. Wrapped the call site in try/catch so legacy-scan failures only emit a warning.
This commit is contained in:
parent
1197122001
commit
01cc32540b
|
|
@ -60,7 +60,7 @@ Dostupná ID nástrojů pro příznak `--tools`:
|
||||||
|
|
||||||
**Preferované:** `claude-code`, `cursor`
|
**Preferované:** `claude-code`, `cursor`
|
||||||
|
|
||||||
Spusťte `npx bmad-method install` interaktivně jednou pro zobrazení aktuálního seznamu podporovaných nástrojů, nebo zkontrolujte [konfiguraci kódů platforem](https://github.com/bmad-code-org/BMAD-METHOD/blob/main/tools/cli/installers/lib/ide/platform-codes.yaml).
|
Spusťte `npx bmad-method install` interaktivně jednou pro zobrazení aktuálního seznamu podporovaných nástrojů, nebo zkontrolujte [konfiguraci kódů platforem](https://github.com/bmad-code-org/BMAD-METHOD/blob/main/tools/installer/ide/platform-codes.yaml).
|
||||||
|
|
||||||
## Režimy instalace
|
## Režimy instalace
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -139,19 +139,10 @@ async function runTests() {
|
||||||
const platformCodes = await loadPlatformCodes();
|
const platformCodes = await loadPlatformCodes();
|
||||||
const windsurfInstaller = platformCodes.platforms.windsurf?.installer;
|
const windsurfInstaller = platformCodes.platforms.windsurf?.installer;
|
||||||
|
|
||||||
assert(windsurfInstaller?.target_dir === '.windsurf/skills', 'Windsurf target_dir uses native skills path');
|
assert(windsurfInstaller?.target_dir === '.agents/skills', 'Windsurf target_dir uses native skills path');
|
||||||
|
|
||||||
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 tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-windsurf-test-'));
|
||||||
const installedBmadDir = await createTestBmadFixture();
|
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();
|
const ideManager = new IdeManager();
|
||||||
await ideManager.ensureInitialized();
|
await ideManager.ensureInitialized();
|
||||||
|
|
@ -162,11 +153,9 @@ async function runTests() {
|
||||||
|
|
||||||
assert(result.success === true, 'Windsurf setup succeeds against temp project');
|
assert(result.success === true, 'Windsurf setup succeeds against temp project');
|
||||||
|
|
||||||
const skillFile = path.join(tempProjectDir, '.windsurf', 'skills', 'bmad-master', 'SKILL.md');
|
const skillFile = path.join(tempProjectDir, '.agents', 'skills', 'bmad-master', 'SKILL.md');
|
||||||
assert(await fs.pathExists(skillFile), 'Windsurf install writes SKILL.md directory output');
|
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(tempProjectDir);
|
||||||
await fs.remove(path.dirname(installedBmadDir));
|
await fs.remove(path.dirname(installedBmadDir));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -187,17 +176,8 @@ async function runTests() {
|
||||||
|
|
||||||
assert(kiroInstaller?.target_dir === '.kiro/skills', 'Kiro target_dir uses native skills path');
|
assert(kiroInstaller?.target_dir === '.kiro/skills', 'Kiro target_dir uses native skills path');
|
||||||
|
|
||||||
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 tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-kiro-test-'));
|
||||||
const installedBmadDir = await createTestBmadFixture();
|
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();
|
const ideManager = new IdeManager();
|
||||||
await ideManager.ensureInitialized();
|
await ideManager.ensureInitialized();
|
||||||
|
|
@ -211,8 +191,6 @@ async function runTests() {
|
||||||
const skillFile = path.join(tempProjectDir, '.kiro', 'skills', 'bmad-master', 'SKILL.md');
|
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(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(tempProjectDir);
|
||||||
await fs.remove(path.dirname(installedBmadDir));
|
await fs.remove(path.dirname(installedBmadDir));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -233,17 +211,8 @@ async function runTests() {
|
||||||
|
|
||||||
assert(antigravityInstaller?.target_dir === '.agent/skills', 'Antigravity target_dir uses native skills path');
|
assert(antigravityInstaller?.target_dir === '.agent/skills', 'Antigravity target_dir uses native skills path');
|
||||||
|
|
||||||
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 tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-antigravity-test-'));
|
||||||
const installedBmadDir = await createTestBmadFixture();
|
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();
|
const ideManager = new IdeManager();
|
||||||
await ideManager.ensureInitialized();
|
await ideManager.ensureInitialized();
|
||||||
|
|
@ -257,8 +226,6 @@ async function runTests() {
|
||||||
const skillFile = path.join(tempProjectDir, '.agent', 'skills', 'bmad-master', 'SKILL.md');
|
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(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(tempProjectDir);
|
||||||
await fs.remove(path.dirname(installedBmadDir));
|
await fs.remove(path.dirname(installedBmadDir));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -277,12 +244,7 @@ async function runTests() {
|
||||||
const platformCodes = await loadPlatformCodes();
|
const platformCodes = await loadPlatformCodes();
|
||||||
const auggieInstaller = platformCodes.platforms.auggie?.installer;
|
const auggieInstaller = platformCodes.platforms.auggie?.installer;
|
||||||
|
|
||||||
assert(auggieInstaller?.target_dir === '.augment/skills', 'Auggie target_dir uses native skills path');
|
assert(auggieInstaller?.target_dir === '.agents/skills', 'Auggie target_dir uses native skills path');
|
||||||
|
|
||||||
assert(
|
|
||||||
Array.isArray(auggieInstaller?.legacy_targets) && auggieInstaller.legacy_targets.includes('.augment/commands'),
|
|
||||||
'Auggie installer cleans legacy command output',
|
|
||||||
);
|
|
||||||
|
|
||||||
assert(
|
assert(
|
||||||
auggieInstaller?.ancestor_conflict_check !== true,
|
auggieInstaller?.ancestor_conflict_check !== true,
|
||||||
|
|
@ -291,10 +253,6 @@ async function runTests() {
|
||||||
|
|
||||||
const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-auggie-test-'));
|
const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-auggie-test-'));
|
||||||
const installedBmadDir = await createTestBmadFixture();
|
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();
|
const ideManager = new IdeManager();
|
||||||
await ideManager.ensureInitialized();
|
await ideManager.ensureInitialized();
|
||||||
|
|
@ -305,11 +263,9 @@ async function runTests() {
|
||||||
|
|
||||||
assert(result.success === true, 'Auggie setup succeeds against temp project');
|
assert(result.success === true, 'Auggie setup succeeds against temp project');
|
||||||
|
|
||||||
const skillFile = path.join(tempProjectDir, '.augment', 'skills', 'bmad-master', 'SKILL.md');
|
const skillFile = path.join(tempProjectDir, '.agents', 'skills', 'bmad-master', 'SKILL.md');
|
||||||
assert(await fs.pathExists(skillFile), 'Auggie install writes SKILL.md directory output');
|
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(tempProjectDir);
|
||||||
await fs.remove(path.dirname(installedBmadDir));
|
await fs.remove(path.dirname(installedBmadDir));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -328,30 +284,10 @@ async function runTests() {
|
||||||
const platformCodes = await loadPlatformCodes();
|
const platformCodes = await loadPlatformCodes();
|
||||||
const opencodeInstaller = platformCodes.platforms.opencode?.installer;
|
const opencodeInstaller = platformCodes.platforms.opencode?.installer;
|
||||||
|
|
||||||
assert(opencodeInstaller?.target_dir === '.opencode/skills', 'OpenCode target_dir uses native skills path');
|
assert(opencodeInstaller?.target_dir === '.agents/skills', 'OpenCode target_dir uses native skills path');
|
||||||
|
|
||||||
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 tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-opencode-test-'));
|
||||||
const installedBmadDir = await createTestBmadFixture();
|
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();
|
const ideManager = new IdeManager();
|
||||||
await ideManager.ensureInitialized();
|
await ideManager.ensureInitialized();
|
||||||
|
|
@ -362,16 +298,9 @@ async function runTests() {
|
||||||
|
|
||||||
assert(result.success === true, 'OpenCode setup succeeds against temp project');
|
assert(result.success === true, 'OpenCode setup succeeds against temp project');
|
||||||
|
|
||||||
const skillFile = path.join(tempProjectDir, '.opencode', 'skills', 'bmad-master', 'SKILL.md');
|
const skillFile = path.join(tempProjectDir, '.agents', 'skills', 'bmad-master', 'SKILL.md');
|
||||||
assert(await fs.pathExists(skillFile), 'OpenCode install writes SKILL.md directory output');
|
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(tempProjectDir);
|
||||||
await fs.remove(path.dirname(installedBmadDir));
|
await fs.remove(path.dirname(installedBmadDir));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -392,16 +321,8 @@ async function runTests() {
|
||||||
|
|
||||||
assert(claudeInstaller?.target_dir === '.claude/skills', 'Claude Code target_dir uses native skills path');
|
assert(claudeInstaller?.target_dir === '.claude/skills', 'Claude Code target_dir uses native skills path');
|
||||||
|
|
||||||
assert(
|
|
||||||
Array.isArray(claudeInstaller?.legacy_targets) && claudeInstaller.legacy_targets.includes('.claude/commands'),
|
|
||||||
'Claude Code installer cleans legacy command output',
|
|
||||||
);
|
|
||||||
|
|
||||||
const tempProjectDir9 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-claude-code-test-'));
|
const tempProjectDir9 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-claude-code-test-'));
|
||||||
const installedBmadDir9 = await createTestBmadFixture();
|
const installedBmadDir9 = await createTestBmadFixture();
|
||||||
const legacyDir9 = path.join(tempProjectDir9, '.claude', 'commands');
|
|
||||||
await fs.ensureDir(legacyDir9);
|
|
||||||
await fs.writeFile(path.join(legacyDir9, 'bmad-legacy.md'), 'legacy\n');
|
|
||||||
|
|
||||||
const ideManager9 = new IdeManager();
|
const ideManager9 = new IdeManager();
|
||||||
await ideManager9.ensureInitialized();
|
await ideManager9.ensureInitialized();
|
||||||
|
|
@ -420,8 +341,6 @@ async function runTests() {
|
||||||
const nameMatch9 = skillContent9.match(/^name:\s*(.+)$/m);
|
const nameMatch9 = skillContent9.match(/^name:\s*(.+)$/m);
|
||||||
assert(nameMatch9 && nameMatch9[1].trim() === 'bmad-master', 'Claude Code skill name frontmatter matches directory name exactly');
|
assert(nameMatch9 && nameMatch9[1].trim() === 'bmad-master', 'Claude Code skill name frontmatter matches directory name exactly');
|
||||||
|
|
||||||
assert(!(await fs.pathExists(legacyDir9)), 'Claude Code setup removes legacy commands dir');
|
|
||||||
|
|
||||||
await fs.remove(tempProjectDir9);
|
await fs.remove(tempProjectDir9);
|
||||||
await fs.remove(path.dirname(installedBmadDir9));
|
await fs.remove(path.dirname(installedBmadDir9));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -444,16 +363,8 @@ async function runTests() {
|
||||||
|
|
||||||
assert(codexInstaller?.target_dir === '.agents/skills', 'Codex target_dir uses native skills path');
|
assert(codexInstaller?.target_dir === '.agents/skills', 'Codex target_dir uses native skills path');
|
||||||
|
|
||||||
assert(
|
|
||||||
Array.isArray(codexInstaller?.legacy_targets) && codexInstaller.legacy_targets.includes('.codex/prompts'),
|
|
||||||
'Codex installer cleans legacy prompt output',
|
|
||||||
);
|
|
||||||
|
|
||||||
const tempProjectDir11 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-codex-test-'));
|
const tempProjectDir11 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-codex-test-'));
|
||||||
const installedBmadDir11 = await createTestBmadFixture();
|
const installedBmadDir11 = await createTestBmadFixture();
|
||||||
const legacyDir11 = path.join(tempProjectDir11, '.codex', 'prompts');
|
|
||||||
await fs.ensureDir(legacyDir11);
|
|
||||||
await fs.writeFile(path.join(legacyDir11, 'bmad-legacy.md'), 'legacy\n');
|
|
||||||
|
|
||||||
const ideManager11 = new IdeManager();
|
const ideManager11 = new IdeManager();
|
||||||
await ideManager11.ensureInitialized();
|
await ideManager11.ensureInitialized();
|
||||||
|
|
@ -472,8 +383,6 @@ async function runTests() {
|
||||||
const nameMatch11 = skillContent11.match(/^name:\s*(.+)$/m);
|
const nameMatch11 = skillContent11.match(/^name:\s*(.+)$/m);
|
||||||
assert(nameMatch11 && nameMatch11[1].trim() === 'bmad-master', 'Codex skill name frontmatter matches directory name exactly');
|
assert(nameMatch11 && nameMatch11[1].trim() === 'bmad-master', 'Codex skill name frontmatter matches directory name exactly');
|
||||||
|
|
||||||
assert(!(await fs.pathExists(legacyDir11)), 'Codex setup removes legacy prompts dir');
|
|
||||||
|
|
||||||
await fs.remove(tempProjectDir11);
|
await fs.remove(tempProjectDir11);
|
||||||
await fs.remove(path.dirname(installedBmadDir11));
|
await fs.remove(path.dirname(installedBmadDir11));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -494,20 +403,12 @@ async function runTests() {
|
||||||
const platformCodes13 = await loadPlatformCodes();
|
const platformCodes13 = await loadPlatformCodes();
|
||||||
const cursorInstaller = platformCodes13.platforms.cursor?.installer;
|
const cursorInstaller = platformCodes13.platforms.cursor?.installer;
|
||||||
|
|
||||||
assert(cursorInstaller?.target_dir === '.cursor/skills', 'Cursor target_dir uses native skills path');
|
assert(cursorInstaller?.target_dir === '.agents/skills', 'Cursor target_dir uses native skills path');
|
||||||
|
|
||||||
assert(
|
|
||||||
Array.isArray(cursorInstaller?.legacy_targets) && cursorInstaller.legacy_targets.includes('.cursor/commands'),
|
|
||||||
'Cursor installer cleans legacy command output',
|
|
||||||
);
|
|
||||||
|
|
||||||
assert(!cursorInstaller?.ancestor_conflict_check, 'Cursor installer does not enable ancestor conflict checks');
|
assert(!cursorInstaller?.ancestor_conflict_check, 'Cursor installer does not enable ancestor conflict checks');
|
||||||
|
|
||||||
const tempProjectDir13c = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-cursor-test-'));
|
const tempProjectDir13c = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-cursor-test-'));
|
||||||
const installedBmadDir13c = await createTestBmadFixture();
|
const installedBmadDir13c = await createTestBmadFixture();
|
||||||
const legacyDir13c = path.join(tempProjectDir13c, '.cursor', 'commands');
|
|
||||||
await fs.ensureDir(legacyDir13c);
|
|
||||||
await fs.writeFile(path.join(legacyDir13c, 'bmad-legacy.md'), 'legacy\n');
|
|
||||||
|
|
||||||
const ideManager13c = new IdeManager();
|
const ideManager13c = new IdeManager();
|
||||||
await ideManager13c.ensureInitialized();
|
await ideManager13c.ensureInitialized();
|
||||||
|
|
@ -518,7 +419,7 @@ async function runTests() {
|
||||||
|
|
||||||
assert(result13c.success === true, 'Cursor setup succeeds against temp project');
|
assert(result13c.success === true, 'Cursor setup succeeds against temp project');
|
||||||
|
|
||||||
const skillFile13c = path.join(tempProjectDir13c, '.cursor', 'skills', 'bmad-master', 'SKILL.md');
|
const skillFile13c = path.join(tempProjectDir13c, '.agents', 'skills', 'bmad-master', 'SKILL.md');
|
||||||
assert(await fs.pathExists(skillFile13c), 'Cursor install writes SKILL.md directory output');
|
assert(await fs.pathExists(skillFile13c), 'Cursor install writes SKILL.md directory output');
|
||||||
|
|
||||||
// Verify name frontmatter matches directory name
|
// Verify name frontmatter matches directory name
|
||||||
|
|
@ -526,8 +427,6 @@ async function runTests() {
|
||||||
const nameMatch13c = skillContent13c.match(/^name:\s*(.+)$/m);
|
const nameMatch13c = skillContent13c.match(/^name:\s*(.+)$/m);
|
||||||
assert(nameMatch13c && nameMatch13c[1].trim() === 'bmad-master', 'Cursor skill name frontmatter matches directory name exactly');
|
assert(nameMatch13c && nameMatch13c[1].trim() === 'bmad-master', 'Cursor skill name frontmatter matches directory name exactly');
|
||||||
|
|
||||||
assert(!(await fs.pathExists(legacyDir13c)), 'Cursor setup removes legacy commands dir');
|
|
||||||
|
|
||||||
await fs.remove(tempProjectDir13c);
|
await fs.remove(tempProjectDir13c);
|
||||||
await fs.remove(path.dirname(installedBmadDir13c));
|
await fs.remove(path.dirname(installedBmadDir13c));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -546,19 +445,10 @@ async function runTests() {
|
||||||
const platformCodes13 = await loadPlatformCodes();
|
const platformCodes13 = await loadPlatformCodes();
|
||||||
const rooInstaller = platformCodes13.platforms.roo?.installer;
|
const rooInstaller = platformCodes13.platforms.roo?.installer;
|
||||||
|
|
||||||
assert(rooInstaller?.target_dir === '.roo/skills', 'Roo target_dir uses native skills path');
|
assert(rooInstaller?.target_dir === '.agents/skills', 'Roo target_dir uses native skills path');
|
||||||
|
|
||||||
assert(
|
|
||||||
Array.isArray(rooInstaller?.legacy_targets) && rooInstaller.legacy_targets.includes('.roo/commands'),
|
|
||||||
'Roo installer cleans legacy command output',
|
|
||||||
);
|
|
||||||
|
|
||||||
const tempProjectDir13 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-roo-test-'));
|
const tempProjectDir13 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-roo-test-'));
|
||||||
const installedBmadDir13 = await createTestBmadFixture();
|
const installedBmadDir13 = await createTestBmadFixture();
|
||||||
const legacyDir13 = path.join(tempProjectDir13, '.roo', 'commands', 'bmad-legacy-dir');
|
|
||||||
await fs.ensureDir(legacyDir13);
|
|
||||||
await fs.writeFile(path.join(tempProjectDir13, '.roo', 'commands', 'bmad-legacy.md'), 'legacy\n');
|
|
||||||
await fs.writeFile(path.join(legacyDir13, 'SKILL.md'), 'legacy\n');
|
|
||||||
|
|
||||||
const ideManager13 = new IdeManager();
|
const ideManager13 = new IdeManager();
|
||||||
await ideManager13.ensureInitialized();
|
await ideManager13.ensureInitialized();
|
||||||
|
|
@ -569,7 +459,7 @@ async function runTests() {
|
||||||
|
|
||||||
assert(result13.success === true, 'Roo setup succeeds against temp project');
|
assert(result13.success === true, 'Roo setup succeeds against temp project');
|
||||||
|
|
||||||
const skillFile13 = path.join(tempProjectDir13, '.roo', 'skills', 'bmad-master', 'SKILL.md');
|
const skillFile13 = path.join(tempProjectDir13, '.agents', 'skills', 'bmad-master', 'SKILL.md');
|
||||||
assert(await fs.pathExists(skillFile13), 'Roo install writes SKILL.md directory output');
|
assert(await fs.pathExists(skillFile13), 'Roo install writes SKILL.md directory output');
|
||||||
|
|
||||||
// Verify name frontmatter matches directory name (Roo constraint: lowercase alphanumeric + hyphens)
|
// Verify name frontmatter matches directory name (Roo constraint: lowercase alphanumeric + hyphens)
|
||||||
|
|
@ -580,8 +470,6 @@ async function runTests() {
|
||||||
'Roo skill name frontmatter matches directory name exactly (lowercase alphanumeric + hyphens)',
|
'Roo skill name frontmatter matches directory name exactly (lowercase alphanumeric + hyphens)',
|
||||||
);
|
);
|
||||||
|
|
||||||
assert(!(await fs.pathExists(path.join(tempProjectDir13, '.roo', 'commands'))), 'Roo setup removes legacy commands dir');
|
|
||||||
|
|
||||||
// Reinstall/upgrade: run setup again over existing skills output
|
// Reinstall/upgrade: run setup again over existing skills output
|
||||||
const result13b = await ideManager13.setup('roo', tempProjectDir13, installedBmadDir13, {
|
const result13b = await ideManager13.setup('roo', tempProjectDir13, installedBmadDir13, {
|
||||||
silent: true,
|
silent: true,
|
||||||
|
|
@ -615,31 +503,13 @@ async function runTests() {
|
||||||
const platformCodes17 = await loadPlatformCodes();
|
const platformCodes17 = await loadPlatformCodes();
|
||||||
const copilotInstaller = platformCodes17.platforms['github-copilot']?.installer;
|
const copilotInstaller = platformCodes17.platforms['github-copilot']?.installer;
|
||||||
|
|
||||||
assert(copilotInstaller?.target_dir === '.github/skills', 'GitHub Copilot target_dir uses native skills path');
|
assert(copilotInstaller?.target_dir === '.agents/skills', 'GitHub Copilot target_dir uses native skills path');
|
||||||
|
|
||||||
assert(
|
|
||||||
Array.isArray(copilotInstaller?.legacy_targets) && copilotInstaller.legacy_targets.includes('.github/agents'),
|
|
||||||
'GitHub Copilot installer cleans legacy agents output',
|
|
||||||
);
|
|
||||||
|
|
||||||
assert(
|
|
||||||
Array.isArray(copilotInstaller?.legacy_targets) && copilotInstaller.legacy_targets.includes('.github/prompts'),
|
|
||||||
'GitHub Copilot installer cleans legacy prompts output',
|
|
||||||
);
|
|
||||||
|
|
||||||
const tempProjectDir17 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-copilot-test-'));
|
const tempProjectDir17 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-copilot-test-'));
|
||||||
const installedBmadDir17 = await createTestBmadFixture();
|
const installedBmadDir17 = await createTestBmadFixture();
|
||||||
|
|
||||||
// Create legacy .github/agents/ and .github/prompts/ files
|
|
||||||
const legacyAgentsDir17 = path.join(tempProjectDir17, '.github', 'agents');
|
|
||||||
const legacyPromptsDir17 = path.join(tempProjectDir17, '.github', 'prompts');
|
|
||||||
await fs.ensureDir(legacyAgentsDir17);
|
|
||||||
await fs.ensureDir(legacyPromptsDir17);
|
|
||||||
await fs.writeFile(path.join(legacyAgentsDir17, 'bmad-legacy.agent.md'), 'legacy agent\n');
|
|
||||||
await fs.writeFile(path.join(legacyPromptsDir17, 'bmad-legacy.prompt.md'), 'legacy prompt\n');
|
|
||||||
|
|
||||||
// Create legacy copilot-instructions.md with BMAD markers
|
|
||||||
const copilotInstructionsPath17 = path.join(tempProjectDir17, '.github', 'copilot-instructions.md');
|
const copilotInstructionsPath17 = path.join(tempProjectDir17, '.github', 'copilot-instructions.md');
|
||||||
|
await fs.ensureDir(path.dirname(copilotInstructionsPath17));
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
copilotInstructionsPath17,
|
copilotInstructionsPath17,
|
||||||
'User content before\n<!-- BMAD:START -->\nBMAD generated content\n<!-- BMAD:END -->\nUser content after\n',
|
'User content before\n<!-- BMAD:START -->\nBMAD generated content\n<!-- BMAD:END -->\nUser content after\n',
|
||||||
|
|
@ -654,7 +524,7 @@ async function runTests() {
|
||||||
|
|
||||||
assert(result17.success === true, 'GitHub Copilot setup succeeds against temp project');
|
assert(result17.success === true, 'GitHub Copilot setup succeeds against temp project');
|
||||||
|
|
||||||
const skillFile17 = path.join(tempProjectDir17, '.github', 'skills', 'bmad-master', 'SKILL.md');
|
const skillFile17 = path.join(tempProjectDir17, '.agents', 'skills', 'bmad-master', 'SKILL.md');
|
||||||
assert(await fs.pathExists(skillFile17), 'GitHub Copilot install writes SKILL.md directory output');
|
assert(await fs.pathExists(skillFile17), 'GitHub Copilot install writes SKILL.md directory output');
|
||||||
|
|
||||||
// Verify name frontmatter matches directory name
|
// Verify name frontmatter matches directory name
|
||||||
|
|
@ -662,10 +532,6 @@ async function runTests() {
|
||||||
const nameMatch17 = skillContent17.match(/^name:\s*(.+)$/m);
|
const nameMatch17 = skillContent17.match(/^name:\s*(.+)$/m);
|
||||||
assert(nameMatch17 && nameMatch17[1].trim() === 'bmad-master', 'GitHub Copilot skill name frontmatter matches directory name exactly');
|
assert(nameMatch17 && nameMatch17[1].trim() === 'bmad-master', 'GitHub Copilot skill name frontmatter matches directory name exactly');
|
||||||
|
|
||||||
assert(!(await fs.pathExists(legacyAgentsDir17)), 'GitHub Copilot setup removes legacy agents dir');
|
|
||||||
|
|
||||||
assert(!(await fs.pathExists(legacyPromptsDir17)), 'GitHub Copilot setup removes legacy prompts dir');
|
|
||||||
|
|
||||||
// Verify copilot-instructions.md BMAD markers were stripped but user content preserved
|
// Verify copilot-instructions.md BMAD markers were stripped but user content preserved
|
||||||
const cleanedInstructions17 = await fs.readFile(copilotInstructionsPath17, 'utf8');
|
const cleanedInstructions17 = await fs.readFile(copilotInstructionsPath17, 'utf8');
|
||||||
assert(
|
assert(
|
||||||
|
|
@ -697,17 +563,8 @@ async function runTests() {
|
||||||
|
|
||||||
assert(clineInstaller?.target_dir === '.cline/skills', 'Cline target_dir uses native skills path');
|
assert(clineInstaller?.target_dir === '.cline/skills', 'Cline target_dir uses native skills path');
|
||||||
|
|
||||||
assert(
|
|
||||||
Array.isArray(clineInstaller?.legacy_targets) && clineInstaller.legacy_targets.includes('.clinerules/workflows'),
|
|
||||||
'Cline installer cleans legacy workflow output',
|
|
||||||
);
|
|
||||||
|
|
||||||
const tempProjectDir18 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-cline-test-'));
|
const tempProjectDir18 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-cline-test-'));
|
||||||
const installedBmadDir18 = await createTestBmadFixture();
|
const installedBmadDir18 = await createTestBmadFixture();
|
||||||
const legacyDir18 = path.join(tempProjectDir18, '.clinerules', 'workflows', 'bmad-legacy-dir');
|
|
||||||
await fs.ensureDir(legacyDir18);
|
|
||||||
await fs.writeFile(path.join(tempProjectDir18, '.clinerules', 'workflows', 'bmad-legacy.md'), 'legacy\n');
|
|
||||||
await fs.writeFile(path.join(legacyDir18, 'SKILL.md'), 'legacy\n');
|
|
||||||
|
|
||||||
const ideManager18 = new IdeManager();
|
const ideManager18 = new IdeManager();
|
||||||
await ideManager18.ensureInitialized();
|
await ideManager18.ensureInitialized();
|
||||||
|
|
@ -726,8 +583,6 @@ async function runTests() {
|
||||||
const nameMatch18 = skillContent18.match(/^name:\s*(.+)$/m);
|
const nameMatch18 = skillContent18.match(/^name:\s*(.+)$/m);
|
||||||
assert(nameMatch18 && nameMatch18[1].trim() === 'bmad-master', 'Cline skill name frontmatter matches directory name exactly');
|
assert(nameMatch18 && nameMatch18[1].trim() === 'bmad-master', 'Cline skill name frontmatter matches directory name exactly');
|
||||||
|
|
||||||
assert(!(await fs.pathExists(path.join(tempProjectDir18, '.clinerules', 'workflows'))), 'Cline setup removes legacy workflows dir');
|
|
||||||
|
|
||||||
// Reinstall/upgrade: run setup again over existing skills output
|
// Reinstall/upgrade: run setup again over existing skills output
|
||||||
const result18b = await ideManager18.setup('cline', tempProjectDir18, installedBmadDir18, {
|
const result18b = await ideManager18.setup('cline', tempProjectDir18, installedBmadDir18, {
|
||||||
silent: true,
|
silent: true,
|
||||||
|
|
@ -757,17 +612,8 @@ async function runTests() {
|
||||||
|
|
||||||
assert(codebuddyInstaller?.target_dir === '.codebuddy/skills', 'CodeBuddy target_dir uses native skills path');
|
assert(codebuddyInstaller?.target_dir === '.codebuddy/skills', 'CodeBuddy target_dir uses native skills path');
|
||||||
|
|
||||||
assert(
|
|
||||||
Array.isArray(codebuddyInstaller?.legacy_targets) && codebuddyInstaller.legacy_targets.includes('.codebuddy/commands'),
|
|
||||||
'CodeBuddy installer cleans legacy command output',
|
|
||||||
);
|
|
||||||
|
|
||||||
const tempProjectDir19 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-codebuddy-test-'));
|
const tempProjectDir19 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-codebuddy-test-'));
|
||||||
const installedBmadDir19 = await createTestBmadFixture();
|
const installedBmadDir19 = await createTestBmadFixture();
|
||||||
const legacyDir19 = path.join(tempProjectDir19, '.codebuddy', 'commands', 'bmad-legacy-dir');
|
|
||||||
await fs.ensureDir(legacyDir19);
|
|
||||||
await fs.writeFile(path.join(tempProjectDir19, '.codebuddy', 'commands', 'bmad-legacy.md'), 'legacy\n');
|
|
||||||
await fs.writeFile(path.join(legacyDir19, 'SKILL.md'), 'legacy\n');
|
|
||||||
|
|
||||||
const ideManager19 = new IdeManager();
|
const ideManager19 = new IdeManager();
|
||||||
await ideManager19.ensureInitialized();
|
await ideManager19.ensureInitialized();
|
||||||
|
|
@ -785,8 +631,6 @@ async function runTests() {
|
||||||
const nameMatch19 = skillContent19.match(/^name:\s*(.+)$/m);
|
const nameMatch19 = skillContent19.match(/^name:\s*(.+)$/m);
|
||||||
assert(nameMatch19 && nameMatch19[1].trim() === 'bmad-master', 'CodeBuddy skill name frontmatter matches directory name exactly');
|
assert(nameMatch19 && nameMatch19[1].trim() === 'bmad-master', 'CodeBuddy skill name frontmatter matches directory name exactly');
|
||||||
|
|
||||||
assert(!(await fs.pathExists(path.join(tempProjectDir19, '.codebuddy', 'commands'))), 'CodeBuddy setup removes legacy commands dir');
|
|
||||||
|
|
||||||
const result19b = await ideManager19.setup('codebuddy', tempProjectDir19, installedBmadDir19, {
|
const result19b = await ideManager19.setup('codebuddy', tempProjectDir19, installedBmadDir19, {
|
||||||
silent: true,
|
silent: true,
|
||||||
selectedModules: ['bmm'],
|
selectedModules: ['bmm'],
|
||||||
|
|
@ -813,19 +657,10 @@ async function runTests() {
|
||||||
const platformCodes20 = await loadPlatformCodes();
|
const platformCodes20 = await loadPlatformCodes();
|
||||||
const crushInstaller = platformCodes20.platforms.crush?.installer;
|
const crushInstaller = platformCodes20.platforms.crush?.installer;
|
||||||
|
|
||||||
assert(crushInstaller?.target_dir === '.crush/skills', 'Crush target_dir uses native skills path');
|
assert(crushInstaller?.target_dir === '.agents/skills', 'Crush target_dir uses native skills path');
|
||||||
|
|
||||||
assert(
|
|
||||||
Array.isArray(crushInstaller?.legacy_targets) && crushInstaller.legacy_targets.includes('.crush/commands'),
|
|
||||||
'Crush installer cleans legacy command output',
|
|
||||||
);
|
|
||||||
|
|
||||||
const tempProjectDir20 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-crush-test-'));
|
const tempProjectDir20 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-crush-test-'));
|
||||||
const installedBmadDir20 = await createTestBmadFixture();
|
const installedBmadDir20 = await createTestBmadFixture();
|
||||||
const legacyDir20 = path.join(tempProjectDir20, '.crush', 'commands', 'bmad-legacy-dir');
|
|
||||||
await fs.ensureDir(legacyDir20);
|
|
||||||
await fs.writeFile(path.join(tempProjectDir20, '.crush', 'commands', 'bmad-legacy.md'), 'legacy\n');
|
|
||||||
await fs.writeFile(path.join(legacyDir20, 'SKILL.md'), 'legacy\n');
|
|
||||||
|
|
||||||
const ideManager20 = new IdeManager();
|
const ideManager20 = new IdeManager();
|
||||||
await ideManager20.ensureInitialized();
|
await ideManager20.ensureInitialized();
|
||||||
|
|
@ -836,15 +671,13 @@ async function runTests() {
|
||||||
|
|
||||||
assert(result20.success === true, 'Crush setup succeeds against temp project');
|
assert(result20.success === true, 'Crush setup succeeds against temp project');
|
||||||
|
|
||||||
const skillFile20 = path.join(tempProjectDir20, '.crush', 'skills', 'bmad-master', 'SKILL.md');
|
const skillFile20 = path.join(tempProjectDir20, '.agents', 'skills', 'bmad-master', 'SKILL.md');
|
||||||
assert(await fs.pathExists(skillFile20), 'Crush install writes SKILL.md directory output');
|
assert(await fs.pathExists(skillFile20), 'Crush install writes SKILL.md directory output');
|
||||||
|
|
||||||
const skillContent20 = await fs.readFile(skillFile20, 'utf8');
|
const skillContent20 = await fs.readFile(skillFile20, 'utf8');
|
||||||
const nameMatch20 = skillContent20.match(/^name:\s*(.+)$/m);
|
const nameMatch20 = skillContent20.match(/^name:\s*(.+)$/m);
|
||||||
assert(nameMatch20 && nameMatch20[1].trim() === 'bmad-master', 'Crush skill name frontmatter matches directory name exactly');
|
assert(nameMatch20 && nameMatch20[1].trim() === 'bmad-master', 'Crush skill name frontmatter matches directory name exactly');
|
||||||
|
|
||||||
assert(!(await fs.pathExists(path.join(tempProjectDir20, '.crush', 'commands'))), 'Crush setup removes legacy commands dir');
|
|
||||||
|
|
||||||
const result20b = await ideManager20.setup('crush', tempProjectDir20, installedBmadDir20, {
|
const result20b = await ideManager20.setup('crush', tempProjectDir20, installedBmadDir20, {
|
||||||
silent: true,
|
silent: true,
|
||||||
selectedModules: ['bmm'],
|
selectedModules: ['bmm'],
|
||||||
|
|
@ -873,16 +706,8 @@ async function runTests() {
|
||||||
|
|
||||||
assert(traeInstaller?.target_dir === '.trae/skills', 'Trae target_dir uses native skills path');
|
assert(traeInstaller?.target_dir === '.trae/skills', 'Trae target_dir uses native skills path');
|
||||||
|
|
||||||
assert(
|
|
||||||
Array.isArray(traeInstaller?.legacy_targets) && traeInstaller.legacy_targets.includes('.trae/rules'),
|
|
||||||
'Trae installer cleans legacy rules output',
|
|
||||||
);
|
|
||||||
|
|
||||||
const tempProjectDir21 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-trae-test-'));
|
const tempProjectDir21 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-trae-test-'));
|
||||||
const installedBmadDir21 = await createTestBmadFixture();
|
const installedBmadDir21 = await createTestBmadFixture();
|
||||||
const legacyDir21 = path.join(tempProjectDir21, '.trae', 'rules');
|
|
||||||
await fs.ensureDir(legacyDir21);
|
|
||||||
await fs.writeFile(path.join(legacyDir21, 'bmad-legacy.md'), 'legacy\n');
|
|
||||||
|
|
||||||
const ideManager21 = new IdeManager();
|
const ideManager21 = new IdeManager();
|
||||||
await ideManager21.ensureInitialized();
|
await ideManager21.ensureInitialized();
|
||||||
|
|
@ -900,8 +725,6 @@ async function runTests() {
|
||||||
const nameMatch21 = skillContent21.match(/^name:\s*(.+)$/m);
|
const nameMatch21 = skillContent21.match(/^name:\s*(.+)$/m);
|
||||||
assert(nameMatch21 && nameMatch21[1].trim() === 'bmad-master', 'Trae skill name frontmatter matches directory name exactly');
|
assert(nameMatch21 && nameMatch21[1].trim() === 'bmad-master', 'Trae skill name frontmatter matches directory name exactly');
|
||||||
|
|
||||||
assert(!(await fs.pathExists(path.join(tempProjectDir21, '.trae', 'rules'))), 'Trae setup removes legacy rules dir');
|
|
||||||
|
|
||||||
const result21b = await ideManager21.setup('trae', tempProjectDir21, installedBmadDir21, {
|
const result21b = await ideManager21.setup('trae', tempProjectDir21, installedBmadDir21, {
|
||||||
silent: true,
|
silent: true,
|
||||||
selectedModules: ['bmm'],
|
selectedModules: ['bmm'],
|
||||||
|
|
@ -930,12 +753,7 @@ async function runTests() {
|
||||||
|
|
||||||
assert(!kiloConfig22?.suspended, 'KiloCoder is not suspended');
|
assert(!kiloConfig22?.suspended, 'KiloCoder is not suspended');
|
||||||
|
|
||||||
assert(kiloConfig22?.installer?.target_dir === '.kilocode/skills', 'KiloCoder target_dir uses native skills path');
|
assert(kiloConfig22?.installer?.target_dir === '.agents/skills', 'KiloCoder target_dir uses native skills path');
|
||||||
|
|
||||||
assert(
|
|
||||||
Array.isArray(kiloConfig22?.installer?.legacy_targets) && kiloConfig22.installer.legacy_targets.includes('.kilocode/workflows'),
|
|
||||||
'KiloCoder installer cleans legacy workflows output',
|
|
||||||
);
|
|
||||||
|
|
||||||
const ideManager22 = new IdeManager();
|
const ideManager22 = new IdeManager();
|
||||||
await ideManager22.ensureInitialized();
|
await ideManager22.ensureInitialized();
|
||||||
|
|
@ -950,11 +768,6 @@ async function runTests() {
|
||||||
const tempProjectDir22 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-kilo-test-'));
|
const tempProjectDir22 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-kilo-test-'));
|
||||||
const installedBmadDir22 = await createTestBmadFixture();
|
const installedBmadDir22 = await createTestBmadFixture();
|
||||||
|
|
||||||
// Pre-populate legacy Kilo artifacts that should be cleaned up
|
|
||||||
const legacyDir22 = path.join(tempProjectDir22, '.kilocode', 'workflows');
|
|
||||||
await fs.ensureDir(legacyDir22);
|
|
||||||
await fs.writeFile(path.join(legacyDir22, 'bmad-legacy.md'), 'legacy\n');
|
|
||||||
|
|
||||||
const result22 = await ideManager22.setup('kilo', tempProjectDir22, installedBmadDir22, {
|
const result22 = await ideManager22.setup('kilo', tempProjectDir22, installedBmadDir22, {
|
||||||
silent: true,
|
silent: true,
|
||||||
selectedModules: ['bmm'],
|
selectedModules: ['bmm'],
|
||||||
|
|
@ -962,15 +775,13 @@ async function runTests() {
|
||||||
|
|
||||||
assert(result22.success === true, 'KiloCoder setup succeeds against temp project');
|
assert(result22.success === true, 'KiloCoder setup succeeds against temp project');
|
||||||
|
|
||||||
const skillFile22 = path.join(tempProjectDir22, '.kilocode', 'skills', 'bmad-master', 'SKILL.md');
|
const skillFile22 = path.join(tempProjectDir22, '.agents', 'skills', 'bmad-master', 'SKILL.md');
|
||||||
assert(await fs.pathExists(skillFile22), 'KiloCoder install writes SKILL.md directory output');
|
assert(await fs.pathExists(skillFile22), 'KiloCoder install writes SKILL.md directory output');
|
||||||
|
|
||||||
const skillContent22 = await fs.readFile(skillFile22, 'utf8');
|
const skillContent22 = await fs.readFile(skillFile22, 'utf8');
|
||||||
const nameMatch22 = skillContent22.match(/^name:\s*(.+)$/m);
|
const nameMatch22 = skillContent22.match(/^name:\s*(.+)$/m);
|
||||||
assert(nameMatch22 && nameMatch22[1].trim() === 'bmad-master', 'KiloCoder skill name frontmatter matches directory name exactly');
|
assert(nameMatch22 && nameMatch22[1].trim() === 'bmad-master', 'KiloCoder skill name frontmatter matches directory name exactly');
|
||||||
|
|
||||||
assert(!(await fs.pathExists(path.join(tempProjectDir22, '.kilocode', 'workflows'))), 'KiloCoder setup removes legacy workflows dir');
|
|
||||||
|
|
||||||
const result22b = await ideManager22.setup('kilo', tempProjectDir22, installedBmadDir22, {
|
const result22b = await ideManager22.setup('kilo', tempProjectDir22, installedBmadDir22, {
|
||||||
silent: true,
|
silent: true,
|
||||||
selectedModules: ['bmm'],
|
selectedModules: ['bmm'],
|
||||||
|
|
@ -997,18 +808,10 @@ async function runTests() {
|
||||||
const platformCodes23 = await loadPlatformCodes();
|
const platformCodes23 = await loadPlatformCodes();
|
||||||
const geminiInstaller = platformCodes23.platforms.gemini?.installer;
|
const geminiInstaller = platformCodes23.platforms.gemini?.installer;
|
||||||
|
|
||||||
assert(geminiInstaller?.target_dir === '.gemini/skills', 'Gemini target_dir uses native skills path');
|
assert(geminiInstaller?.target_dir === '.agents/skills', 'Gemini target_dir uses native skills path');
|
||||||
|
|
||||||
assert(
|
|
||||||
Array.isArray(geminiInstaller?.legacy_targets) && geminiInstaller.legacy_targets.includes('.gemini/commands'),
|
|
||||||
'Gemini installer cleans legacy commands output',
|
|
||||||
);
|
|
||||||
|
|
||||||
const tempProjectDir23 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-gemini-test-'));
|
const tempProjectDir23 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-gemini-test-'));
|
||||||
const installedBmadDir23 = await createTestBmadFixture();
|
const installedBmadDir23 = await createTestBmadFixture();
|
||||||
const legacyDir23 = path.join(tempProjectDir23, '.gemini', 'commands');
|
|
||||||
await fs.ensureDir(legacyDir23);
|
|
||||||
await fs.writeFile(path.join(legacyDir23, 'bmad-legacy.toml'), 'legacy\n');
|
|
||||||
|
|
||||||
const ideManager23 = new IdeManager();
|
const ideManager23 = new IdeManager();
|
||||||
await ideManager23.ensureInitialized();
|
await ideManager23.ensureInitialized();
|
||||||
|
|
@ -1019,15 +822,13 @@ async function runTests() {
|
||||||
|
|
||||||
assert(result23.success === true, 'Gemini setup succeeds against temp project');
|
assert(result23.success === true, 'Gemini setup succeeds against temp project');
|
||||||
|
|
||||||
const skillFile23 = path.join(tempProjectDir23, '.gemini', 'skills', 'bmad-master', 'SKILL.md');
|
const skillFile23 = path.join(tempProjectDir23, '.agents', 'skills', 'bmad-master', 'SKILL.md');
|
||||||
assert(await fs.pathExists(skillFile23), 'Gemini install writes SKILL.md directory output');
|
assert(await fs.pathExists(skillFile23), 'Gemini install writes SKILL.md directory output');
|
||||||
|
|
||||||
const skillContent23 = await fs.readFile(skillFile23, 'utf8');
|
const skillContent23 = await fs.readFile(skillFile23, 'utf8');
|
||||||
const nameMatch23 = skillContent23.match(/^name:\s*(.+)$/m);
|
const nameMatch23 = skillContent23.match(/^name:\s*(.+)$/m);
|
||||||
assert(nameMatch23 && nameMatch23[1].trim() === 'bmad-master', 'Gemini skill name frontmatter matches directory name exactly');
|
assert(nameMatch23 && nameMatch23[1].trim() === 'bmad-master', 'Gemini skill name frontmatter matches directory name exactly');
|
||||||
|
|
||||||
assert(!(await fs.pathExists(path.join(tempProjectDir23, '.gemini', 'commands'))), 'Gemini setup removes legacy commands dir');
|
|
||||||
|
|
||||||
const result23b = await ideManager23.setup('gemini', tempProjectDir23, installedBmadDir23, {
|
const result23b = await ideManager23.setup('gemini', tempProjectDir23, installedBmadDir23, {
|
||||||
silent: true,
|
silent: true,
|
||||||
selectedModules: ['bmm'],
|
selectedModules: ['bmm'],
|
||||||
|
|
@ -1055,16 +856,9 @@ async function runTests() {
|
||||||
const iflowInstaller = platformCodes24.platforms.iflow?.installer;
|
const iflowInstaller = platformCodes24.platforms.iflow?.installer;
|
||||||
|
|
||||||
assert(iflowInstaller?.target_dir === '.iflow/skills', 'iFlow target_dir uses native skills path');
|
assert(iflowInstaller?.target_dir === '.iflow/skills', 'iFlow target_dir uses native skills path');
|
||||||
assert(
|
|
||||||
Array.isArray(iflowInstaller?.legacy_targets) && iflowInstaller.legacy_targets.includes('.iflow/commands'),
|
|
||||||
'iFlow installer cleans legacy commands output',
|
|
||||||
);
|
|
||||||
|
|
||||||
const tempProjectDir24 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-iflow-test-'));
|
const tempProjectDir24 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-iflow-test-'));
|
||||||
const installedBmadDir24 = await createTestBmadFixture();
|
const installedBmadDir24 = await createTestBmadFixture();
|
||||||
const legacyDir24 = path.join(tempProjectDir24, '.iflow', 'commands');
|
|
||||||
await fs.ensureDir(legacyDir24);
|
|
||||||
await fs.writeFile(path.join(legacyDir24, 'bmad-legacy.md'), 'legacy\n');
|
|
||||||
|
|
||||||
const ideManager24 = new IdeManager();
|
const ideManager24 = new IdeManager();
|
||||||
await ideManager24.ensureInitialized();
|
await ideManager24.ensureInitialized();
|
||||||
|
|
@ -1083,8 +877,6 @@ async function runTests() {
|
||||||
const nameMatch24 = skillContent24.match(/^name:\s*(.+)$/m);
|
const nameMatch24 = skillContent24.match(/^name:\s*(.+)$/m);
|
||||||
assert(nameMatch24 && nameMatch24[1].trim() === 'bmad-master', 'iFlow skill name frontmatter matches directory name exactly');
|
assert(nameMatch24 && nameMatch24[1].trim() === 'bmad-master', 'iFlow skill name frontmatter matches directory name exactly');
|
||||||
|
|
||||||
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(path.dirname(installedBmadDir24));
|
await fs.remove(path.dirname(installedBmadDir24));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -1104,16 +896,9 @@ async function runTests() {
|
||||||
const qwenInstaller = platformCodes25.platforms.qwen?.installer;
|
const qwenInstaller = platformCodes25.platforms.qwen?.installer;
|
||||||
|
|
||||||
assert(qwenInstaller?.target_dir === '.qwen/skills', 'QwenCoder target_dir uses native skills path');
|
assert(qwenInstaller?.target_dir === '.qwen/skills', 'QwenCoder target_dir uses native skills path');
|
||||||
assert(
|
|
||||||
Array.isArray(qwenInstaller?.legacy_targets) && qwenInstaller.legacy_targets.includes('.qwen/commands'),
|
|
||||||
'QwenCoder installer cleans legacy commands output',
|
|
||||||
);
|
|
||||||
|
|
||||||
const tempProjectDir25 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-qwen-test-'));
|
const tempProjectDir25 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-qwen-test-'));
|
||||||
const installedBmadDir25 = await createTestBmadFixture();
|
const installedBmadDir25 = await createTestBmadFixture();
|
||||||
const legacyDir25 = path.join(tempProjectDir25, '.qwen', 'commands');
|
|
||||||
await fs.ensureDir(legacyDir25);
|
|
||||||
await fs.writeFile(path.join(legacyDir25, 'bmad-legacy.md'), 'legacy\n');
|
|
||||||
|
|
||||||
const ideManager25 = new IdeManager();
|
const ideManager25 = new IdeManager();
|
||||||
await ideManager25.ensureInitialized();
|
await ideManager25.ensureInitialized();
|
||||||
|
|
@ -1132,8 +917,6 @@ async function runTests() {
|
||||||
const nameMatch25 = skillContent25.match(/^name:\s*(.+)$/m);
|
const nameMatch25 = skillContent25.match(/^name:\s*(.+)$/m);
|
||||||
assert(nameMatch25 && nameMatch25[1].trim() === 'bmad-master', 'QwenCoder skill name frontmatter matches directory name exactly');
|
assert(nameMatch25 && nameMatch25[1].trim() === 'bmad-master', 'QwenCoder skill name frontmatter matches directory name exactly');
|
||||||
|
|
||||||
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(path.dirname(installedBmadDir25));
|
await fs.remove(path.dirname(installedBmadDir25));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -1152,17 +935,10 @@ async function runTests() {
|
||||||
const platformCodes26 = await loadPlatformCodes();
|
const platformCodes26 = await loadPlatformCodes();
|
||||||
const rovoInstaller = platformCodes26.platforms['rovo-dev']?.installer;
|
const rovoInstaller = platformCodes26.platforms['rovo-dev']?.installer;
|
||||||
|
|
||||||
assert(rovoInstaller?.target_dir === '.rovodev/skills', 'Rovo Dev target_dir uses native skills path');
|
assert(rovoInstaller?.target_dir === '.agents/skills', 'Rovo Dev target_dir uses native skills path');
|
||||||
assert(
|
|
||||||
Array.isArray(rovoInstaller?.legacy_targets) && rovoInstaller.legacy_targets.includes('.rovodev/workflows'),
|
|
||||||
'Rovo Dev installer cleans legacy workflows output',
|
|
||||||
);
|
|
||||||
|
|
||||||
const tempProjectDir26 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-rovodev-test-'));
|
const tempProjectDir26 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-rovodev-test-'));
|
||||||
const installedBmadDir26 = await createTestBmadFixture();
|
const installedBmadDir26 = await createTestBmadFixture();
|
||||||
const legacyDir26 = path.join(tempProjectDir26, '.rovodev', 'workflows');
|
|
||||||
await fs.ensureDir(legacyDir26);
|
|
||||||
await fs.writeFile(path.join(legacyDir26, 'bmad-legacy.md'), 'legacy\n');
|
|
||||||
|
|
||||||
// Create a prompts.yml with BMAD entries and a user entry
|
// Create a prompts.yml with BMAD entries and a user entry
|
||||||
const yaml26 = require('yaml');
|
const yaml26 = require('yaml');
|
||||||
|
|
@ -1173,6 +949,7 @@ async function runTests() {
|
||||||
{ name: 'my-custom-prompt', description: 'User prompt', content_file: 'custom.md' },
|
{ name: 'my-custom-prompt', description: 'User prompt', content_file: 'custom.md' },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
await fs.ensureDir(path.dirname(promptsPath26));
|
||||||
await fs.writeFile(promptsPath26, promptsContent26);
|
await fs.writeFile(promptsPath26, promptsContent26);
|
||||||
|
|
||||||
const ideManager26 = new IdeManager();
|
const ideManager26 = new IdeManager();
|
||||||
|
|
@ -1184,7 +961,7 @@ async function runTests() {
|
||||||
|
|
||||||
assert(result26.success === true, 'Rovo Dev setup succeeds against temp project');
|
assert(result26.success === true, 'Rovo Dev setup succeeds against temp project');
|
||||||
|
|
||||||
const skillFile26 = path.join(tempProjectDir26, '.rovodev', 'skills', 'bmad-master', 'SKILL.md');
|
const skillFile26 = path.join(tempProjectDir26, '.agents', 'skills', 'bmad-master', 'SKILL.md');
|
||||||
assert(await fs.pathExists(skillFile26), 'Rovo Dev install writes SKILL.md directory output');
|
assert(await fs.pathExists(skillFile26), 'Rovo Dev install writes SKILL.md directory output');
|
||||||
|
|
||||||
// Verify name frontmatter matches directory name
|
// Verify name frontmatter matches directory name
|
||||||
|
|
@ -1192,8 +969,6 @@ async function runTests() {
|
||||||
const nameMatch26 = skillContent26.match(/^name:\s*(.+)$/m);
|
const nameMatch26 = skillContent26.match(/^name:\s*(.+)$/m);
|
||||||
assert(nameMatch26 && nameMatch26[1].trim() === 'bmad-master', 'Rovo Dev skill name frontmatter matches directory name exactly');
|
assert(nameMatch26 && nameMatch26[1].trim() === 'bmad-master', 'Rovo Dev skill name frontmatter matches directory name exactly');
|
||||||
|
|
||||||
assert(!(await fs.pathExists(path.join(tempProjectDir26, '.rovodev', 'workflows'))), 'Rovo Dev setup removes legacy workflows dir');
|
|
||||||
|
|
||||||
// Verify prompts.yml cleanup: BMAD entries removed, user entry preserved
|
// Verify prompts.yml cleanup: BMAD entries removed, user entry preserved
|
||||||
const cleanedPrompts26 = yaml26.parse(await fs.readFile(promptsPath26, 'utf8'));
|
const cleanedPrompts26 = yaml26.parse(await fs.readFile(promptsPath26, 'utf8'));
|
||||||
assert(
|
assert(
|
||||||
|
|
@ -1295,7 +1070,7 @@ async function runTests() {
|
||||||
const platformCodes28 = await loadPlatformCodes();
|
const platformCodes28 = await loadPlatformCodes();
|
||||||
const piInstaller = platformCodes28.platforms.pi?.installer;
|
const piInstaller = platformCodes28.platforms.pi?.installer;
|
||||||
|
|
||||||
assert(piInstaller?.target_dir === '.pi/skills', 'Pi target_dir uses native skills path');
|
assert(piInstaller?.target_dir === '.agents/skills', 'Pi target_dir uses native skills path');
|
||||||
|
|
||||||
tempProjectDir28 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-pi-test-'));
|
tempProjectDir28 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-pi-test-'));
|
||||||
installedBmadDir28 = await createTestBmadFixture();
|
installedBmadDir28 = await createTestBmadFixture();
|
||||||
|
|
@ -1325,7 +1100,7 @@ async function runTests() {
|
||||||
const detectedAfter28 = await ideManager28.detectInstalledIdes(tempProjectDir28);
|
const detectedAfter28 = await ideManager28.detectInstalledIdes(tempProjectDir28);
|
||||||
assert(detectedAfter28.includes('pi'), 'Pi is detected after install');
|
assert(detectedAfter28.includes('pi'), 'Pi is detected after install');
|
||||||
|
|
||||||
const skillFile28 = path.join(tempProjectDir28, '.pi', 'skills', 'bmad-master', 'SKILL.md');
|
const skillFile28 = path.join(tempProjectDir28, '.agents', 'skills', 'bmad-master', 'SKILL.md');
|
||||||
assert(await fs.pathExists(skillFile28), 'Pi install writes SKILL.md directory output');
|
assert(await fs.pathExists(skillFile28), 'Pi install writes SKILL.md directory output');
|
||||||
|
|
||||||
// Parse YAML frontmatter between --- markers
|
// Parse YAML frontmatter between --- markers
|
||||||
|
|
@ -1607,7 +1382,7 @@ 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 === '1 skills', 'Installer detail reports skill count');
|
assert(result.detail === '1 skills → .agent/skills', 'Installer detail reports skill count and target dir');
|
||||||
assert(result.handlerResult.results.skillDirectories === 1, 'Result exposes unique skill directory count');
|
assert(result.handlerResult.results.skillDirectories === 1, 'Result exposes unique skill directory count');
|
||||||
assert(result.handlerResult.results.skills === 1, 'Result retains verbatim skill count');
|
assert(result.handlerResult.results.skills === 1, 'Result retains verbatim skill count');
|
||||||
assert(
|
assert(
|
||||||
|
|
@ -2847,6 +2622,157 @@ async function runTests() {
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Test Suite 40: Shared target_dir coordination
|
||||||
|
// ============================================================
|
||||||
|
console.log(`${colors.yellow}Test Suite 40: Shared target_dir coordination${colors.reset}\n`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Cursor and Gemini both use .agents/skills — verify they coordinate.
|
||||||
|
clearCache();
|
||||||
|
const platformCodes40 = await loadPlatformCodes();
|
||||||
|
const cursorTarget = platformCodes40.platforms.cursor?.installer?.target_dir;
|
||||||
|
const geminiTarget = platformCodes40.platforms.gemini?.installer?.target_dir;
|
||||||
|
assert(cursorTarget === '.agents/skills' && geminiTarget === '.agents/skills', 'Cursor and Gemini share .agents/skills target_dir');
|
||||||
|
|
||||||
|
const tempProjectDir40 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-shared-target-'));
|
||||||
|
const installedBmadDir40 = await createTestBmadFixture();
|
||||||
|
|
||||||
|
const ideManager40 = new IdeManager();
|
||||||
|
await ideManager40.ensureInitialized();
|
||||||
|
|
||||||
|
// Run setupBatch with both platforms — second should skip skill write.
|
||||||
|
const batchResults = await ideManager40.setupBatch(['cursor', 'gemini'], tempProjectDir40, installedBmadDir40, {
|
||||||
|
silent: true,
|
||||||
|
selectedModules: ['core'],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(batchResults.length === 2, 'setupBatch returns one result per IDE');
|
||||||
|
assert(batchResults[0].success === true, 'First platform (cursor) succeeds');
|
||||||
|
assert(batchResults[1].success === true, 'Second platform (gemini) succeeds');
|
||||||
|
assert(
|
||||||
|
batchResults[1].handlerResult?.results?.sharedTargetHandledByPeer === true,
|
||||||
|
'Second platform marked sharedTargetHandledByPeer (skipped redundant write)',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Skill should be present in the shared dir after batch.
|
||||||
|
const sharedDir = path.join(tempProjectDir40, '.agents', 'skills');
|
||||||
|
const sharedDirEntries = await fs.readdir(sharedDir);
|
||||||
|
assert(sharedDirEntries.includes('bmad-master'), 'Shared .agents/skills/ contains bmad-master after batched install');
|
||||||
|
|
||||||
|
// Now uninstall just cursor while gemini remains. Skills must survive.
|
||||||
|
const cleanupResults = await ideManager40.cleanupByList(tempProjectDir40, ['cursor'], {
|
||||||
|
silent: true,
|
||||||
|
remainingIdes: ['gemini'],
|
||||||
|
});
|
||||||
|
assert(cleanupResults[0].skippedTarget === true, 'Cursor cleanup skips target_dir wipe when Gemini remains');
|
||||||
|
const stillThere = await fs.readdir(sharedDir);
|
||||||
|
assert(stillThere.includes('bmad-master'), 'bmad-master still present after partial uninstall (gemini still installed)');
|
||||||
|
|
||||||
|
// (Cleanup of the last sharing platform requires bmadDir to be inside
|
||||||
|
// projectDir to compute removalSet; that's the production layout. The
|
||||||
|
// fixture above keeps bmad in a separate temp dir, so test 41 below
|
||||||
|
// exercises the in-project layout instead.)
|
||||||
|
|
||||||
|
await fs.remove(tempProjectDir40).catch(() => {});
|
||||||
|
await fs.remove(path.dirname(installedBmadDir40)).catch(() => {});
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`${colors.red}Test Suite 40 setup failed: ${error.message}${colors.reset}`);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Test Suite 40b: setupBatch — failed first writer does not poison peers
|
||||||
|
// ============================================================
|
||||||
|
console.log(`${colors.yellow}Test Suite 40b: setupBatch resilience to first-writer failure${colors.reset}\n`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tempProjectDir40b = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-batch-fail-'));
|
||||||
|
const installedBmadDir40b = await createTestBmadFixture();
|
||||||
|
|
||||||
|
const ideManager40b = new IdeManager();
|
||||||
|
await ideManager40b.ensureInitialized();
|
||||||
|
|
||||||
|
// Force cursor's setup() to fail. With the bug, gemini would see the
|
||||||
|
// claimed target and skip — leaving .agents/skills/ empty.
|
||||||
|
const cursorHandler40b = ideManager40b.handlers.get('cursor');
|
||||||
|
const originalSetup = cursorHandler40b.setup.bind(cursorHandler40b);
|
||||||
|
cursorHandler40b.setup = async () => {
|
||||||
|
throw new Error('Simulated cursor failure');
|
||||||
|
};
|
||||||
|
|
||||||
|
const batchResults40b = await ideManager40b.setupBatch(['cursor', 'gemini'], tempProjectDir40b, installedBmadDir40b, {
|
||||||
|
silent: true,
|
||||||
|
selectedModules: ['core'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restore so other tests aren't affected.
|
||||||
|
cursorHandler40b.setup = originalSetup;
|
||||||
|
|
||||||
|
assert(batchResults40b[0].success === false, 'Cursor reports failure');
|
||||||
|
assert(batchResults40b[1].success === true, 'Gemini still succeeds despite cursor failure');
|
||||||
|
assert(
|
||||||
|
batchResults40b[1].handlerResult?.results?.sharedTargetHandledByPeer !== true,
|
||||||
|
'Gemini does NOT skip its own write — it becomes the new first writer',
|
||||||
|
);
|
||||||
|
|
||||||
|
const sharedDir40b = path.join(tempProjectDir40b, '.agents', 'skills');
|
||||||
|
const entries40b = await fs.readdir(sharedDir40b);
|
||||||
|
assert(entries40b.includes('bmad-master'), 'Shared dir is populated by gemini after cursor failure');
|
||||||
|
|
||||||
|
await fs.remove(tempProjectDir40b).catch(() => {});
|
||||||
|
await fs.remove(path.dirname(installedBmadDir40b)).catch(() => {});
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`${colors.red}Test Suite 40b setup failed: ${error.message}${colors.reset}`);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Test Suite 41: Custom-module skill ownership (non-bmad prefix)
|
||||||
|
// ============================================================
|
||||||
|
console.log(`${colors.yellow}Test Suite 41: Custom-module skill ownership${colors.reset}\n`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// A custom module can ship a skill with any canonicalId (e.g. "fred-cool-skill").
|
||||||
|
// detect() must recognize it as BMAD-owned via the manifest, not the bmad- prefix.
|
||||||
|
const fixtureRoot41 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-custom-prefix-'));
|
||||||
|
const bmadDir41 = path.join(fixtureRoot41, '_bmad');
|
||||||
|
await fs.ensureDir(path.join(bmadDir41, '_config'));
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(bmadDir41, '_config', 'skill-manifest.csv'),
|
||||||
|
[
|
||||||
|
'canonicalId,name,description,module,path',
|
||||||
|
'"fred-cool-skill","fred-cool-skill","Custom module skill","fred","_bmad/fred/skills/fred-cool-skill/SKILL.md"',
|
||||||
|
'',
|
||||||
|
].join('\n'),
|
||||||
|
);
|
||||||
|
const fredSkill = path.join(bmadDir41, 'fred', 'skills', 'fred-cool-skill');
|
||||||
|
await fs.ensureDir(fredSkill);
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(fredSkill, 'SKILL.md'),
|
||||||
|
['---', 'name: fred-cool-skill', 'description: Custom module skill', '---', '', 'A custom module skill.'].join('\n'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const ideManager41 = new IdeManager();
|
||||||
|
await ideManager41.ensureInitialized();
|
||||||
|
await ideManager41.setup('cursor', fixtureRoot41, bmadDir41, { silent: true, selectedModules: ['fred'] });
|
||||||
|
|
||||||
|
const cursorHandler = ideManager41.handlers.get('cursor');
|
||||||
|
const detected = await cursorHandler.detect(fixtureRoot41);
|
||||||
|
assert(detected === true, 'detect() recognizes non-bmad-prefixed skill as BMAD-owned via skill-manifest.csv');
|
||||||
|
|
||||||
|
await fs.remove(fixtureRoot41).catch(() => {});
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`${colors.red}Test Suite 41 setup failed: ${error.message}${colors.reset}`);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Summary
|
// Summary
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -222,7 +222,6 @@ Support assumption: full Agent Skills support. Gemini CLI docs confirm workspace
|
||||||
|
|
||||||
- [x] Confirm Gemini CLI native skills path is `.gemini/skills/{skill-name}/SKILL.md` (per [geminicli.com/docs/cli/skills](https://geminicli.com/docs/cli/skills/))
|
- [x] Confirm Gemini CLI native skills path is `.gemini/skills/{skill-name}/SKILL.md` (per [geminicli.com/docs/cli/skills](https://geminicli.com/docs/cli/skills/))
|
||||||
- [x] Implement native skills output — target_dir `.gemini/skills`, skill_format true, template_type default (replaces TOML templates)
|
- [x] Implement native skills output — target_dir `.gemini/skills`, skill_format true, template_type default (replaces TOML templates)
|
||||||
- [x] Add legacy cleanup for `.gemini/commands` (via `legacy_targets`)
|
|
||||||
- [x] Test fresh install — skills written to `.gemini/skills/bmad-master/SKILL.md` with correct frontmatter
|
- [x] Test fresh install — skills written to `.gemini/skills/bmad-master/SKILL.md` with correct frontmatter
|
||||||
- [x] Test reinstall/upgrade from legacy TOML command output — legacy dir removed, skills installed
|
- [x] Test reinstall/upgrade from legacy TOML command output — legacy dir removed, skills installed
|
||||||
- [x] Confirm no ancestor conflict protection is needed — Gemini CLI uses workspace > user > extension precedence, no ancestor directory inheritance
|
- [x] Confirm no ancestor conflict protection is needed — Gemini CLI uses workspace > user > extension precedence, no ancestor directory inheritance
|
||||||
|
|
@ -236,7 +235,6 @@ Support assumption: full Agent Skills support. iFlow docs confirm workspace skil
|
||||||
|
|
||||||
- [x] Confirm iFlow native skills path is `.iflow/skills/{skill-name}/SKILL.md`
|
- [x] Confirm iFlow native skills path is `.iflow/skills/{skill-name}/SKILL.md`
|
||||||
- [x] Implement native skills output — target_dir `.iflow/skills`, skill_format true, template_type default
|
- [x] Implement native skills output — target_dir `.iflow/skills`, skill_format true, template_type default
|
||||||
- [x] Add legacy cleanup for `.iflow/commands` (via `legacy_targets`)
|
|
||||||
- [x] Test fresh install — skills written to `.iflow/skills/bmad-master/SKILL.md`
|
- [x] Test fresh install — skills written to `.iflow/skills/bmad-master/SKILL.md`
|
||||||
- [x] Test legacy cleanup — legacy commands dir removed
|
- [x] Test legacy cleanup — legacy commands dir removed
|
||||||
- [x] Implement/extend automated tests — 6 assertions in test suite 24
|
- [x] Implement/extend automated tests — 6 assertions in test suite 24
|
||||||
|
|
@ -249,7 +247,6 @@ Support assumption: full Agent Skills support. Qwen Code supports workspace skil
|
||||||
|
|
||||||
- [x] Confirm QwenCoder native skills path is `.qwen/skills/{skill-name}/SKILL.md`
|
- [x] Confirm QwenCoder native skills path is `.qwen/skills/{skill-name}/SKILL.md`
|
||||||
- [x] Implement native skills output — target_dir `.qwen/skills`, skill_format true, template_type default
|
- [x] Implement native skills output — target_dir `.qwen/skills`, skill_format true, template_type default
|
||||||
- [x] Add legacy cleanup for `.qwen/commands` (via `legacy_targets`)
|
|
||||||
- [x] Test fresh install — skills written to `.qwen/skills/bmad-master/SKILL.md`
|
- [x] Test fresh install — skills written to `.qwen/skills/bmad-master/SKILL.md`
|
||||||
- [x] Test legacy cleanup — legacy commands dir removed
|
- [x] Test legacy cleanup — legacy commands dir removed
|
||||||
- [x] Implement/extend automated tests — 6 assertions in test suite 25
|
- [x] Implement/extend automated tests — 6 assertions in test suite 25
|
||||||
|
|
@ -262,7 +259,6 @@ Support assumption: full Agent Skills support. Rovo Dev now supports workspace s
|
||||||
|
|
||||||
- [x] Confirm Rovo Dev native skills path is `.rovodev/skills/{skill-name}/SKILL.md` (per Atlassian blog)
|
- [x] Confirm Rovo Dev native skills path is `.rovodev/skills/{skill-name}/SKILL.md` (per Atlassian blog)
|
||||||
- [x] Replace 257-line custom `rovodev.js` with config-driven entry in `platform-codes.yaml`
|
- [x] Replace 257-line custom `rovodev.js` with config-driven entry in `platform-codes.yaml`
|
||||||
- [x] Add legacy cleanup for `.rovodev/workflows` (via `legacy_targets`) and BMAD entries in `prompts.yml` (via `cleanupRovoDevPrompts()` in `_config-driven.js`)
|
|
||||||
- [x] Test fresh install — skills written to `.rovodev/skills/bmad-master/SKILL.md`
|
- [x] Test fresh install — skills written to `.rovodev/skills/bmad-master/SKILL.md`
|
||||||
- [x] Test legacy cleanup — legacy workflows dir removed, `prompts.yml` BMAD entries stripped while preserving user entries
|
- [x] Test legacy cleanup — legacy workflows dir removed, `prompts.yml` BMAD entries stripped while preserving user entries
|
||||||
- [x] Implement/extend automated tests — 8 assertions in test suite 26
|
- [x] Implement/extend automated tests — 8 assertions in test suite 26
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ const { ExternalModuleManager } = require('../modules/external-manager');
|
||||||
const { resolveModuleVersion } = require('../modules/version-resolver');
|
const { resolveModuleVersion } = require('../modules/version-resolver');
|
||||||
|
|
||||||
const { ExistingInstall } = require('./existing-install');
|
const { ExistingInstall } = require('./existing-install');
|
||||||
|
const { warnPreNativeSkillsLegacy } = require('./legacy-warnings');
|
||||||
|
|
||||||
class Installer {
|
class Installer {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -41,6 +42,16 @@ class Installer {
|
||||||
const officialModules = await OfficialModules.build(config, paths);
|
const officialModules = await OfficialModules.build(config, paths);
|
||||||
const existingInstall = await ExistingInstall.detect(paths.bmadDir);
|
const existingInstall = await ExistingInstall.detect(paths.bmadDir);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await warnPreNativeSkillsLegacy({
|
||||||
|
projectRoot: paths.projectRoot,
|
||||||
|
existingVersion: existingInstall.installed ? existingInstall.version : null,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Legacy-dir scan is informational; never let it abort install.
|
||||||
|
await prompts.log.warn(`Warning: Could not check for legacy BMAD entries: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (existingInstall.installed) {
|
if (existingInstall.installed) {
|
||||||
await this._removeDeselectedModules(existingInstall, config, paths);
|
await this._removeDeselectedModules(existingInstall, config, paths);
|
||||||
updateState = await this._prepareUpdateState(paths, config, existingInstall, officialModules);
|
updateState = await this._prepareUpdateState(paths, config, existingInstall, officialModules);
|
||||||
|
|
@ -183,15 +194,16 @@ class Installer {
|
||||||
|
|
||||||
if (toRemove.length === 0) return;
|
if (toRemove.length === 0) return;
|
||||||
|
|
||||||
await this.ideManager.ensureInitialized();
|
// Pass the newly-selected list as remainingIdes so cleanupByList skips
|
||||||
for (const ide of toRemove) {
|
// target_dir wipes for IDEs whose directory is still owned by a peer
|
||||||
try {
|
// (e.g. removing 'cursor' while 'gemini' remains — both share .agents/skills).
|
||||||
const handler = this.ideManager.handlers.get(ide);
|
const results = await this.ideManager.cleanupByList(paths.projectRoot, toRemove, {
|
||||||
if (handler) {
|
remainingIdes: [...newlySelected],
|
||||||
await handler.cleanup(paths.projectRoot);
|
});
|
||||||
}
|
|
||||||
} catch (error) {
|
for (const result of results || []) {
|
||||||
await prompts.log.warn(`Warning: Failed to remove ${ide}: ${error.message}`);
|
if (result && result.success === false) {
|
||||||
|
await prompts.log.warn(`Warning: Failed to remove ${result.ide}: ${result.error || 'unknown error'}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -342,13 +354,14 @@ class Installer {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const ide of validIdes) {
|
const setupResults = await this.ideManager.setupBatch(validIdes, paths.projectRoot, paths.bmadDir, {
|
||||||
const setupResult = await this.ideManager.setup(ide, paths.projectRoot, paths.bmadDir, {
|
|
||||||
selectedModules: allModules || [],
|
selectedModules: allModules || [],
|
||||||
verbose: config.verbose,
|
verbose: config.verbose,
|
||||||
previousSkillIds,
|
previousSkillIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const setupResult of setupResults) {
|
||||||
|
const ide = setupResult.ide;
|
||||||
if (setupResult.success) {
|
if (setupResult.success) {
|
||||||
addResult(ide, 'ok', setupResult.detail || '');
|
addResult(ide, 'ok', setupResult.detail || '');
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
const os = require('node:os');
|
||||||
|
const path = require('node:path');
|
||||||
|
const semver = require('semver');
|
||||||
|
const fs = require('../fs-native');
|
||||||
|
const prompts = require('../prompts');
|
||||||
|
const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');
|
||||||
|
const { getInstalledCanonicalIds, isBmadOwnedEntry } = require('../ide/shared/installed-skills');
|
||||||
|
|
||||||
|
const MIN_NATIVE_SKILLS_VERSION = '6.1.0';
|
||||||
|
|
||||||
|
// Pre-v6.1.0 paths: BMAD used to install commands/workflows/etc in tool-specific dirs.
|
||||||
|
// In v6.1.0 BMAD switched to native SKILL.md format.
|
||||||
|
const LEGACY_COMMAND_PATHS = [
|
||||||
|
'.agent/workflows',
|
||||||
|
'.augment/commands',
|
||||||
|
'.claude/commands',
|
||||||
|
'.clinerules/workflows',
|
||||||
|
'.codex/prompts',
|
||||||
|
'~/.codex/prompts',
|
||||||
|
'.codebuddy/commands',
|
||||||
|
'.crush/commands',
|
||||||
|
'.cursor/commands',
|
||||||
|
'.gemini/commands',
|
||||||
|
'.github/agents',
|
||||||
|
'.github/prompts',
|
||||||
|
'.iflow/commands',
|
||||||
|
'.kilocode/workflows',
|
||||||
|
'.kiro/steering',
|
||||||
|
'.opencode/agents',
|
||||||
|
'.opencode/commands',
|
||||||
|
'.opencode/agent',
|
||||||
|
'.opencode/command',
|
||||||
|
'.qwen/commands',
|
||||||
|
'.roo/commands',
|
||||||
|
'.rovodev/workflows',
|
||||||
|
'.trae/rules',
|
||||||
|
'.windsurf/workflows',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Skill paths that moved to the cross-tool .agents/skills/ standard.
|
||||||
|
// Users upgrading from a prior install may have stale BMAD skills here that
|
||||||
|
// the AI tool will load alongside the new ones, causing duplicates.
|
||||||
|
const LEGACY_SKILL_PATHS = [
|
||||||
|
'.augment/skills',
|
||||||
|
'~/.augment/skills',
|
||||||
|
'.codex/skills',
|
||||||
|
'.crush/skills',
|
||||||
|
'.cursor/skills',
|
||||||
|
'~/.cursor/skills',
|
||||||
|
'.gemini/skills',
|
||||||
|
'~/.gemini/skills',
|
||||||
|
'.github/skills',
|
||||||
|
'~/.github/skills',
|
||||||
|
'.kilocode/skills',
|
||||||
|
'.kimi/skills',
|
||||||
|
'~/.kimi/skills',
|
||||||
|
'.opencode/skills',
|
||||||
|
'~/.opencode/skills',
|
||||||
|
'.pi/skills',
|
||||||
|
'~/.pi/skills',
|
||||||
|
'.roo/skills',
|
||||||
|
'~/.roo/skills',
|
||||||
|
'.rovodev/skills',
|
||||||
|
'~/.rovodev/skills',
|
||||||
|
'.windsurf/skills',
|
||||||
|
'~/.windsurf/skills',
|
||||||
|
'~/.codeium/windsurf/skills',
|
||||||
|
];
|
||||||
|
|
||||||
|
const LEGACY_PATHS = [...LEGACY_COMMAND_PATHS, ...LEGACY_SKILL_PATHS];
|
||||||
|
|
||||||
|
function expandPath(p) {
|
||||||
|
if (p === '~') return os.homedir();
|
||||||
|
if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2));
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveLegacyPath(projectRoot, p) {
|
||||||
|
if (path.isAbsolute(p) || p.startsWith('~')) return expandPath(p);
|
||||||
|
return path.join(projectRoot, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findStaleLegacyDirs(projectRoot) {
|
||||||
|
const bmadDir = path.join(projectRoot, BMAD_FOLDER_NAME);
|
||||||
|
const canonicalIds = await getInstalledCanonicalIds(bmadDir);
|
||||||
|
|
||||||
|
const findings = [];
|
||||||
|
for (const legacyPath of LEGACY_PATHS) {
|
||||||
|
const resolved = resolveLegacyPath(projectRoot, legacyPath);
|
||||||
|
if (!(await fs.pathExists(resolved))) continue;
|
||||||
|
try {
|
||||||
|
const entries = await fs.readdir(resolved);
|
||||||
|
const bmadEntries = entries.filter((e) => isBmadOwnedEntry(e, canonicalIds));
|
||||||
|
if (bmadEntries.length > 0) {
|
||||||
|
findings.push({ path: resolved, displayPath: legacyPath, count: bmadEntries.length, entries: bmadEntries });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Unreadable dir — skip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return findings;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPreNativeSkillsVersion(version) {
|
||||||
|
if (!version) return false;
|
||||||
|
const coerced = semver.valid(version) || semver.valid(semver.coerce(version));
|
||||||
|
if (!coerced) return false;
|
||||||
|
return semver.lt(coerced, MIN_NATIVE_SKILLS_VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function warnPreNativeSkillsLegacy({ projectRoot, existingVersion } = {}) {
|
||||||
|
const versionTriggered = isPreNativeSkillsVersion(existingVersion);
|
||||||
|
const staleDirs = await findStaleLegacyDirs(projectRoot);
|
||||||
|
|
||||||
|
if (!versionTriggered && staleDirs.length === 0) return;
|
||||||
|
|
||||||
|
if (versionTriggered) {
|
||||||
|
await prompts.log.warn(
|
||||||
|
`Detected previous BMAD install v${existingVersion} (pre-${MIN_NATIVE_SKILLS_VERSION}). ` +
|
||||||
|
`BMAD switched to native skills format in v${MIN_NATIVE_SKILLS_VERSION}; old command/workflow directories from your prior install may still be present.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (staleDirs.length > 0) {
|
||||||
|
await prompts.log.warn(
|
||||||
|
`Found stale BMAD entries in ${staleDirs.length} legacy location(s) that the new installer no longer manages. ` +
|
||||||
|
`Your AI tool may load these alongside the new skills, causing duplicates. Remove them manually:`,
|
||||||
|
);
|
||||||
|
for (const finding of staleDirs) {
|
||||||
|
// Print each entry by exact name. A `bmad*` glob would (a) miss
|
||||||
|
// custom-module skills the canonicalId scan now picks up, and
|
||||||
|
// (b) match bmad-os-* utility skills the user should keep.
|
||||||
|
const entries = finding.entries || [];
|
||||||
|
for (const entry of entries) {
|
||||||
|
await prompts.log.message(` rm -rf "${path.join(finding.path, entry)}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (versionTriggered) {
|
||||||
|
await prompts.log.message(
|
||||||
|
' No stale legacy directories detected, but if your AI tool shows duplicate BMAD commands after install, check for old `bmad-*` entries in tool-specific dirs (e.g. .claude/commands, .cursor/commands).',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
warnPreNativeSkillsLegacy,
|
||||||
|
findStaleLegacyDirs,
|
||||||
|
isPreNativeSkillsVersion,
|
||||||
|
LEGACY_PATHS,
|
||||||
|
MIN_NATIVE_SKILLS_VERSION,
|
||||||
|
};
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
const os = require('node:os');
|
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const fs = require('../fs-native');
|
const fs = require('../fs-native');
|
||||||
const yaml = require('yaml');
|
const yaml = require('yaml');
|
||||||
const prompts = require('../prompts');
|
const prompts = require('../prompts');
|
||||||
const csv = require('csv-parse/sync');
|
const csv = require('csv-parse/sync');
|
||||||
const { BMAD_FOLDER_NAME } = require('./shared/path-utils');
|
const { BMAD_FOLDER_NAME } = require('./shared/path-utils');
|
||||||
|
const { getInstalledCanonicalIds, isBmadOwnedEntry } = require('./shared/installed-skills');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Config-driven IDE setup handler
|
* Config-driven IDE setup handler
|
||||||
|
|
@ -16,7 +16,7 @@ const { BMAD_FOLDER_NAME } = require('./shared/path-utils');
|
||||||
* Features:
|
* Features:
|
||||||
* - Config-driven from platform-codes.yaml
|
* - Config-driven from platform-codes.yaml
|
||||||
* - Verbatim skill installation from skill-manifest.csv
|
* - Verbatim skill installation from skill-manifest.csv
|
||||||
* - Legacy directory cleanup and IDE-specific marker removal
|
* - IDE-specific marker removal (copilot-instructions, kilo modes, rovodev prompts)
|
||||||
*/
|
*/
|
||||||
class ConfigDrivenIdeSetup {
|
class ConfigDrivenIdeSetup {
|
||||||
constructor(platformCode, platformConfig) {
|
constructor(platformCode, platformConfig) {
|
||||||
|
|
@ -44,16 +44,20 @@ class ConfigDrivenIdeSetup {
|
||||||
async detect(projectDir) {
|
async detect(projectDir) {
|
||||||
if (!this.configDir) return false;
|
if (!this.configDir) return false;
|
||||||
|
|
||||||
const dir = path.join(projectDir || process.cwd(), this.configDir);
|
const root = projectDir || process.cwd();
|
||||||
if (await fs.pathExists(dir)) {
|
const dir = path.join(root, this.configDir);
|
||||||
|
if (!(await fs.pathExists(dir))) return false;
|
||||||
|
|
||||||
|
let entries;
|
||||||
try {
|
try {
|
||||||
const entries = await fs.readdir(dir);
|
entries = await fs.readdir(dir);
|
||||||
return entries.some((e) => typeof e === 'string' && e.startsWith('bmad'));
|
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return false;
|
const bmadDir = await this._findBmadDir(root);
|
||||||
|
const canonicalIds = await getInstalledCanonicalIds(bmadDir);
|
||||||
|
return entries.some((e) => isBmadOwnedEntry(e, canonicalIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -92,6 +96,12 @@ class ConfigDrivenIdeSetup {
|
||||||
return { success: false, reason: 'no-config' };
|
return { success: false, reason: 'no-config' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When a peer platform in the same install batch owns this target_dir,
|
||||||
|
// skip the skill write — the peer has already populated it.
|
||||||
|
if (options.skipTarget) {
|
||||||
|
return { success: true, results: { skills: 0, sharedTargetHandledByPeer: true } };
|
||||||
|
}
|
||||||
|
|
||||||
if (this.installerConfig.target_dir) {
|
if (this.installerConfig.target_dir) {
|
||||||
return this.installToTarget(projectDir, bmadDir, this.installerConfig, options);
|
return this.installToTarget(projectDir, bmadDir, this.installerConfig, options);
|
||||||
}
|
}
|
||||||
|
|
@ -222,27 +232,6 @@ class ConfigDrivenIdeSetup {
|
||||||
removalSet = new Set();
|
removalSet = new Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate legacy target directories (e.g. .opencode/agent → .opencode/agents)
|
|
||||||
// Legacy dirs are abandoned entirely, so use prefix matching (null removalSet)
|
|
||||||
if (this.installerConfig?.legacy_targets) {
|
|
||||||
const legacyDirsExist = await Promise.all(
|
|
||||||
this.installerConfig.legacy_targets.map((d) =>
|
|
||||||
this.isGlobalPath(d) ? fs.pathExists(d.replace(/^~/, os.homedir())) : fs.pathExists(path.join(projectDir, d)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (legacyDirsExist.some(Boolean)) {
|
|
||||||
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, null);
|
|
||||||
await this.removeEmptyParents(projectDir, legacyDir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strip BMAD markers from copilot-instructions.md if present
|
// Strip BMAD markers from copilot-instructions.md if present
|
||||||
if (this.name === 'github-copilot') {
|
if (this.name === 'github-copilot') {
|
||||||
await this.cleanupCopilotInstructions(projectDir, options);
|
await this.cleanupCopilotInstructions(projectDir, options);
|
||||||
|
|
@ -258,47 +247,17 @@ class ConfigDrivenIdeSetup {
|
||||||
await this.cleanupRovoDevPrompts(projectDir, options);
|
await this.cleanupRovoDevPrompts(projectDir, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip target_dir cleanup when a peer platform owns this directory
|
||||||
|
// (set during dedup'd install or when uninstalling one of several
|
||||||
|
// platforms that share the same target_dir).
|
||||||
|
if (options.skipTarget) return;
|
||||||
|
|
||||||
// Clean current target directory
|
// Clean current target directory
|
||||||
if (this.installerConfig?.target_dir) {
|
if (this.installerConfig?.target_dir) {
|
||||||
await this.cleanupTarget(projectDir, this.installerConfig.target_dir, options, removalSet);
|
await this.cleanupTarget(projectDir, this.installerConfig.target_dir, options, removalSet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the _bmad directory in a project
|
* Find the _bmad directory in a project
|
||||||
* @param {string} projectDir - Project directory
|
* @param {string} projectDir - Project directory
|
||||||
|
|
@ -426,8 +385,8 @@ class ConfigDrivenIdeSetup {
|
||||||
// Always preserve bmad-os-* utility skills regardless of cleanup mode
|
// Always preserve bmad-os-* utility skills regardless of cleanup mode
|
||||||
if (entry.startsWith('bmad-os-')) continue;
|
if (entry.startsWith('bmad-os-')) continue;
|
||||||
|
|
||||||
// Surgical removal from set, or legacy prefix matching when set is null
|
// Surgical removal from set, or fallback to manifest+prefix detection when null
|
||||||
const shouldRemove = removalSet ? removalSet.has(entry) : entry.startsWith('bmad');
|
const shouldRemove = removalSet ? removalSet.has(entry) : isBmadOwnedEntry(entry, null);
|
||||||
|
|
||||||
if (shouldRemove) {
|
if (shouldRemove) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -590,10 +549,9 @@ class ConfigDrivenIdeSetup {
|
||||||
try {
|
try {
|
||||||
if (await fs.pathExists(candidatePath)) {
|
if (await fs.pathExists(candidatePath)) {
|
||||||
const entries = await fs.readdir(candidatePath);
|
const entries = await fs.readdir(candidatePath);
|
||||||
const hasBmad = entries.some(
|
const ancestorBmadDir = await this._findBmadDir(current);
|
||||||
(e) => typeof e === 'string' && e.toLowerCase().startsWith('bmad') && !e.toLowerCase().startsWith('bmad-os-'),
|
const canonicalIds = await getInstalledCanonicalIds(ancestorBmadDir);
|
||||||
);
|
if (entries.some((e) => isBmadOwnedEntry(e, canonicalIds))) {
|
||||||
if (hasBmad) {
|
|
||||||
return candidatePath;
|
return candidatePath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -605,43 +563,6 @@ class ConfigDrivenIdeSetup {
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Walk up ancestor directories from relativeDir toward projectDir, removing each if empty
|
|
||||||
* Stops at projectDir boundary — never removes projectDir itself
|
|
||||||
* @param {string} projectDir - Project root (boundary)
|
|
||||||
* @param {string} relativeDir - Relative directory to start from
|
|
||||||
*/
|
|
||||||
async removeEmptyParents(projectDir, relativeDir) {
|
|
||||||
const resolvedProject = path.resolve(projectDir);
|
|
||||||
let current = relativeDir;
|
|
||||||
let last = null;
|
|
||||||
while (current && current !== '.' && current !== last) {
|
|
||||||
last = current;
|
|
||||||
const fullPath = path.resolve(projectDir, current);
|
|
||||||
// Boundary guard: never traverse outside projectDir
|
|
||||||
if (!fullPath.startsWith(resolvedProject + path.sep) && fullPath !== resolvedProject) break;
|
|
||||||
try {
|
|
||||||
if (!(await fs.pathExists(fullPath))) {
|
|
||||||
// Dir already gone — advance current; last is reset at top of next iteration
|
|
||||||
current = path.dirname(current);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const remaining = await fs.readdir(fullPath);
|
|
||||||
if (remaining.length > 0) break;
|
|
||||||
await fs.rmdir(fullPath);
|
|
||||||
} catch (error) {
|
|
||||||
// ENOTEMPTY: TOCTOU race (file added between readdir and rmdir) — skip level, continue upward
|
|
||||||
// ENOENT: dir removed by another process between pathExists and rmdir — skip level, continue upward
|
|
||||||
if (error.code === 'ENOTEMPTY' || error.code === 'ENOENT') {
|
|
||||||
current = path.dirname(current);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
break; // fatal error (e.g. EACCES) — stop upward walk
|
|
||||||
}
|
|
||||||
current = path.dirname(current);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { ConfigDrivenIdeSetup };
|
module.exports = { ConfigDrivenIdeSetup };
|
||||||
|
|
|
||||||
|
|
@ -160,8 +160,18 @@ class IdeManager {
|
||||||
let detail = '';
|
let detail = '';
|
||||||
if (handlerResult && handlerResult.results) {
|
if (handlerResult && handlerResult.results) {
|
||||||
const r = handlerResult.results;
|
const r = handlerResult.results;
|
||||||
const count = r.skillDirectories || r.skills || 0;
|
let count = r.skillDirectories || r.skills || 0;
|
||||||
if (count > 0) detail = `${count} skills`;
|
// Dedup'd platform: report the count its peer wrote so the user sees
|
||||||
|
// a consistent picture across all platforms sharing the dir.
|
||||||
|
if (count === 0 && r.sharedTargetHandledByPeer && options.sharedSkillCount) {
|
||||||
|
count = options.sharedSkillCount;
|
||||||
|
}
|
||||||
|
const targetDir = handler.installerConfig?.target_dir || null;
|
||||||
|
if (count > 0 && targetDir) {
|
||||||
|
detail = `${count} skills → ${targetDir}`;
|
||||||
|
} else if (count > 0) {
|
||||||
|
detail = `${count} skills`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 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;
|
||||||
|
|
@ -172,6 +182,57 @@ class IdeManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run setup for multiple IDEs as a single batch.
|
||||||
|
* Dedupes work when several selected platforms share the same target_dir:
|
||||||
|
* the first platform owns the directory write, peers skip it.
|
||||||
|
* @param {Array<string>} ideList - IDE names to set up
|
||||||
|
* @param {string} projectDir
|
||||||
|
* @param {string} bmadDir
|
||||||
|
* @param {Object} [options] - Forwarded to each handler.setup
|
||||||
|
* @returns {Promise<Array>} Per-IDE results
|
||||||
|
*/
|
||||||
|
async setupBatch(ideList, projectDir, bmadDir, options = {}) {
|
||||||
|
await this.ensureInitialized();
|
||||||
|
const results = [];
|
||||||
|
// target_dir → { firstIde, skillCount } from the platform that actually wrote it
|
||||||
|
const claimedTargets = new Map();
|
||||||
|
|
||||||
|
for (const ideName of ideList) {
|
||||||
|
const handler = this.handlers.get(ideName.toLowerCase());
|
||||||
|
if (!handler) {
|
||||||
|
results.push(await this.setup(ideName, projectDir, bmadDir, options));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = handler.installerConfig?.target_dir || null;
|
||||||
|
const claim = target ? claimedTargets.get(target) : null;
|
||||||
|
const skipTarget = !!claim;
|
||||||
|
|
||||||
|
const result = await this.setup(ideName, projectDir, bmadDir, {
|
||||||
|
...options,
|
||||||
|
skipTarget,
|
||||||
|
sharedWith: claim?.firstIde || null,
|
||||||
|
sharedTarget: target,
|
||||||
|
sharedSkillCount: claim?.skillCount || 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (target && !claim) {
|
||||||
|
const writtenCount = result.handlerResult?.results?.skillDirectories || result.handlerResult?.results?.skills || 0;
|
||||||
|
// Only claim the target when the install actually succeeded and wrote skills.
|
||||||
|
// If the first platform fails (ancestor conflict, exception, etc.), leave the
|
||||||
|
// dir unclaimed so the next peer becomes the new first writer instead of
|
||||||
|
// silently skipping into a broken/empty target_dir.
|
||||||
|
if (result.success && writtenCount > 0) {
|
||||||
|
claimedTargets.set(target, { firstIde: ideName, skillCount: writtenCount });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results.push(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cleanup IDE configurations
|
* Cleanup IDE configurations
|
||||||
* @param {string} projectDir - Project directory
|
* @param {string} projectDir - Project directory
|
||||||
|
|
@ -198,6 +259,8 @@ class IdeManager {
|
||||||
* @param {string} projectDir - Project directory
|
* @param {string} projectDir - Project directory
|
||||||
* @param {Array<string>} ideList - List of IDE names to clean up
|
* @param {Array<string>} ideList - List of IDE names to clean up
|
||||||
* @param {Object} [options] - Cleanup options passed through to handlers
|
* @param {Object} [options] - Cleanup options passed through to handlers
|
||||||
|
* options.remainingIdes - IDE names still installed after this cleanup; used
|
||||||
|
* to skip target_dir wipe when a co-installed platform shares the dir.
|
||||||
* @returns {Array} Results array
|
* @returns {Array} Results array
|
||||||
*/
|
*/
|
||||||
async cleanupByList(projectDir, ideList, options = {}) {
|
async cleanupByList(projectDir, ideList, options = {}) {
|
||||||
|
|
@ -211,13 +274,27 @@ class IdeManager {
|
||||||
// Build lowercase lookup for case-insensitive matching
|
// Build lowercase lookup for case-insensitive matching
|
||||||
const lowercaseHandlers = new Map([...this.handlers.entries()].map(([k, v]) => [k.toLowerCase(), v]));
|
const lowercaseHandlers = new Map([...this.handlers.entries()].map(([k, v]) => [k.toLowerCase(), v]));
|
||||||
|
|
||||||
|
// Resolve target_dirs for IDEs that will remain installed after this cleanup
|
||||||
|
const remainingTargets = new Set();
|
||||||
|
if (Array.isArray(options.remainingIdes)) {
|
||||||
|
for (const remaining of options.remainingIdes) {
|
||||||
|
const h = lowercaseHandlers.get(String(remaining).toLowerCase());
|
||||||
|
const t = h?.installerConfig?.target_dir;
|
||||||
|
if (t) remainingTargets.add(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const ideName of ideList) {
|
for (const ideName of ideList) {
|
||||||
const handler = lowercaseHandlers.get(ideName.toLowerCase());
|
const handler = lowercaseHandlers.get(ideName.toLowerCase());
|
||||||
if (!handler) continue;
|
if (!handler) continue;
|
||||||
|
|
||||||
|
const target = handler.installerConfig?.target_dir || null;
|
||||||
|
const skipTarget = target && remainingTargets.has(target);
|
||||||
|
const cleanupOptions = skipTarget ? { ...options, skipTarget: true } : options;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await handler.cleanup(projectDir, options);
|
await handler.cleanup(projectDir, cleanupOptions);
|
||||||
results.push({ ide: ideName, success: true });
|
results.push({ ide: ideName, success: true, skippedTarget: !!skipTarget });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
results.push({ ide: ideName, success: false, error: error.message });
|
results.push({ ide: ideName, success: false, error: error.message });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,128 +5,203 @@
|
||||||
# preferred: Whether shown as a recommended option on install
|
# preferred: Whether shown as a recommended option on install
|
||||||
# suspended: (optional) Message explaining why install is blocked
|
# suspended: (optional) Message explaining why install is blocked
|
||||||
# installer:
|
# installer:
|
||||||
# target_dir: Directory where skill directories are installed
|
# target_dir: Directory where skill directories are installed (project/workspace)
|
||||||
# legacy_targets: (optional) Old target dirs to clean up on reinstall
|
# global_target_dir: (optional) User-home directory for global install
|
||||||
# ancestor_conflict_check: (optional) Refuse install when ancestor dir has BMAD files
|
# ancestor_conflict_check: (optional) Refuse install when ancestor dir has BMAD files
|
||||||
|
#
|
||||||
|
# Multiple platforms may share the same target_dir or global_target_dir — many tools
|
||||||
|
# read from the shared `.agents/skills/` and `~/.agents/skills/` cross-tool standard.
|
||||||
|
# Paths verified against each tool's primary docs as of 2026-04-25.
|
||||||
|
|
||||||
platforms:
|
platforms:
|
||||||
|
adal:
|
||||||
|
name: "AdaL"
|
||||||
|
preferred: false
|
||||||
|
installer:
|
||||||
|
target_dir: .adal/skills
|
||||||
|
global_target_dir: ~/.adal/skills
|
||||||
|
|
||||||
|
amp:
|
||||||
|
name: "Sourcegraph Amp"
|
||||||
|
preferred: false
|
||||||
|
installer:
|
||||||
|
target_dir: .agents/skills
|
||||||
|
global_target_dir: ~/.config/agents/skills
|
||||||
|
|
||||||
antigravity:
|
antigravity:
|
||||||
name: "Google Antigravity"
|
name: "Google Antigravity"
|
||||||
preferred: false
|
preferred: false
|
||||||
installer:
|
installer:
|
||||||
legacy_targets:
|
|
||||||
- .agent/workflows
|
|
||||||
target_dir: .agent/skills
|
target_dir: .agent/skills
|
||||||
|
global_target_dir: ~/.gemini/antigravity/skills
|
||||||
|
|
||||||
auggie:
|
auggie:
|
||||||
name: "Auggie"
|
name: "Auggie"
|
||||||
preferred: false
|
preferred: false
|
||||||
installer:
|
installer:
|
||||||
legacy_targets:
|
target_dir: .agents/skills
|
||||||
- .augment/commands
|
global_target_dir: ~/.agents/skills
|
||||||
target_dir: .augment/skills
|
|
||||||
|
bob:
|
||||||
|
name: "IBM Bob"
|
||||||
|
preferred: false
|
||||||
|
installer:
|
||||||
|
target_dir: .bob/skills
|
||||||
|
global_target_dir: ~/.bob/skills
|
||||||
|
|
||||||
claude-code:
|
claude-code:
|
||||||
name: "Claude Code"
|
name: "Claude Code"
|
||||||
preferred: true
|
preferred: true
|
||||||
installer:
|
installer:
|
||||||
legacy_targets:
|
|
||||||
- .claude/commands
|
|
||||||
target_dir: .claude/skills
|
target_dir: .claude/skills
|
||||||
|
global_target_dir: ~/.claude/skills
|
||||||
|
|
||||||
cline:
|
cline:
|
||||||
name: "Cline"
|
name: "Cline"
|
||||||
preferred: false
|
preferred: false
|
||||||
installer:
|
installer:
|
||||||
legacy_targets:
|
|
||||||
- .clinerules/workflows
|
|
||||||
target_dir: .cline/skills
|
target_dir: .cline/skills
|
||||||
|
global_target_dir: ~/.cline/skills
|
||||||
|
|
||||||
codex:
|
codex:
|
||||||
name: "Codex"
|
name: "Codex"
|
||||||
preferred: false
|
preferred: true
|
||||||
installer:
|
installer:
|
||||||
legacy_targets:
|
|
||||||
- .codex/prompts
|
|
||||||
- ~/.codex/prompts
|
|
||||||
target_dir: .agents/skills
|
target_dir: .agents/skills
|
||||||
|
global_target_dir: ~/.codex/skills
|
||||||
|
|
||||||
codebuddy:
|
codebuddy:
|
||||||
name: "CodeBuddy"
|
name: "CodeBuddy"
|
||||||
preferred: false
|
preferred: false
|
||||||
installer:
|
installer:
|
||||||
legacy_targets:
|
|
||||||
- .codebuddy/commands
|
|
||||||
target_dir: .codebuddy/skills
|
target_dir: .codebuddy/skills
|
||||||
|
global_target_dir: ~/.codebuddy/skills
|
||||||
|
|
||||||
|
command-code:
|
||||||
|
name: "Command Code"
|
||||||
|
preferred: false
|
||||||
|
installer:
|
||||||
|
target_dir: .agents/skills
|
||||||
|
global_target_dir: ~/.agents/skills
|
||||||
|
|
||||||
|
cortex:
|
||||||
|
name: "Snowflake Cortex Code"
|
||||||
|
preferred: false
|
||||||
|
installer:
|
||||||
|
target_dir: .cortex/skills
|
||||||
|
global_target_dir: ~/.snowflake/cortex/skills
|
||||||
|
|
||||||
crush:
|
crush:
|
||||||
name: "Crush"
|
name: "Crush"
|
||||||
preferred: false
|
preferred: false
|
||||||
installer:
|
installer:
|
||||||
legacy_targets:
|
target_dir: .agents/skills
|
||||||
- .crush/commands
|
global_target_dir: ~/.config/agents/skills
|
||||||
target_dir: .crush/skills
|
|
||||||
|
|
||||||
cursor:
|
cursor:
|
||||||
name: "Cursor"
|
name: "Cursor"
|
||||||
preferred: true
|
preferred: true
|
||||||
installer:
|
installer:
|
||||||
legacy_targets:
|
target_dir: .agents/skills
|
||||||
- .cursor/commands
|
global_target_dir: ~/.agents/skills
|
||||||
target_dir: .cursor/skills
|
|
||||||
|
droid:
|
||||||
|
name: "Factory Droid"
|
||||||
|
preferred: false
|
||||||
|
installer:
|
||||||
|
target_dir: .factory/skills
|
||||||
|
global_target_dir: ~/.factory/skills
|
||||||
|
|
||||||
|
firebender:
|
||||||
|
name: "Firebender"
|
||||||
|
preferred: false
|
||||||
|
installer:
|
||||||
|
target_dir: .firebender/skills
|
||||||
|
global_target_dir: ~/.agents/skills
|
||||||
|
|
||||||
gemini:
|
gemini:
|
||||||
name: "Gemini CLI"
|
name: "Gemini CLI"
|
||||||
preferred: false
|
preferred: false
|
||||||
installer:
|
installer:
|
||||||
legacy_targets:
|
target_dir: .agents/skills
|
||||||
- .gemini/commands
|
global_target_dir: ~/.agents/skills
|
||||||
target_dir: .gemini/skills
|
|
||||||
|
|
||||||
github-copilot:
|
github-copilot:
|
||||||
name: "GitHub Copilot"
|
name: "GitHub Copilot"
|
||||||
|
preferred: true
|
||||||
|
installer:
|
||||||
|
target_dir: .agents/skills
|
||||||
|
global_target_dir: ~/.agents/skills
|
||||||
|
|
||||||
|
goose:
|
||||||
|
name: "Block Goose"
|
||||||
preferred: false
|
preferred: false
|
||||||
installer:
|
installer:
|
||||||
legacy_targets:
|
target_dir: .agents/skills
|
||||||
- .github/agents
|
global_target_dir: ~/.config/agents/skills
|
||||||
- .github/prompts
|
|
||||||
target_dir: .github/skills
|
|
||||||
|
|
||||||
iflow:
|
iflow:
|
||||||
name: "iFlow"
|
name: "iFlow"
|
||||||
preferred: false
|
preferred: false
|
||||||
installer:
|
installer:
|
||||||
legacy_targets:
|
|
||||||
- .iflow/commands
|
|
||||||
target_dir: .iflow/skills
|
target_dir: .iflow/skills
|
||||||
|
global_target_dir: ~/.iflow/skills
|
||||||
|
|
||||||
junie:
|
junie:
|
||||||
name: "Junie"
|
name: "Junie"
|
||||||
preferred: false
|
preferred: false
|
||||||
installer:
|
installer:
|
||||||
target_dir: .agents/skills
|
target_dir: .junie/skills
|
||||||
|
global_target_dir: ~/.junie/skills
|
||||||
|
|
||||||
kilo:
|
kilo:
|
||||||
name: "KiloCoder"
|
name: "KiloCoder"
|
||||||
preferred: false
|
preferred: false
|
||||||
installer:
|
installer:
|
||||||
legacy_targets:
|
target_dir: .agents/skills
|
||||||
- .kilocode/workflows
|
global_target_dir: ~/.kilocode/skills
|
||||||
target_dir: .kilocode/skills
|
|
||||||
|
|
||||||
kimi-code:
|
kimi-code:
|
||||||
name: "Kimi Code"
|
name: "Kimi Code"
|
||||||
preferred: false
|
preferred: false
|
||||||
installer:
|
installer:
|
||||||
target_dir: .kimi/skills
|
target_dir: .agents/skills
|
||||||
|
global_target_dir: ~/.agents/skills
|
||||||
|
|
||||||
kiro:
|
kiro:
|
||||||
name: "Kiro"
|
name: "Kiro"
|
||||||
preferred: false
|
preferred: false
|
||||||
installer:
|
installer:
|
||||||
legacy_targets:
|
|
||||||
- .kiro/steering
|
|
||||||
target_dir: .kiro/skills
|
target_dir: .kiro/skills
|
||||||
|
global_target_dir: ~/.kiro/skills
|
||||||
|
|
||||||
|
kode:
|
||||||
|
name: "Kode"
|
||||||
|
preferred: false
|
||||||
|
installer:
|
||||||
|
target_dir: .kode/skills
|
||||||
|
global_target_dir: ~/.kode/skills
|
||||||
|
|
||||||
|
mistral-vibe:
|
||||||
|
name: "Mistral Vibe"
|
||||||
|
preferred: false
|
||||||
|
installer:
|
||||||
|
target_dir: .agents/skills
|
||||||
|
global_target_dir: ~/.vibe/skills
|
||||||
|
|
||||||
|
mux:
|
||||||
|
name: "Mux"
|
||||||
|
preferred: false
|
||||||
|
installer:
|
||||||
|
target_dir: .agents/skills
|
||||||
|
global_target_dir: ~/.agents/skills
|
||||||
|
|
||||||
|
neovate:
|
||||||
|
name: "Neovate"
|
||||||
|
preferred: false
|
||||||
|
installer:
|
||||||
|
target_dir: .neovate/skills
|
||||||
|
global_target_dir: ~/.neovate/skills
|
||||||
|
|
||||||
ona:
|
ona:
|
||||||
name: "Ona"
|
name: "Ona"
|
||||||
|
|
@ -134,65 +209,98 @@ platforms:
|
||||||
installer:
|
installer:
|
||||||
target_dir: .ona/skills
|
target_dir: .ona/skills
|
||||||
|
|
||||||
|
openclaw:
|
||||||
|
name: "OpenClaw"
|
||||||
|
preferred: false
|
||||||
|
installer:
|
||||||
|
target_dir: .agents/skills
|
||||||
|
global_target_dir: ~/.agents/skills
|
||||||
|
|
||||||
opencode:
|
opencode:
|
||||||
name: "OpenCode"
|
name: "OpenCode"
|
||||||
preferred: false
|
preferred: false
|
||||||
installer:
|
installer:
|
||||||
legacy_targets:
|
target_dir: .agents/skills
|
||||||
- .opencode/agents
|
global_target_dir: ~/.agents/skills
|
||||||
- .opencode/commands
|
|
||||||
- .opencode/agent
|
openhands:
|
||||||
- .opencode/command
|
name: "OpenHands"
|
||||||
target_dir: .opencode/skills
|
preferred: false
|
||||||
|
installer:
|
||||||
|
target_dir: .agents/skills
|
||||||
|
global_target_dir: ~/.agents/skills
|
||||||
|
|
||||||
pi:
|
pi:
|
||||||
name: "Pi"
|
name: "Pi"
|
||||||
preferred: false
|
preferred: false
|
||||||
installer:
|
installer:
|
||||||
target_dir: .pi/skills
|
target_dir: .agents/skills
|
||||||
|
global_target_dir: ~/.agents/skills
|
||||||
|
|
||||||
|
pochi:
|
||||||
|
name: "Pochi"
|
||||||
|
preferred: false
|
||||||
|
installer:
|
||||||
|
target_dir: .agents/skills
|
||||||
|
global_target_dir: ~/.agents/skills
|
||||||
|
|
||||||
qoder:
|
qoder:
|
||||||
name: "Qoder"
|
name: "Qoder"
|
||||||
preferred: false
|
preferred: false
|
||||||
installer:
|
installer:
|
||||||
target_dir: .qoder/skills
|
target_dir: .qoder/skills
|
||||||
|
global_target_dir: ~/.qoder/skills
|
||||||
|
|
||||||
qwen:
|
qwen:
|
||||||
name: "QwenCoder"
|
name: "QwenCoder"
|
||||||
preferred: false
|
preferred: false
|
||||||
installer:
|
installer:
|
||||||
legacy_targets:
|
|
||||||
- .qwen/commands
|
|
||||||
target_dir: .qwen/skills
|
target_dir: .qwen/skills
|
||||||
|
global_target_dir: ~/.qwen/skills
|
||||||
|
|
||||||
|
replit:
|
||||||
|
name: "Replit Agent"
|
||||||
|
preferred: false
|
||||||
|
installer:
|
||||||
|
target_dir: .agents/skills
|
||||||
|
|
||||||
roo:
|
roo:
|
||||||
name: "Roo Code"
|
name: "Roo Code"
|
||||||
preferred: false
|
preferred: false
|
||||||
installer:
|
installer:
|
||||||
legacy_targets:
|
target_dir: .agents/skills
|
||||||
- .roo/commands
|
global_target_dir: ~/.agents/skills
|
||||||
target_dir: .roo/skills
|
|
||||||
|
|
||||||
rovo-dev:
|
rovo-dev:
|
||||||
name: "Rovo Dev"
|
name: "Rovo Dev"
|
||||||
preferred: false
|
preferred: false
|
||||||
installer:
|
installer:
|
||||||
legacy_targets:
|
target_dir: .agents/skills
|
||||||
- .rovodev/workflows
|
global_target_dir: ~/.agents/skills
|
||||||
target_dir: .rovodev/skills
|
|
||||||
|
|
||||||
trae:
|
trae:
|
||||||
name: "Trae"
|
name: "Trae"
|
||||||
preferred: false
|
preferred: false
|
||||||
installer:
|
installer:
|
||||||
legacy_targets:
|
|
||||||
- .trae/rules
|
|
||||||
target_dir: .trae/skills
|
target_dir: .trae/skills
|
||||||
|
|
||||||
|
warp:
|
||||||
|
name: "Warp"
|
||||||
|
preferred: false
|
||||||
|
installer:
|
||||||
|
target_dir: .agents/skills
|
||||||
|
global_target_dir: ~/.agents/skills
|
||||||
|
|
||||||
windsurf:
|
windsurf:
|
||||||
name: "Windsurf"
|
name: "Windsurf"
|
||||||
preferred: false
|
preferred: false
|
||||||
installer:
|
installer:
|
||||||
legacy_targets:
|
target_dir: .agents/skills
|
||||||
- .windsurf/workflows
|
global_target_dir: ~/.agents/skills
|
||||||
target_dir: .windsurf/skills
|
|
||||||
|
zencoder:
|
||||||
|
name: "Zencoder"
|
||||||
|
preferred: false
|
||||||
|
installer:
|
||||||
|
target_dir: .zencoder/skills
|
||||||
|
global_target_dir: ~/.zencoder/skills
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
const path = require('node:path');
|
||||||
|
const fs = require('../../fs-native');
|
||||||
|
const csv = require('csv-parse/sync');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the global skill-manifest.csv and return the set of canonicalIds.
|
||||||
|
* These define which directory entries in a target_dir are BMAD-owned, regardless
|
||||||
|
* of whether they happen to start with "bmad-" (custom modules can ship skills
|
||||||
|
* with any prefix, e.g. "fred-cool-skill").
|
||||||
|
*
|
||||||
|
* @param {string} bmadDir - Path to the _bmad install directory
|
||||||
|
* @returns {Promise<Set<string>>} Set of canonicalIds, or empty set if manifest missing
|
||||||
|
*/
|
||||||
|
async function getInstalledCanonicalIds(bmadDir) {
|
||||||
|
const ids = new Set();
|
||||||
|
if (!bmadDir) return ids;
|
||||||
|
|
||||||
|
const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv');
|
||||||
|
if (!(await fs.pathExists(csvPath))) return ids;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = await fs.readFile(csvPath, 'utf8');
|
||||||
|
const records = csv.parse(content, { columns: true, skip_empty_lines: true });
|
||||||
|
for (const record of records) {
|
||||||
|
if (record.canonicalId) ids.add(record.canonicalId);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Unreadable/invalid manifest — treat as no info
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test whether a directory entry is BMAD-owned.
|
||||||
|
* Prefers the manifest's canonicalIds; falls back to the legacy "bmad" prefix
|
||||||
|
* when no manifest is available (early install, ancestor lookup with no bmad dir).
|
||||||
|
*
|
||||||
|
* @param {string} entry - Directory entry name
|
||||||
|
* @param {Set<string>|null} canonicalIds - From getInstalledCanonicalIds, or null
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isBmadOwnedEntry(entry, canonicalIds) {
|
||||||
|
if (!entry || typeof entry !== 'string') return false;
|
||||||
|
if (entry.toLowerCase().startsWith('bmad-os-')) return false;
|
||||||
|
if (canonicalIds && canonicalIds.size > 0) return canonicalIds.has(entry);
|
||||||
|
return entry.toLowerCase().startsWith('bmad');
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { getInstalledCanonicalIds, isBmadOwnedEntry };
|
||||||
|
|
@ -1,175 +0,0 @@
|
||||||
# BMAD Platform Codes Configuration
|
|
||||||
# Central configuration for all platform/IDE codes used in the BMAD system
|
|
||||||
#
|
|
||||||
# This file defines the standardized platform codes that are used throughout
|
|
||||||
# the installation system to identify different platforms (IDEs, tools, etc.)
|
|
||||||
#
|
|
||||||
# Format:
|
|
||||||
# code: Platform identifier used internally
|
|
||||||
# name: Display name shown to users
|
|
||||||
# preferred: Whether this platform is shown as a recommended option on install
|
|
||||||
# category: Type of platform (ide, tool, service, etc.)
|
|
||||||
|
|
||||||
platforms:
|
|
||||||
# Recommended Platforms
|
|
||||||
claude-code:
|
|
||||||
name: "Claude Code"
|
|
||||||
preferred: true
|
|
||||||
category: cli
|
|
||||||
description: "Anthropic's official CLI for Claude"
|
|
||||||
|
|
||||||
cursor:
|
|
||||||
name: "Cursor"
|
|
||||||
preferred: true
|
|
||||||
category: ide
|
|
||||||
description: "AI-first code editor"
|
|
||||||
|
|
||||||
# Other IDEs and Tools
|
|
||||||
cline:
|
|
||||||
name: "Cline"
|
|
||||||
preferred: false
|
|
||||||
category: ide
|
|
||||||
description: "AI coding assistant"
|
|
||||||
|
|
||||||
opencode:
|
|
||||||
name: "OpenCode"
|
|
||||||
preferred: false
|
|
||||||
category: ide
|
|
||||||
description: "OpenCode terminal coding assistant"
|
|
||||||
|
|
||||||
codebuddy:
|
|
||||||
name: "CodeBuddy"
|
|
||||||
preferred: false
|
|
||||||
category: ide
|
|
||||||
description: "Tencent Cloud Code Assistant - AI-powered coding companion"
|
|
||||||
|
|
||||||
auggie:
|
|
||||||
name: "Auggie"
|
|
||||||
preferred: false
|
|
||||||
category: cli
|
|
||||||
description: "AI development tool"
|
|
||||||
|
|
||||||
roo:
|
|
||||||
name: "Roo Code"
|
|
||||||
preferred: false
|
|
||||||
category: ide
|
|
||||||
description: "Enhanced Cline fork"
|
|
||||||
|
|
||||||
rovo-dev:
|
|
||||||
name: "Rovo Dev"
|
|
||||||
preferred: false
|
|
||||||
category: ide
|
|
||||||
description: "Atlassian's Rovo development environment"
|
|
||||||
|
|
||||||
kiro:
|
|
||||||
name: "Kiro"
|
|
||||||
preferred: false
|
|
||||||
category: ide
|
|
||||||
description: "Amazon's AI-powered IDE"
|
|
||||||
|
|
||||||
github-copilot:
|
|
||||||
name: "GitHub Copilot"
|
|
||||||
preferred: false
|
|
||||||
category: ide
|
|
||||||
description: "GitHub's AI pair programmer"
|
|
||||||
|
|
||||||
codex:
|
|
||||||
name: "Codex"
|
|
||||||
preferred: false
|
|
||||||
category: cli
|
|
||||||
description: "OpenAI Codex integration"
|
|
||||||
|
|
||||||
qwen:
|
|
||||||
name: "QwenCoder"
|
|
||||||
preferred: false
|
|
||||||
category: ide
|
|
||||||
description: "Qwen AI coding assistant"
|
|
||||||
|
|
||||||
gemini:
|
|
||||||
name: "Gemini CLI"
|
|
||||||
preferred: false
|
|
||||||
category: cli
|
|
||||||
description: "Google's CLI for Gemini"
|
|
||||||
|
|
||||||
iflow:
|
|
||||||
name: "iFlow"
|
|
||||||
preferred: false
|
|
||||||
category: ide
|
|
||||||
description: "AI workflow automation"
|
|
||||||
|
|
||||||
kilo:
|
|
||||||
name: "KiloCoder"
|
|
||||||
preferred: false
|
|
||||||
category: ide
|
|
||||||
description: "AI coding platform"
|
|
||||||
|
|
||||||
kimi-code:
|
|
||||||
name: "Kimi Code"
|
|
||||||
preferred: false
|
|
||||||
category: cli
|
|
||||||
description: "Moonshot AI's Kimi Code CLI"
|
|
||||||
|
|
||||||
crush:
|
|
||||||
name: "Crush"
|
|
||||||
preferred: false
|
|
||||||
category: ide
|
|
||||||
description: "AI development assistant"
|
|
||||||
|
|
||||||
antigravity:
|
|
||||||
name: "Google Antigravity"
|
|
||||||
preferred: false
|
|
||||||
category: ide
|
|
||||||
description: "Google's AI development environment"
|
|
||||||
|
|
||||||
trae:
|
|
||||||
name: "Trae"
|
|
||||||
preferred: false
|
|
||||||
category: ide
|
|
||||||
description: "AI coding tool"
|
|
||||||
|
|
||||||
windsurf:
|
|
||||||
name: "Windsurf"
|
|
||||||
preferred: false
|
|
||||||
category: ide
|
|
||||||
description: "AI-powered IDE with cascade flows"
|
|
||||||
|
|
||||||
junie:
|
|
||||||
name: "Junie"
|
|
||||||
preferred: false
|
|
||||||
category: cli
|
|
||||||
description: "AI coding agent by JetBrains"
|
|
||||||
|
|
||||||
ona:
|
|
||||||
name: "Ona"
|
|
||||||
preferred: false
|
|
||||||
category: ide
|
|
||||||
description: "Ona AI development environment"
|
|
||||||
|
|
||||||
# Platform categories
|
|
||||||
categories:
|
|
||||||
ide:
|
|
||||||
name: "Integrated Development Environment"
|
|
||||||
description: "Full-featured code editors with AI assistance"
|
|
||||||
|
|
||||||
cli:
|
|
||||||
name: "Command Line Interface"
|
|
||||||
description: "Terminal-based tools"
|
|
||||||
|
|
||||||
tool:
|
|
||||||
name: "Development Tool"
|
|
||||||
description: "Standalone development utilities"
|
|
||||||
|
|
||||||
service:
|
|
||||||
name: "Cloud Service"
|
|
||||||
description: "Cloud-based development platforms"
|
|
||||||
|
|
||||||
extension:
|
|
||||||
name: "Editor Extension"
|
|
||||||
description: "Plugins for existing editors"
|
|
||||||
|
|
||||||
# Naming conventions and rules
|
|
||||||
conventions:
|
|
||||||
code_format: "lowercase-kebab-case"
|
|
||||||
name_format: "Title Case"
|
|
||||||
max_code_length: 20
|
|
||||||
allowed_characters: "a-z0-9-"
|
|
||||||
Loading…
Reference in New Issue