bmad-method 6.4.0 → 6.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "bmad-method",
4
- "version": "6.4.0",
4
+ "version": "6.5.0",
5
5
  "description": "Breakthrough Method of Agile AI-driven Development",
6
6
  "keywords": [
7
7
  "agile",
@@ -14,6 +14,7 @@ const { ExternalModuleManager } = require('../modules/external-manager');
14
14
  const { resolveModuleVersion } = require('../modules/version-resolver');
15
15
 
16
16
  const { ExistingInstall } = require('./existing-install');
17
+ const { warnPreNativeSkillsLegacy } = require('./legacy-warnings');
17
18
 
18
19
  class Installer {
19
20
  constructor() {
@@ -41,6 +42,16 @@ class Installer {
41
42
  const officialModules = await OfficialModules.build(config, paths);
42
43
  const existingInstall = await ExistingInstall.detect(paths.bmadDir);
43
44
 
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
+
44
55
  if (existingInstall.installed) {
45
56
  await this._removeDeselectedModules(existingInstall, config, paths);
46
57
  updateState = await this._prepareUpdateState(paths, config, existingInstall, officialModules);
@@ -183,15 +194,16 @@ class Installer {
183
194
 
184
195
  if (toRemove.length === 0) return;
185
196
 
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'}`);
195
207
  }
196
208
  }
197
209
  }
@@ -342,13 +354,14 @@ class Installer {
342
354
  return;
343
355
  }
344
356
 
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
+ });
351
362
 
363
+ for (const setupResult of setupResults) {
364
+ const ide = setupResult.ide;
352
365
  if (setupResult.success) {
353
366
  addResult(ide, 'ok', setupResult.detail || '');
354
367
  } else {
@@ -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
+ };
@@ -1,10 +1,10 @@
1
- const os = require('node:os');
2
1
  const path = require('node:path');
3
2
  const fs = require('../fs-native');
4
3
  const yaml = require('yaml');
5
4
  const prompts = require('../prompts');
6
5
  const csv = require('csv-parse/sync');
7
6
  const { BMAD_FOLDER_NAME } = require('./shared/path-utils');
7
+ const { getInstalledCanonicalIds, isBmadOwnedEntry } = require('./shared/installed-skills');
8
8
 
9
9
  /**
10
10
  * Config-driven IDE setup handler
@@ -16,7 +16,7 @@ const { BMAD_FOLDER_NAME } = require('./shared/path-utils');
16
16
  * Features:
17
17
  * - Config-driven from platform-codes.yaml
18
18
  * - Verbatim skill installation from skill-manifest.csv
19
- * - Legacy directory cleanup and IDE-specific marker removal
19
+ * - IDE-specific marker removal (copilot-instructions, kilo modes, rovodev prompts)
20
20
  */
21
21
  class ConfigDrivenIdeSetup {
22
22
  constructor(platformCode, platformConfig) {
@@ -44,16 +44,20 @@ class ConfigDrivenIdeSetup {
44
44
  async detect(projectDir) {
45
45
  if (!this.configDir) return false;
46
46
 
47
- const dir = path.join(projectDir || process.cwd(), this.configDir);
48
- if (await fs.pathExists(dir)) {
49
- try {
50
- const entries = await fs.readdir(dir);
51
- return entries.some((e) => typeof e === 'string' && e.startsWith('bmad'));
52
- } catch {
53
- return false;
54
- }
47
+ const root = projectDir || process.cwd();
48
+ const dir = path.join(root, this.configDir);
49
+ if (!(await fs.pathExists(dir))) return false;
50
+
51
+ let entries;
52
+ try {
53
+ entries = await fs.readdir(dir);
54
+ } catch {
55
+ return false;
55
56
  }
56
- return false;
57
+
58
+ const bmadDir = await this._findBmadDir(root);
59
+ const canonicalIds = await getInstalledCanonicalIds(bmadDir);
60
+ return entries.some((e) => isBmadOwnedEntry(e, canonicalIds));
57
61
  }
58
62
 
59
63
  /**
@@ -92,6 +96,12 @@ class ConfigDrivenIdeSetup {
92
96
  return { success: false, reason: 'no-config' };
93
97
  }
94
98
 
99
+ // When a peer platform in the same install batch owns this target_dir,
100
+ // skip the skill write — the peer has already populated it.
101
+ if (options.skipTarget) {
102
+ return { success: true, results: { skills: 0, sharedTargetHandledByPeer: true } };
103
+ }
104
+
95
105
  if (this.installerConfig.target_dir) {
96
106
  return this.installToTarget(projectDir, bmadDir, this.installerConfig, options);
97
107
  }
@@ -222,27 +232,6 @@ class ConfigDrivenIdeSetup {
222
232
  removalSet = new Set();
223
233
  }
224
234
 
225
- // Migrate legacy target directories (e.g. .opencode/agent → .opencode/agents)
226
- // Legacy dirs are abandoned entirely, so use prefix matching (null removalSet)
227
- if (this.installerConfig?.legacy_targets) {
228
- const legacyDirsExist = await Promise.all(
229
- this.installerConfig.legacy_targets.map((d) =>
230
- this.isGlobalPath(d) ? fs.pathExists(d.replace(/^~/, os.homedir())) : fs.pathExists(path.join(projectDir, d)),
231
- ),
232
- );
233
- if (legacyDirsExist.some(Boolean)) {
234
- if (!options.silent) await prompts.log.message(' Migrating legacy directories...');
235
- for (const legacyDir of this.installerConfig.legacy_targets) {
236
- if (this.isGlobalPath(legacyDir)) {
237
- await this.warnGlobalLegacy(legacyDir, options);
238
- } else {
239
- await this.cleanupTarget(projectDir, legacyDir, options, null);
240
- await this.removeEmptyParents(projectDir, legacyDir);
241
- }
242
- }
243
- }
244
- }
245
-
246
235
  // Strip BMAD markers from copilot-instructions.md if present
247
236
  if (this.name === 'github-copilot') {
248
237
  await this.cleanupCopilotInstructions(projectDir, options);
@@ -258,47 +247,17 @@ class ConfigDrivenIdeSetup {
258
247
  await this.cleanupRovoDevPrompts(projectDir, options);
259
248
  }
260
249
 
250
+ // Skip target_dir cleanup when a peer platform owns this directory
251
+ // (set during dedup'd install or when uninstalling one of several
252
+ // platforms that share the same target_dir).
253
+ if (options.skipTarget) return;
254
+
261
255
  // Clean current target directory
262
256
  if (this.installerConfig?.target_dir) {
263
257
  await this.cleanupTarget(projectDir, this.installerConfig.target_dir, options, removalSet);
264
258
  }
265
259
  }
266
260
 
267
- /**
268
- * Check if a path is global (starts with ~ or is absolute)
269
- * @param {string} p - Path to check
270
- * @returns {boolean}
271
- */
272
- isGlobalPath(p) {
273
- return p.startsWith('~') || path.isAbsolute(p);
274
- }
275
-
276
- /**
277
- * Warn about stale BMAD files in a global legacy directory (never auto-deletes)
278
- * @param {string} legacyDir - Legacy directory path (may start with ~)
279
- * @param {Object} options - Options (silent, etc.)
280
- */
281
- async warnGlobalLegacy(legacyDir, options = {}) {
282
- try {
283
- const expanded = legacyDir.startsWith('~/')
284
- ? path.join(os.homedir(), legacyDir.slice(2))
285
- : legacyDir === '~'
286
- ? os.homedir()
287
- : legacyDir;
288
-
289
- if (!(await fs.pathExists(expanded))) return;
290
-
291
- const entries = await fs.readdir(expanded);
292
- const bmadFiles = entries.filter((e) => typeof e === 'string' && e.startsWith('bmad'));
293
-
294
- if (bmadFiles.length > 0 && !options.silent) {
295
- await prompts.log.warn(`Found ${bmadFiles.length} stale BMAD file(s) in ${expanded}. Remove manually: rm ${expanded}/bmad-*`);
296
- }
297
- } catch {
298
- // Errors reading global paths are silently ignored
299
- }
300
- }
301
-
302
261
  /**
303
262
  * Find the _bmad directory in a project
304
263
  * @param {string} projectDir - Project directory
@@ -426,8 +385,8 @@ class ConfigDrivenIdeSetup {
426
385
  // Always preserve bmad-os-* utility skills regardless of cleanup mode
427
386
  if (entry.startsWith('bmad-os-')) continue;
428
387
 
429
- // Surgical removal from set, or legacy prefix matching when set is null
430
- const shouldRemove = removalSet ? removalSet.has(entry) : entry.startsWith('bmad');
388
+ // Surgical removal from set, or fallback to manifest+prefix detection when null
389
+ const shouldRemove = removalSet ? removalSet.has(entry) : isBmadOwnedEntry(entry, null);
431
390
 
432
391
  if (shouldRemove) {
433
392
  try {
@@ -590,10 +549,9 @@ class ConfigDrivenIdeSetup {
590
549
  try {
591
550
  if (await fs.pathExists(candidatePath)) {
592
551
  const entries = await fs.readdir(candidatePath);
593
- const hasBmad = entries.some(
594
- (e) => typeof e === 'string' && e.toLowerCase().startsWith('bmad') && !e.toLowerCase().startsWith('bmad-os-'),
595
- );
596
- if (hasBmad) {
552
+ const ancestorBmadDir = await this._findBmadDir(current);
553
+ const canonicalIds = await getInstalledCanonicalIds(ancestorBmadDir);
554
+ if (entries.some((e) => isBmadOwnedEntry(e, canonicalIds))) {
597
555
  return candidatePath;
598
556
  }
599
557
  }
@@ -605,43 +563,6 @@ class ConfigDrivenIdeSetup {
605
563
 
606
564
  return null;
607
565
  }
608
-
609
- /**
610
- * Walk up ancestor directories from relativeDir toward projectDir, removing each if empty
611
- * Stops at projectDir boundary — never removes projectDir itself
612
- * @param {string} projectDir - Project root (boundary)
613
- * @param {string} relativeDir - Relative directory to start from
614
- */
615
- async removeEmptyParents(projectDir, relativeDir) {
616
- const resolvedProject = path.resolve(projectDir);
617
- let current = relativeDir;
618
- let last = null;
619
- while (current && current !== '.' && current !== last) {
620
- last = current;
621
- const fullPath = path.resolve(projectDir, current);
622
- // Boundary guard: never traverse outside projectDir
623
- if (!fullPath.startsWith(resolvedProject + path.sep) && fullPath !== resolvedProject) break;
624
- try {
625
- if (!(await fs.pathExists(fullPath))) {
626
- // Dir already gone — advance current; last is reset at top of next iteration
627
- current = path.dirname(current);
628
- continue;
629
- }
630
- const remaining = await fs.readdir(fullPath);
631
- if (remaining.length > 0) break;
632
- await fs.rmdir(fullPath);
633
- } catch (error) {
634
- // ENOTEMPTY: TOCTOU race (file added between readdir and rmdir) — skip level, continue upward
635
- // ENOENT: dir removed by another process between pathExists and rmdir — skip level, continue upward
636
- if (error.code === 'ENOTEMPTY' || error.code === 'ENOENT') {
637
- current = path.dirname(current);
638
- continue;
639
- }
640
- break; // fatal error (e.g. EACCES) — stop upward walk
641
- }
642
- current = path.dirname(current);
643
- }
644
- }
645
566
  }
646
567
 
647
568
  module.exports = { ConfigDrivenIdeSetup };
@@ -160,8 +160,18 @@ class IdeManager {
160
160
  let detail = '';
161
161
  if (handlerResult && handlerResult.results) {
162
162
  const r = handlerResult.results;
163
- const count = r.skillDirectories || r.skills || 0;
164
- if (count > 0) detail = `${count} skills`;
163
+ let count = r.skillDirectories || r.skills || 0;
164
+ // Dedup'd platform: report the count its peer wrote so the user sees
165
+ // a consistent picture across all platforms sharing the dir.
166
+ if (count === 0 && r.sharedTargetHandledByPeer && options.sharedSkillCount) {
167
+ count = options.sharedSkillCount;
168
+ }
169
+ const targetDir = handler.installerConfig?.target_dir || null;
170
+ if (count > 0 && targetDir) {
171
+ detail = `${count} skills → ${targetDir}`;
172
+ } else if (count > 0) {
173
+ detail = `${count} skills`;
174
+ }
165
175
  }
166
176
  // Propagate handler's success status (default true for backward compat)
167
177
  const success = handlerResult?.success !== false;
@@ -172,6 +182,57 @@ class IdeManager {
172
182
  }
173
183
  }
174
184
 
185
+ /**
186
+ * Run setup for multiple IDEs as a single batch.
187
+ * Dedupes work when several selected platforms share the same target_dir:
188
+ * the first platform owns the directory write, peers skip it.
189
+ * @param {Array<string>} ideList - IDE names to set up
190
+ * @param {string} projectDir
191
+ * @param {string} bmadDir
192
+ * @param {Object} [options] - Forwarded to each handler.setup
193
+ * @returns {Promise<Array>} Per-IDE results
194
+ */
195
+ async setupBatch(ideList, projectDir, bmadDir, options = {}) {
196
+ await this.ensureInitialized();
197
+ const results = [];
198
+ // target_dir → { firstIde, skillCount } from the platform that actually wrote it
199
+ const claimedTargets = new Map();
200
+
201
+ for (const ideName of ideList) {
202
+ const handler = this.handlers.get(ideName.toLowerCase());
203
+ if (!handler) {
204
+ results.push(await this.setup(ideName, projectDir, bmadDir, options));
205
+ continue;
206
+ }
207
+
208
+ const target = handler.installerConfig?.target_dir || null;
209
+ const claim = target ? claimedTargets.get(target) : null;
210
+ const skipTarget = !!claim;
211
+
212
+ const result = await this.setup(ideName, projectDir, bmadDir, {
213
+ ...options,
214
+ skipTarget,
215
+ sharedWith: claim?.firstIde || null,
216
+ sharedTarget: target,
217
+ sharedSkillCount: claim?.skillCount || 0,
218
+ });
219
+
220
+ if (target && !claim) {
221
+ const writtenCount = result.handlerResult?.results?.skillDirectories || result.handlerResult?.results?.skills || 0;
222
+ // Only claim the target when the install actually succeeded and wrote skills.
223
+ // If the first platform fails (ancestor conflict, exception, etc.), leave the
224
+ // dir unclaimed so the next peer becomes the new first writer instead of
225
+ // silently skipping into a broken/empty target_dir.
226
+ if (result.success && writtenCount > 0) {
227
+ claimedTargets.set(target, { firstIde: ideName, skillCount: writtenCount });
228
+ }
229
+ }
230
+ results.push(result);
231
+ }
232
+
233
+ return results;
234
+ }
235
+
175
236
  /**
176
237
  * Cleanup IDE configurations
177
238
  * @param {string} projectDir - Project directory
@@ -198,6 +259,8 @@ class IdeManager {
198
259
  * @param {string} projectDir - Project directory
199
260
  * @param {Array<string>} ideList - List of IDE names to clean up
200
261
  * @param {Object} [options] - Cleanup options passed through to handlers
262
+ * options.remainingIdes - IDE names still installed after this cleanup; used
263
+ * to skip target_dir wipe when a co-installed platform shares the dir.
201
264
  * @returns {Array} Results array
202
265
  */
203
266
  async cleanupByList(projectDir, ideList, options = {}) {
@@ -211,13 +274,27 @@ class IdeManager {
211
274
  // Build lowercase lookup for case-insensitive matching
212
275
  const lowercaseHandlers = new Map([...this.handlers.entries()].map(([k, v]) => [k.toLowerCase(), v]));
213
276
 
277
+ // Resolve target_dirs for IDEs that will remain installed after this cleanup
278
+ const remainingTargets = new Set();
279
+ if (Array.isArray(options.remainingIdes)) {
280
+ for (const remaining of options.remainingIdes) {
281
+ const h = lowercaseHandlers.get(String(remaining).toLowerCase());
282
+ const t = h?.installerConfig?.target_dir;
283
+ if (t) remainingTargets.add(t);
284
+ }
285
+ }
286
+
214
287
  for (const ideName of ideList) {
215
288
  const handler = lowercaseHandlers.get(ideName.toLowerCase());
216
289
  if (!handler) continue;
217
290
 
291
+ const target = handler.installerConfig?.target_dir || null;
292
+ const skipTarget = target && remainingTargets.has(target);
293
+ const cleanupOptions = skipTarget ? { ...options, skipTarget: true } : options;
294
+
218
295
  try {
219
- await handler.cleanup(projectDir, options);
220
- results.push({ ide: ideName, success: true });
296
+ await handler.cleanup(projectDir, cleanupOptions);
297
+ results.push({ ide: ideName, success: true, skippedTarget: !!skipTarget });
221
298
  } catch (error) {
222
299
  results.push({ ide: ideName, success: false, error: error.message });
223
300
  }
@@ -5,128 +5,203 @@
5
5
  # preferred: Whether shown as a recommended option on install
6
6
  # suspended: (optional) Message explaining why install is blocked
7
7
  # installer:
8
- # target_dir: Directory where skill directories are installed
9
- # legacy_targets: (optional) Old target dirs to clean up on reinstall
8
+ # target_dir: Directory where skill directories are installed (project/workspace)
9
+ # global_target_dir: (optional) User-home directory for global install
10
10
  # ancestor_conflict_check: (optional) Refuse install when ancestor dir has BMAD files
11
+ #
12
+ # Multiple platforms may share the same target_dir or global_target_dir — many tools
13
+ # read from the shared `.agents/skills/` and `~/.agents/skills/` cross-tool standard.
14
+ # Paths verified against each tool's primary docs as of 2026-04-25.
11
15
 
12
16
  platforms:
17
+ adal:
18
+ name: "AdaL"
19
+ preferred: false
20
+ installer:
21
+ target_dir: .adal/skills
22
+ global_target_dir: ~/.adal/skills
23
+
24
+ amp:
25
+ name: "Sourcegraph Amp"
26
+ preferred: false
27
+ installer:
28
+ target_dir: .agents/skills
29
+ global_target_dir: ~/.config/agents/skills
30
+
13
31
  antigravity:
14
32
  name: "Google Antigravity"
15
33
  preferred: false
16
34
  installer:
17
- legacy_targets:
18
- - .agent/workflows
19
35
  target_dir: .agent/skills
36
+ global_target_dir: ~/.gemini/antigravity/skills
20
37
 
21
38
  auggie:
22
39
  name: "Auggie"
23
40
  preferred: false
24
41
  installer:
25
- legacy_targets:
26
- - .augment/commands
27
- target_dir: .augment/skills
42
+ target_dir: .agents/skills
43
+ global_target_dir: ~/.agents/skills
44
+
45
+ bob:
46
+ name: "IBM Bob"
47
+ preferred: false
48
+ installer:
49
+ target_dir: .bob/skills
50
+ global_target_dir: ~/.bob/skills
28
51
 
29
52
  claude-code:
30
53
  name: "Claude Code"
31
54
  preferred: true
32
55
  installer:
33
- legacy_targets:
34
- - .claude/commands
35
56
  target_dir: .claude/skills
57
+ global_target_dir: ~/.claude/skills
36
58
 
37
59
  cline:
38
60
  name: "Cline"
39
61
  preferred: false
40
62
  installer:
41
- legacy_targets:
42
- - .clinerules/workflows
43
63
  target_dir: .cline/skills
64
+ global_target_dir: ~/.cline/skills
44
65
 
45
66
  codex:
46
67
  name: "Codex"
47
- preferred: false
68
+ preferred: true
48
69
  installer:
49
- legacy_targets:
50
- - .codex/prompts
51
- - ~/.codex/prompts
52
70
  target_dir: .agents/skills
71
+ global_target_dir: ~/.codex/skills
53
72
 
54
73
  codebuddy:
55
74
  name: "CodeBuddy"
56
75
  preferred: false
57
76
  installer:
58
- legacy_targets:
59
- - .codebuddy/commands
60
77
  target_dir: .codebuddy/skills
78
+ global_target_dir: ~/.codebuddy/skills
79
+
80
+ command-code:
81
+ name: "Command Code"
82
+ preferred: false
83
+ installer:
84
+ target_dir: .agents/skills
85
+ global_target_dir: ~/.agents/skills
86
+
87
+ cortex:
88
+ name: "Snowflake Cortex Code"
89
+ preferred: false
90
+ installer:
91
+ target_dir: .cortex/skills
92
+ global_target_dir: ~/.snowflake/cortex/skills
61
93
 
62
94
  crush:
63
95
  name: "Crush"
64
96
  preferred: false
65
97
  installer:
66
- legacy_targets:
67
- - .crush/commands
68
- target_dir: .crush/skills
98
+ target_dir: .agents/skills
99
+ global_target_dir: ~/.config/agents/skills
69
100
 
70
101
  cursor:
71
102
  name: "Cursor"
72
103
  preferred: true
73
104
  installer:
74
- legacy_targets:
75
- - .cursor/commands
76
- target_dir: .cursor/skills
105
+ target_dir: .agents/skills
106
+ global_target_dir: ~/.agents/skills
107
+
108
+ droid:
109
+ name: "Factory Droid"
110
+ preferred: false
111
+ installer:
112
+ target_dir: .factory/skills
113
+ global_target_dir: ~/.factory/skills
114
+
115
+ firebender:
116
+ name: "Firebender"
117
+ preferred: false
118
+ installer:
119
+ target_dir: .firebender/skills
120
+ global_target_dir: ~/.agents/skills
77
121
 
78
122
  gemini:
79
123
  name: "Gemini CLI"
80
124
  preferred: false
81
125
  installer:
82
- legacy_targets:
83
- - .gemini/commands
84
- target_dir: .gemini/skills
126
+ target_dir: .agents/skills
127
+ global_target_dir: ~/.agents/skills
85
128
 
86
129
  github-copilot:
87
130
  name: "GitHub Copilot"
131
+ preferred: true
132
+ installer:
133
+ target_dir: .agents/skills
134
+ global_target_dir: ~/.agents/skills
135
+
136
+ goose:
137
+ name: "Block Goose"
88
138
  preferred: false
89
139
  installer:
90
- legacy_targets:
91
- - .github/agents
92
- - .github/prompts
93
- target_dir: .github/skills
140
+ target_dir: .agents/skills
141
+ global_target_dir: ~/.config/agents/skills
94
142
 
95
143
  iflow:
96
144
  name: "iFlow"
97
145
  preferred: false
98
146
  installer:
99
- legacy_targets:
100
- - .iflow/commands
101
147
  target_dir: .iflow/skills
148
+ global_target_dir: ~/.iflow/skills
102
149
 
103
150
  junie:
104
151
  name: "Junie"
105
152
  preferred: false
106
153
  installer:
107
- target_dir: .agents/skills
154
+ target_dir: .junie/skills
155
+ global_target_dir: ~/.junie/skills
108
156
 
109
157
  kilo:
110
158
  name: "KiloCoder"
111
159
  preferred: false
112
160
  installer:
113
- legacy_targets:
114
- - .kilocode/workflows
115
- target_dir: .kilocode/skills
161
+ target_dir: .agents/skills
162
+ global_target_dir: ~/.kilocode/skills
116
163
 
117
164
  kimi-code:
118
165
  name: "Kimi Code"
119
166
  preferred: false
120
167
  installer:
121
- target_dir: .kimi/skills
168
+ target_dir: .agents/skills
169
+ global_target_dir: ~/.agents/skills
122
170
 
123
171
  kiro:
124
172
  name: "Kiro"
125
173
  preferred: false
126
174
  installer:
127
- legacy_targets:
128
- - .kiro/steering
129
175
  target_dir: .kiro/skills
176
+ global_target_dir: ~/.kiro/skills
177
+
178
+ kode:
179
+ name: "Kode"
180
+ preferred: false
181
+ installer:
182
+ target_dir: .kode/skills
183
+ global_target_dir: ~/.kode/skills
184
+
185
+ mistral-vibe:
186
+ name: "Mistral Vibe"
187
+ preferred: false
188
+ installer:
189
+ target_dir: .agents/skills
190
+ global_target_dir: ~/.vibe/skills
191
+
192
+ mux:
193
+ name: "Mux"
194
+ preferred: false
195
+ installer:
196
+ target_dir: .agents/skills
197
+ global_target_dir: ~/.agents/skills
198
+
199
+ neovate:
200
+ name: "Neovate"
201
+ preferred: false
202
+ installer:
203
+ target_dir: .neovate/skills
204
+ global_target_dir: ~/.neovate/skills
130
205
 
131
206
  ona:
132
207
  name: "Ona"
@@ -134,65 +209,98 @@ platforms:
134
209
  installer:
135
210
  target_dir: .ona/skills
136
211
 
212
+ openclaw:
213
+ name: "OpenClaw"
214
+ preferred: false
215
+ installer:
216
+ target_dir: .agents/skills
217
+ global_target_dir: ~/.agents/skills
218
+
137
219
  opencode:
138
220
  name: "OpenCode"
139
221
  preferred: false
140
222
  installer:
141
- legacy_targets:
142
- - .opencode/agents
143
- - .opencode/commands
144
- - .opencode/agent
145
- - .opencode/command
146
- target_dir: .opencode/skills
223
+ target_dir: .agents/skills
224
+ global_target_dir: ~/.agents/skills
225
+
226
+ openhands:
227
+ name: "OpenHands"
228
+ preferred: false
229
+ installer:
230
+ target_dir: .agents/skills
231
+ global_target_dir: ~/.agents/skills
147
232
 
148
233
  pi:
149
234
  name: "Pi"
150
235
  preferred: false
151
236
  installer:
152
- target_dir: .pi/skills
237
+ target_dir: .agents/skills
238
+ global_target_dir: ~/.agents/skills
239
+
240
+ pochi:
241
+ name: "Pochi"
242
+ preferred: false
243
+ installer:
244
+ target_dir: .agents/skills
245
+ global_target_dir: ~/.agents/skills
153
246
 
154
247
  qoder:
155
248
  name: "Qoder"
156
249
  preferred: false
157
250
  installer:
158
251
  target_dir: .qoder/skills
252
+ global_target_dir: ~/.qoder/skills
159
253
 
160
254
  qwen:
161
255
  name: "QwenCoder"
162
256
  preferred: false
163
257
  installer:
164
- legacy_targets:
165
- - .qwen/commands
166
258
  target_dir: .qwen/skills
259
+ global_target_dir: ~/.qwen/skills
260
+
261
+ replit:
262
+ name: "Replit Agent"
263
+ preferred: false
264
+ installer:
265
+ target_dir: .agents/skills
167
266
 
168
267
  roo:
169
268
  name: "Roo Code"
170
269
  preferred: false
171
270
  installer:
172
- legacy_targets:
173
- - .roo/commands
174
- target_dir: .roo/skills
271
+ target_dir: .agents/skills
272
+ global_target_dir: ~/.agents/skills
175
273
 
176
274
  rovo-dev:
177
275
  name: "Rovo Dev"
178
276
  preferred: false
179
277
  installer:
180
- legacy_targets:
181
- - .rovodev/workflows
182
- target_dir: .rovodev/skills
278
+ target_dir: .agents/skills
279
+ global_target_dir: ~/.agents/skills
183
280
 
184
281
  trae:
185
282
  name: "Trae"
186
283
  preferred: false
187
284
  installer:
188
- legacy_targets:
189
- - .trae/rules
190
285
  target_dir: .trae/skills
191
286
 
287
+ warp:
288
+ name: "Warp"
289
+ preferred: false
290
+ installer:
291
+ target_dir: .agents/skills
292
+ global_target_dir: ~/.agents/skills
293
+
192
294
  windsurf:
193
295
  name: "Windsurf"
194
296
  preferred: false
195
297
  installer:
196
- legacy_targets:
197
- - .windsurf/workflows
198
- target_dir: .windsurf/skills
298
+ target_dir: .agents/skills
299
+ global_target_dir: ~/.agents/skills
300
+
301
+ zencoder:
302
+ name: "Zencoder"
303
+ preferred: false
304
+ installer:
305
+ target_dir: .zencoder/skills
306
+ global_target_dir: ~/.zencoder/skills
@@ -0,0 +1,50 @@
1
+ const path = require('node:path');
2
+ const fs = require('../../fs-native');
3
+ const csv = require('csv-parse/sync');
4
+
5
+ /**
6
+ * Read the global skill-manifest.csv and return the set of canonicalIds.
7
+ * These define which directory entries in a target_dir are BMAD-owned, regardless
8
+ * of whether they happen to start with "bmad-" (custom modules can ship skills
9
+ * with any prefix, e.g. "fred-cool-skill").
10
+ *
11
+ * @param {string} bmadDir - Path to the _bmad install directory
12
+ * @returns {Promise<Set<string>>} Set of canonicalIds, or empty set if manifest missing
13
+ */
14
+ async function getInstalledCanonicalIds(bmadDir) {
15
+ const ids = new Set();
16
+ if (!bmadDir) return ids;
17
+
18
+ const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv');
19
+ if (!(await fs.pathExists(csvPath))) return ids;
20
+
21
+ try {
22
+ const content = await fs.readFile(csvPath, 'utf8');
23
+ const records = csv.parse(content, { columns: true, skip_empty_lines: true });
24
+ for (const record of records) {
25
+ if (record.canonicalId) ids.add(record.canonicalId);
26
+ }
27
+ } catch {
28
+ // Unreadable/invalid manifest — treat as no info
29
+ }
30
+
31
+ return ids;
32
+ }
33
+
34
+ /**
35
+ * Test whether a directory entry is BMAD-owned.
36
+ * Prefers the manifest's canonicalIds; falls back to the legacy "bmad" prefix
37
+ * when no manifest is available (early install, ancestor lookup with no bmad dir).
38
+ *
39
+ * @param {string} entry - Directory entry name
40
+ * @param {Set<string>|null} canonicalIds - From getInstalledCanonicalIds, or null
41
+ * @returns {boolean}
42
+ */
43
+ function isBmadOwnedEntry(entry, canonicalIds) {
44
+ if (!entry || typeof entry !== 'string') return false;
45
+ if (entry.toLowerCase().startsWith('bmad-os-')) return false;
46
+ if (canonicalIds && canonicalIds.size > 0) return canonicalIds.has(entry);
47
+ return entry.toLowerCase().startsWith('bmad');
48
+ }
49
+
50
+ module.exports = { getInstalledCanonicalIds, isBmadOwnedEntry };
@@ -1,175 +0,0 @@
1
- # BMAD Platform Codes Configuration
2
- # Central configuration for all platform/IDE codes used in the BMAD system
3
- #
4
- # This file defines the standardized platform codes that are used throughout
5
- # the installation system to identify different platforms (IDEs, tools, etc.)
6
- #
7
- # Format:
8
- # code: Platform identifier used internally
9
- # name: Display name shown to users
10
- # preferred: Whether this platform is shown as a recommended option on install
11
- # category: Type of platform (ide, tool, service, etc.)
12
-
13
- platforms:
14
- # Recommended Platforms
15
- claude-code:
16
- name: "Claude Code"
17
- preferred: true
18
- category: cli
19
- description: "Anthropic's official CLI for Claude"
20
-
21
- cursor:
22
- name: "Cursor"
23
- preferred: true
24
- category: ide
25
- description: "AI-first code editor"
26
-
27
- # Other IDEs and Tools
28
- cline:
29
- name: "Cline"
30
- preferred: false
31
- category: ide
32
- description: "AI coding assistant"
33
-
34
- opencode:
35
- name: "OpenCode"
36
- preferred: false
37
- category: ide
38
- description: "OpenCode terminal coding assistant"
39
-
40
- codebuddy:
41
- name: "CodeBuddy"
42
- preferred: false
43
- category: ide
44
- description: "Tencent Cloud Code Assistant - AI-powered coding companion"
45
-
46
- auggie:
47
- name: "Auggie"
48
- preferred: false
49
- category: cli
50
- description: "AI development tool"
51
-
52
- roo:
53
- name: "Roo Code"
54
- preferred: false
55
- category: ide
56
- description: "Enhanced Cline fork"
57
-
58
- rovo-dev:
59
- name: "Rovo Dev"
60
- preferred: false
61
- category: ide
62
- description: "Atlassian's Rovo development environment"
63
-
64
- kiro:
65
- name: "Kiro"
66
- preferred: false
67
- category: ide
68
- description: "Amazon's AI-powered IDE"
69
-
70
- github-copilot:
71
- name: "GitHub Copilot"
72
- preferred: false
73
- category: ide
74
- description: "GitHub's AI pair programmer"
75
-
76
- codex:
77
- name: "Codex"
78
- preferred: false
79
- category: cli
80
- description: "OpenAI Codex integration"
81
-
82
- qwen:
83
- name: "QwenCoder"
84
- preferred: false
85
- category: ide
86
- description: "Qwen AI coding assistant"
87
-
88
- gemini:
89
- name: "Gemini CLI"
90
- preferred: false
91
- category: cli
92
- description: "Google's CLI for Gemini"
93
-
94
- iflow:
95
- name: "iFlow"
96
- preferred: false
97
- category: ide
98
- description: "AI workflow automation"
99
-
100
- kilo:
101
- name: "KiloCoder"
102
- preferred: false
103
- category: ide
104
- description: "AI coding platform"
105
-
106
- kimi-code:
107
- name: "Kimi Code"
108
- preferred: false
109
- category: cli
110
- description: "Moonshot AI's Kimi Code CLI"
111
-
112
- crush:
113
- name: "Crush"
114
- preferred: false
115
- category: ide
116
- description: "AI development assistant"
117
-
118
- antigravity:
119
- name: "Google Antigravity"
120
- preferred: false
121
- category: ide
122
- description: "Google's AI development environment"
123
-
124
- trae:
125
- name: "Trae"
126
- preferred: false
127
- category: ide
128
- description: "AI coding tool"
129
-
130
- windsurf:
131
- name: "Windsurf"
132
- preferred: false
133
- category: ide
134
- description: "AI-powered IDE with cascade flows"
135
-
136
- junie:
137
- name: "Junie"
138
- preferred: false
139
- category: cli
140
- description: "AI coding agent by JetBrains"
141
-
142
- ona:
143
- name: "Ona"
144
- preferred: false
145
- category: ide
146
- description: "Ona AI development environment"
147
-
148
- # Platform categories
149
- categories:
150
- ide:
151
- name: "Integrated Development Environment"
152
- description: "Full-featured code editors with AI assistance"
153
-
154
- cli:
155
- name: "Command Line Interface"
156
- description: "Terminal-based tools"
157
-
158
- tool:
159
- name: "Development Tool"
160
- description: "Standalone development utilities"
161
-
162
- service:
163
- name: "Cloud Service"
164
- description: "Cloud-based development platforms"
165
-
166
- extension:
167
- name: "Editor Extension"
168
- description: "Plugins for existing editors"
169
-
170
- # Naming conventions and rules
171
- conventions:
172
- code_format: "lowercase-kebab-case"
173
- name_format: "Title Case"
174
- max_code_length: 20
175
- allowed_characters: "a-z0-9-"