Skip to content

Commit 01cc325

Browse files
authored
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.
1 parent 1197122 commit 01cc325

10 files changed

Lines changed: 685 additions & 618 deletions

docs/cs/how-to/non-interactive-installation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ Dostupná ID nástrojů pro příznak `--tools`:
6060

6161
**Preferované:** `claude-code`, `cursor`
6262

63-
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).
63+
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).
6464

6565
## Režimy instalace
6666

test/test-installation-components.js

Lines changed: 176 additions & 250 deletions
Large diffs are not rendered by default.

tools/docs/native-skills-migration-checklist.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,6 @@ Support assumption: full Agent Skills support. Gemini CLI docs confirm workspace
222222

223223
- [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/))
224224
- [x] Implement native skills output — target_dir `.gemini/skills`, skill_format true, template_type default (replaces TOML templates)
225-
- [x] Add legacy cleanup for `.gemini/commands` (via `legacy_targets`)
226225
- [x] Test fresh install — skills written to `.gemini/skills/bmad-master/SKILL.md` with correct frontmatter
227226
- [x] Test reinstall/upgrade from legacy TOML command output — legacy dir removed, skills installed
228227
- [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
236235

237236
- [x] Confirm iFlow native skills path is `.iflow/skills/{skill-name}/SKILL.md`
238237
- [x] Implement native skills output — target_dir `.iflow/skills`, skill_format true, template_type default
239-
- [x] Add legacy cleanup for `.iflow/commands` (via `legacy_targets`)
240238
- [x] Test fresh install — skills written to `.iflow/skills/bmad-master/SKILL.md`
241239
- [x] Test legacy cleanup — legacy commands dir removed
242240
- [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
249247

250248
- [x] Confirm QwenCoder native skills path is `.qwen/skills/{skill-name}/SKILL.md`
251249
- [x] Implement native skills output — target_dir `.qwen/skills`, skill_format true, template_type default
252-
- [x] Add legacy cleanup for `.qwen/commands` (via `legacy_targets`)
253250
- [x] Test fresh install — skills written to `.qwen/skills/bmad-master/SKILL.md`
254251
- [x] Test legacy cleanup — legacy commands dir removed
255252
- [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
262259

263260
- [x] Confirm Rovo Dev native skills path is `.rovodev/skills/{skill-name}/SKILL.md` (per Atlassian blog)
264261
- [x] Replace 257-line custom `rovodev.js` with config-driven entry in `platform-codes.yaml`
265-
- [x] Add legacy cleanup for `.rovodev/workflows` (via `legacy_targets`) and BMAD entries in `prompts.yml` (via `cleanupRovoDevPrompts()` in `_config-driven.js`)
266262
- [x] Test fresh install — skills written to `.rovodev/skills/bmad-master/SKILL.md`
267263
- [x] Test legacy cleanup — legacy workflows dir removed, `prompts.yml` BMAD entries stripped while preserving user entries
268264
- [x] Implement/extend automated tests — 8 assertions in test suite 26

tools/installer/core/installer.js

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const { ExternalModuleManager } = require('../modules/external-manager');
1414
const { resolveModuleVersion } = require('../modules/version-resolver');
1515

1616
const { ExistingInstall } = require('./existing-install');
17+
const { warnPreNativeSkillsLegacy } = require('./legacy-warnings');
1718

1819
class Installer {
1920
constructor() {
@@ -41,6 +42,16 @@ class Installer {
4142
const officialModules = await OfficialModules.build(config, paths);
4243
const existingInstall = await ExistingInstall.detect(paths.bmadDir);
4344

45+
try {
46+
await warnPreNativeSkillsLegacy({
47+
projectRoot: paths.projectRoot,
48+
existingVersion: existingInstall.installed ? existingInstall.version : null,
49+
});
50+
} catch (error) {
51+
// Legacy-dir scan is informational; never let it abort install.
52+
await prompts.log.warn(`Warning: Could not check for legacy BMAD entries: ${error.message}`);
53+
}
54+
4455
if (existingInstall.installed) {
4556
await this._removeDeselectedModules(existingInstall, config, paths);
4657
updateState = await this._prepareUpdateState(paths, config, existingInstall, officialModules);
@@ -183,15 +194,16 @@ class Installer {
183194

184195
if (toRemove.length === 0) return;
185196

186-
await this.ideManager.ensureInitialized();
187-
for (const ide of toRemove) {
188-
try {
189-
const handler = this.ideManager.handlers.get(ide);
190-
if (handler) {
191-
await handler.cleanup(paths.projectRoot);
192-
}
193-
} catch (error) {
194-
await prompts.log.warn(`Warning: Failed to remove ${ide}: ${error.message}`);
197+
// Pass the newly-selected list as remainingIdes so cleanupByList skips
198+
// target_dir wipes for IDEs whose directory is still owned by a peer
199+
// (e.g. removing 'cursor' while 'gemini' remains — both share .agents/skills).
200+
const results = await this.ideManager.cleanupByList(paths.projectRoot, toRemove, {
201+
remainingIdes: [...newlySelected],
202+
});
203+
204+
for (const result of results || []) {
205+
if (result && result.success === false) {
206+
await prompts.log.warn(`Warning: Failed to remove ${result.ide}: ${result.error || 'unknown error'}`);
195207
}
196208
}
197209
}
@@ -342,13 +354,14 @@ class Installer {
342354
return;
343355
}
344356

345-
for (const ide of validIdes) {
346-
const setupResult = await this.ideManager.setup(ide, paths.projectRoot, paths.bmadDir, {
347-
selectedModules: allModules || [],
348-
verbose: config.verbose,
349-
previousSkillIds,
350-
});
357+
const setupResults = await this.ideManager.setupBatch(validIdes, paths.projectRoot, paths.bmadDir, {
358+
selectedModules: allModules || [],
359+
verbose: config.verbose,
360+
previousSkillIds,
361+
});
351362

363+
for (const setupResult of setupResults) {
364+
const ide = setupResult.ide;
352365
if (setupResult.success) {
353366
addResult(ide, 'ok', setupResult.detail || '');
354367
} else {
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
const os = require('node:os');
2+
const path = require('node:path');
3+
const semver = require('semver');
4+
const fs = require('../fs-native');
5+
const prompts = require('../prompts');
6+
const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');
7+
const { getInstalledCanonicalIds, isBmadOwnedEntry } = require('../ide/shared/installed-skills');
8+
9+
const MIN_NATIVE_SKILLS_VERSION = '6.1.0';
10+
11+
// Pre-v6.1.0 paths: BMAD used to install commands/workflows/etc in tool-specific dirs.
12+
// In v6.1.0 BMAD switched to native SKILL.md format.
13+
const LEGACY_COMMAND_PATHS = [
14+
'.agent/workflows',
15+
'.augment/commands',
16+
'.claude/commands',
17+
'.clinerules/workflows',
18+
'.codex/prompts',
19+
'~/.codex/prompts',
20+
'.codebuddy/commands',
21+
'.crush/commands',
22+
'.cursor/commands',
23+
'.gemini/commands',
24+
'.github/agents',
25+
'.github/prompts',
26+
'.iflow/commands',
27+
'.kilocode/workflows',
28+
'.kiro/steering',
29+
'.opencode/agents',
30+
'.opencode/commands',
31+
'.opencode/agent',
32+
'.opencode/command',
33+
'.qwen/commands',
34+
'.roo/commands',
35+
'.rovodev/workflows',
36+
'.trae/rules',
37+
'.windsurf/workflows',
38+
];
39+
40+
// Skill paths that moved to the cross-tool .agents/skills/ standard.
41+
// Users upgrading from a prior install may have stale BMAD skills here that
42+
// the AI tool will load alongside the new ones, causing duplicates.
43+
const LEGACY_SKILL_PATHS = [
44+
'.augment/skills',
45+
'~/.augment/skills',
46+
'.codex/skills',
47+
'.crush/skills',
48+
'.cursor/skills',
49+
'~/.cursor/skills',
50+
'.gemini/skills',
51+
'~/.gemini/skills',
52+
'.github/skills',
53+
'~/.github/skills',
54+
'.kilocode/skills',
55+
'.kimi/skills',
56+
'~/.kimi/skills',
57+
'.opencode/skills',
58+
'~/.opencode/skills',
59+
'.pi/skills',
60+
'~/.pi/skills',
61+
'.roo/skills',
62+
'~/.roo/skills',
63+
'.rovodev/skills',
64+
'~/.rovodev/skills',
65+
'.windsurf/skills',
66+
'~/.windsurf/skills',
67+
'~/.codeium/windsurf/skills',
68+
];
69+
70+
const LEGACY_PATHS = [...LEGACY_COMMAND_PATHS, ...LEGACY_SKILL_PATHS];
71+
72+
function expandPath(p) {
73+
if (p === '~') return os.homedir();
74+
if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2));
75+
return p;
76+
}
77+
78+
function resolveLegacyPath(projectRoot, p) {
79+
if (path.isAbsolute(p) || p.startsWith('~')) return expandPath(p);
80+
return path.join(projectRoot, p);
81+
}
82+
83+
async function findStaleLegacyDirs(projectRoot) {
84+
const bmadDir = path.join(projectRoot, BMAD_FOLDER_NAME);
85+
const canonicalIds = await getInstalledCanonicalIds(bmadDir);
86+
87+
const findings = [];
88+
for (const legacyPath of LEGACY_PATHS) {
89+
const resolved = resolveLegacyPath(projectRoot, legacyPath);
90+
if (!(await fs.pathExists(resolved))) continue;
91+
try {
92+
const entries = await fs.readdir(resolved);
93+
const bmadEntries = entries.filter((e) => isBmadOwnedEntry(e, canonicalIds));
94+
if (bmadEntries.length > 0) {
95+
findings.push({ path: resolved, displayPath: legacyPath, count: bmadEntries.length, entries: bmadEntries });
96+
}
97+
} catch {
98+
// Unreadable dir — skip
99+
}
100+
}
101+
return findings;
102+
}
103+
104+
function isPreNativeSkillsVersion(version) {
105+
if (!version) return false;
106+
const coerced = semver.valid(version) || semver.valid(semver.coerce(version));
107+
if (!coerced) return false;
108+
return semver.lt(coerced, MIN_NATIVE_SKILLS_VERSION);
109+
}
110+
111+
async function warnPreNativeSkillsLegacy({ projectRoot, existingVersion } = {}) {
112+
const versionTriggered = isPreNativeSkillsVersion(existingVersion);
113+
const staleDirs = await findStaleLegacyDirs(projectRoot);
114+
115+
if (!versionTriggered && staleDirs.length === 0) return;
116+
117+
if (versionTriggered) {
118+
await prompts.log.warn(
119+
`Detected previous BMAD install v${existingVersion} (pre-${MIN_NATIVE_SKILLS_VERSION}). ` +
120+
`BMAD switched to native skills format in v${MIN_NATIVE_SKILLS_VERSION}; old command/workflow directories from your prior install may still be present.`,
121+
);
122+
}
123+
124+
if (staleDirs.length > 0) {
125+
await prompts.log.warn(
126+
`Found stale BMAD entries in ${staleDirs.length} legacy location(s) that the new installer no longer manages. ` +
127+
`Your AI tool may load these alongside the new skills, causing duplicates. Remove them manually:`,
128+
);
129+
for (const finding of staleDirs) {
130+
// Print each entry by exact name. A `bmad*` glob would (a) miss
131+
// custom-module skills the canonicalId scan now picks up, and
132+
// (b) match bmad-os-* utility skills the user should keep.
133+
const entries = finding.entries || [];
134+
for (const entry of entries) {
135+
await prompts.log.message(` rm -rf "${path.join(finding.path, entry)}"`);
136+
}
137+
}
138+
} else if (versionTriggered) {
139+
await prompts.log.message(
140+
' 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).',
141+
);
142+
}
143+
}
144+
145+
module.exports = {
146+
warnPreNativeSkillsLegacy,
147+
findStaleLegacyDirs,
148+
isPreNativeSkillsVersion,
149+
LEGACY_PATHS,
150+
MIN_NATIVE_SKILLS_VERSION,
151+
};

0 commit comments

Comments
 (0)