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 +1 -1
- package/tools/installer/core/installer.js +28 -15
- package/tools/installer/core/legacy-warnings.js +151 -0
- package/tools/installer/ide/_config-driven.js +31 -110
- package/tools/installer/ide/manager.js +81 -4
- package/tools/installer/ide/platform-codes.yaml +167 -59
- package/tools/installer/ide/shared/installed-skills.js +50 -0
- package/tools/platform-codes.yaml +0 -175
package/package.json
CHANGED
|
@@ -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
|
-
|
|
187
|
-
for
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
* -
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
|
430
|
-
const shouldRemove = removalSet ? removalSet.has(entry) : entry
|
|
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
|
|
594
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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,
|
|
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
|
-
#
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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:
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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: .
|
|
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
|
-
|
|
114
|
-
|
|
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: .
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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: .
|
|
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
|
-
|
|
173
|
-
|
|
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
|
-
|
|
181
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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-"
|