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.
This commit is contained in:
parent
5ff534df94
commit
6e287245ca
|
|
@ -1382,7 +1382,7 @@ async function runTests() {
|
|||
});
|
||||
|
||||
assert(result.success === true, 'Antigravity setup succeeds with overlapping skill names');
|
||||
assert(result.detail === '1 skills', 'Installer detail reports skill count');
|
||||
assert(result.detail === '1 skills → .agent/skills', 'Installer detail reports skill count and target dir');
|
||||
assert(result.handlerResult.results.skillDirectories === 1, 'Result exposes unique skill directory count');
|
||||
assert(result.handlerResult.results.skills === 1, 'Result retains verbatim skill count');
|
||||
assert(
|
||||
|
|
@ -2622,6 +2622,109 @@ async function runTests() {
|
|||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Test Suite 40: Shared target_dir coordination
|
||||
// ============================================================
|
||||
console.log(`${colors.yellow}Test Suite 40: Shared target_dir coordination${colors.reset}\n`);
|
||||
|
||||
try {
|
||||
// Cursor and Gemini both use .agents/skills — verify they coordinate.
|
||||
clearCache();
|
||||
const platformCodes40 = await loadPlatformCodes();
|
||||
const cursorTarget = platformCodes40.platforms.cursor?.installer?.target_dir;
|
||||
const geminiTarget = platformCodes40.platforms.gemini?.installer?.target_dir;
|
||||
assert(cursorTarget === '.agents/skills' && geminiTarget === '.agents/skills', 'Cursor and Gemini share .agents/skills target_dir');
|
||||
|
||||
const tempProjectDir40 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-shared-target-'));
|
||||
const installedBmadDir40 = await createTestBmadFixture();
|
||||
|
||||
const ideManager40 = new IdeManager();
|
||||
await ideManager40.ensureInitialized();
|
||||
|
||||
// Run setupBatch with both platforms — second should skip skill write.
|
||||
const batchResults = await ideManager40.setupBatch(['cursor', 'gemini'], tempProjectDir40, installedBmadDir40, {
|
||||
silent: true,
|
||||
selectedModules: ['core'],
|
||||
});
|
||||
|
||||
assert(batchResults.length === 2, 'setupBatch returns one result per IDE');
|
||||
assert(batchResults[0].success === true, 'First platform (cursor) succeeds');
|
||||
assert(batchResults[1].success === true, 'Second platform (gemini) succeeds');
|
||||
assert(
|
||||
batchResults[1].handlerResult?.results?.sharedTargetHandledByPeer === true,
|
||||
'Second platform marked sharedTargetHandledByPeer (skipped redundant write)',
|
||||
);
|
||||
|
||||
// Skill should be present in the shared dir after batch.
|
||||
const sharedDir = path.join(tempProjectDir40, '.agents', 'skills');
|
||||
const sharedDirEntries = await fs.readdir(sharedDir);
|
||||
assert(sharedDirEntries.includes('bmad-master'), 'Shared .agents/skills/ contains bmad-master after batched install');
|
||||
|
||||
// Now uninstall just cursor while gemini remains. Skills must survive.
|
||||
const cleanupResults = await ideManager40.cleanupByList(tempProjectDir40, ['cursor'], {
|
||||
silent: true,
|
||||
remainingIdes: ['gemini'],
|
||||
});
|
||||
assert(cleanupResults[0].skippedTarget === true, 'Cursor cleanup skips target_dir wipe when Gemini remains');
|
||||
const stillThere = await fs.readdir(sharedDir);
|
||||
assert(stillThere.includes('bmad-master'), 'bmad-master still present after partial uninstall (gemini still installed)');
|
||||
|
||||
// (Cleanup of the last sharing platform requires bmadDir to be inside
|
||||
// projectDir to compute removalSet; that's the production layout. The
|
||||
// fixture above keeps bmad in a separate temp dir, so test 41 below
|
||||
// exercises the in-project layout instead.)
|
||||
|
||||
await fs.remove(tempProjectDir40).catch(() => {});
|
||||
await fs.remove(path.dirname(installedBmadDir40)).catch(() => {});
|
||||
} catch (error) {
|
||||
console.log(`${colors.red}Test Suite 40 setup failed: ${error.message}${colors.reset}`);
|
||||
failed++;
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Test Suite 41: Custom-module skill ownership (non-bmad prefix)
|
||||
// ============================================================
|
||||
console.log(`${colors.yellow}Test Suite 41: Custom-module skill ownership${colors.reset}\n`);
|
||||
|
||||
try {
|
||||
// A custom module can ship a skill with any canonicalId (e.g. "fred-cool-skill").
|
||||
// detect() must recognize it as BMAD-owned via the manifest, not the bmad- prefix.
|
||||
const fixtureRoot41 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-custom-prefix-'));
|
||||
const bmadDir41 = path.join(fixtureRoot41, '_bmad');
|
||||
await fs.ensureDir(path.join(bmadDir41, '_config'));
|
||||
await fs.writeFile(
|
||||
path.join(bmadDir41, '_config', 'skill-manifest.csv'),
|
||||
[
|
||||
'canonicalId,name,description,module,path',
|
||||
'"fred-cool-skill","fred-cool-skill","Custom module skill","fred","_bmad/fred/skills/fred-cool-skill/SKILL.md"',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
const fredSkill = path.join(bmadDir41, 'fred', 'skills', 'fred-cool-skill');
|
||||
await fs.ensureDir(fredSkill);
|
||||
await fs.writeFile(
|
||||
path.join(fredSkill, 'SKILL.md'),
|
||||
['---', 'name: fred-cool-skill', 'description: Custom module skill', '---', '', 'A custom module skill.'].join('\n'),
|
||||
);
|
||||
|
||||
const ideManager41 = new IdeManager();
|
||||
await ideManager41.ensureInitialized();
|
||||
await ideManager41.setup('cursor', fixtureRoot41, bmadDir41, { silent: true, selectedModules: ['fred'] });
|
||||
|
||||
const cursorHandler = ideManager41.handlers.get('cursor');
|
||||
const detected = await cursorHandler.detect(fixtureRoot41);
|
||||
assert(detected === true, 'detect() recognizes non-bmad-prefixed skill as BMAD-owned via skill-manifest.csv');
|
||||
|
||||
await fs.remove(fixtureRoot41).catch(() => {});
|
||||
} catch (error) {
|
||||
console.log(`${colors.red}Test Suite 41 setup failed: ${error.message}${colors.reset}`);
|
||||
failed++;
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Summary
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -189,17 +189,12 @@ class Installer {
|
|||
|
||||
if (toRemove.length === 0) return;
|
||||
|
||||
await this.ideManager.ensureInitialized();
|
||||
for (const ide of toRemove) {
|
||||
try {
|
||||
const handler = this.ideManager.handlers.get(ide);
|
||||
if (handler) {
|
||||
await handler.cleanup(paths.projectRoot);
|
||||
}
|
||||
} catch (error) {
|
||||
await prompts.log.warn(`Warning: Failed to remove ${ide}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
// Pass the newly-selected list as remainingIdes so cleanupByList skips
|
||||
// target_dir wipes for IDEs whose directory is still owned by a peer
|
||||
// (e.g. removing 'cursor' while 'gemini' remains — both share .agents/skills).
|
||||
await this.ideManager.cleanupByList(paths.projectRoot, toRemove, {
|
||||
remainingIdes: [...newlySelected],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -348,13 +343,14 @@ class Installer {
|
|||
return;
|
||||
}
|
||||
|
||||
for (const ide of validIdes) {
|
||||
const setupResult = await this.ideManager.setup(ide, paths.projectRoot, paths.bmadDir, {
|
||||
selectedModules: allModules || [],
|
||||
verbose: config.verbose,
|
||||
previousSkillIds,
|
||||
});
|
||||
const setupResults = await this.ideManager.setupBatch(validIdes, paths.projectRoot, paths.bmadDir, {
|
||||
selectedModules: allModules || [],
|
||||
verbose: config.verbose,
|
||||
previousSkillIds,
|
||||
});
|
||||
|
||||
for (const setupResult of setupResults) {
|
||||
const ide = setupResult.ide;
|
||||
if (setupResult.success) {
|
||||
addResult(ide, 'ok', setupResult.detail || '');
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -3,10 +3,14 @@ const path = require('node:path');
|
|||
const semver = require('semver');
|
||||
const fs = require('../fs-native');
|
||||
const prompts = require('../prompts');
|
||||
const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');
|
||||
const { getInstalledCanonicalIds, isBmadOwnedEntry } = require('../ide/shared/installed-skills');
|
||||
|
||||
const MIN_NATIVE_SKILLS_VERSION = '6.1.0';
|
||||
|
||||
const LEGACY_PATHS = [
|
||||
// Pre-v6.1.0 paths: BMAD used to install commands/workflows/etc in tool-specific dirs.
|
||||
// In v6.1.0 BMAD switched to native SKILL.md format.
|
||||
const LEGACY_COMMAND_PATHS = [
|
||||
'.agent/workflows',
|
||||
'.augment/commands',
|
||||
'.claude/commands',
|
||||
|
|
@ -33,6 +37,38 @@ const LEGACY_PATHS = [
|
|||
'.windsurf/workflows',
|
||||
];
|
||||
|
||||
// Skill paths that moved to the cross-tool .agents/skills/ standard.
|
||||
// Users upgrading from a prior install may have stale BMAD skills here that
|
||||
// the AI tool will load alongside the new ones, causing duplicates.
|
||||
const LEGACY_SKILL_PATHS = [
|
||||
'.augment/skills',
|
||||
'~/.augment/skills',
|
||||
'.codex/skills',
|
||||
'.crush/skills',
|
||||
'.cursor/skills',
|
||||
'~/.cursor/skills',
|
||||
'.gemini/skills',
|
||||
'~/.gemini/skills',
|
||||
'.github/skills',
|
||||
'~/.github/skills',
|
||||
'.kilocode/skills',
|
||||
'.kimi/skills',
|
||||
'~/.kimi/skills',
|
||||
'.opencode/skills',
|
||||
'~/.opencode/skills',
|
||||
'.pi/skills',
|
||||
'~/.pi/skills',
|
||||
'.roo/skills',
|
||||
'~/.roo/skills',
|
||||
'.rovodev/skills',
|
||||
'~/.rovodev/skills',
|
||||
'.windsurf/skills',
|
||||
'~/.windsurf/skills',
|
||||
'~/.codeium/windsurf/skills',
|
||||
];
|
||||
|
||||
const LEGACY_PATHS = [...LEGACY_COMMAND_PATHS, ...LEGACY_SKILL_PATHS];
|
||||
|
||||
function expandPath(p) {
|
||||
if (p === '~') return os.homedir();
|
||||
if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2));
|
||||
|
|
@ -45,15 +81,16 @@ function resolveLegacyPath(projectRoot, p) {
|
|||
}
|
||||
|
||||
async function findStaleLegacyDirs(projectRoot) {
|
||||
const bmadDir = path.join(projectRoot, BMAD_FOLDER_NAME);
|
||||
const canonicalIds = await getInstalledCanonicalIds(bmadDir);
|
||||
|
||||
const findings = [];
|
||||
for (const legacyPath of LEGACY_PATHS) {
|
||||
const resolved = resolveLegacyPath(projectRoot, legacyPath);
|
||||
if (!(await fs.pathExists(resolved))) continue;
|
||||
try {
|
||||
const entries = await fs.readdir(resolved);
|
||||
const bmadEntries = entries.filter(
|
||||
(e) => typeof e === 'string' && e.toLowerCase().startsWith('bmad') && !e.toLowerCase().startsWith('bmad-os-'),
|
||||
);
|
||||
const bmadEntries = entries.filter((e) => isBmadOwnedEntry(e, canonicalIds));
|
||||
if (bmadEntries.length > 0) {
|
||||
findings.push({ path: resolved, displayPath: legacyPath, count: bmadEntries.length });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ const yaml = require('yaml');
|
|||
const prompts = require('../prompts');
|
||||
const csv = require('csv-parse/sync');
|
||||
const { BMAD_FOLDER_NAME } = require('./shared/path-utils');
|
||||
const { getInstalledCanonicalIds, isBmadOwnedEntry } = require('./shared/installed-skills');
|
||||
|
||||
/**
|
||||
* Config-driven IDE setup handler
|
||||
|
|
@ -43,16 +44,20 @@ class ConfigDrivenIdeSetup {
|
|||
async detect(projectDir) {
|
||||
if (!this.configDir) return false;
|
||||
|
||||
const dir = path.join(projectDir || process.cwd(), this.configDir);
|
||||
if (await fs.pathExists(dir)) {
|
||||
try {
|
||||
const entries = await fs.readdir(dir);
|
||||
return entries.some((e) => typeof e === 'string' && e.startsWith('bmad'));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
const root = projectDir || process.cwd();
|
||||
const dir = path.join(root, this.configDir);
|
||||
if (!(await fs.pathExists(dir))) return false;
|
||||
|
||||
let entries;
|
||||
try {
|
||||
entries = await fs.readdir(dir);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
|
||||
const bmadDir = await this._findBmadDir(root);
|
||||
const canonicalIds = await getInstalledCanonicalIds(bmadDir);
|
||||
return entries.some((e) => isBmadOwnedEntry(e, canonicalIds));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -91,6 +96,12 @@ class ConfigDrivenIdeSetup {
|
|||
return { success: false, reason: 'no-config' };
|
||||
}
|
||||
|
||||
// When a peer platform in the same install batch owns this target_dir,
|
||||
// skip the skill write — the peer has already populated it.
|
||||
if (options.skipTarget) {
|
||||
return { success: true, results: { skills: 0, sharedTargetHandledByPeer: true } };
|
||||
}
|
||||
|
||||
if (this.installerConfig.target_dir) {
|
||||
return this.installToTarget(projectDir, bmadDir, this.installerConfig, options);
|
||||
}
|
||||
|
|
@ -236,6 +247,11 @@ class ConfigDrivenIdeSetup {
|
|||
await this.cleanupRovoDevPrompts(projectDir, options);
|
||||
}
|
||||
|
||||
// Skip target_dir cleanup when a peer platform owns this directory
|
||||
// (set during dedup'd install or when uninstalling one of several
|
||||
// platforms that share the same target_dir).
|
||||
if (options.skipTarget) return;
|
||||
|
||||
// Clean current target directory
|
||||
if (this.installerConfig?.target_dir) {
|
||||
await this.cleanupTarget(projectDir, this.installerConfig.target_dir, options, removalSet);
|
||||
|
|
@ -369,8 +385,8 @@ class ConfigDrivenIdeSetup {
|
|||
// Always preserve bmad-os-* utility skills regardless of cleanup mode
|
||||
if (entry.startsWith('bmad-os-')) continue;
|
||||
|
||||
// Surgical removal from set, or legacy prefix matching when set is null
|
||||
const shouldRemove = removalSet ? removalSet.has(entry) : entry.startsWith('bmad');
|
||||
// Surgical removal from set, or fallback to manifest+prefix detection when null
|
||||
const shouldRemove = removalSet ? removalSet.has(entry) : isBmadOwnedEntry(entry, null);
|
||||
|
||||
if (shouldRemove) {
|
||||
try {
|
||||
|
|
@ -533,10 +549,9 @@ class ConfigDrivenIdeSetup {
|
|||
try {
|
||||
if (await fs.pathExists(candidatePath)) {
|
||||
const entries = await fs.readdir(candidatePath);
|
||||
const hasBmad = entries.some(
|
||||
(e) => typeof e === 'string' && e.toLowerCase().startsWith('bmad') && !e.toLowerCase().startsWith('bmad-os-'),
|
||||
);
|
||||
if (hasBmad) {
|
||||
const ancestorBmadDir = await this._findBmadDir(current);
|
||||
const canonicalIds = await getInstalledCanonicalIds(ancestorBmadDir);
|
||||
if (entries.some((e) => isBmadOwnedEntry(e, canonicalIds))) {
|
||||
return candidatePath;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -160,8 +160,18 @@ class IdeManager {
|
|||
let detail = '';
|
||||
if (handlerResult && handlerResult.results) {
|
||||
const r = handlerResult.results;
|
||||
const count = r.skillDirectories || r.skills || 0;
|
||||
if (count > 0) detail = `${count} skills`;
|
||||
let count = r.skillDirectories || r.skills || 0;
|
||||
// Dedup'd platform: report the count its peer wrote so the user sees
|
||||
// a consistent picture across all platforms sharing the dir.
|
||||
if (count === 0 && r.sharedTargetHandledByPeer && options.sharedSkillCount) {
|
||||
count = options.sharedSkillCount;
|
||||
}
|
||||
const targetDir = handler.installerConfig?.target_dir || null;
|
||||
if (count > 0 && targetDir) {
|
||||
detail = `${count} skills → ${targetDir}`;
|
||||
} else if (count > 0) {
|
||||
detail = `${count} skills`;
|
||||
}
|
||||
}
|
||||
// Propagate handler's success status (default true for backward compat)
|
||||
const success = handlerResult?.success !== false;
|
||||
|
|
@ -172,6 +182,51 @@ class IdeManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run setup for multiple IDEs as a single batch.
|
||||
* Dedupes work when several selected platforms share the same target_dir:
|
||||
* the first platform owns the directory write, peers skip it.
|
||||
* @param {Array<string>} ideList - IDE names to set up
|
||||
* @param {string} projectDir
|
||||
* @param {string} bmadDir
|
||||
* @param {Object} [options] - Forwarded to each handler.setup
|
||||
* @returns {Promise<Array>} Per-IDE results
|
||||
*/
|
||||
async setupBatch(ideList, projectDir, bmadDir, options = {}) {
|
||||
await this.ensureInitialized();
|
||||
const results = [];
|
||||
// target_dir → { firstIde, skillCount } from the platform that actually wrote it
|
||||
const claimedTargets = new Map();
|
||||
|
||||
for (const ideName of ideList) {
|
||||
const handler = this.handlers.get(ideName.toLowerCase());
|
||||
if (!handler) {
|
||||
results.push(await this.setup(ideName, projectDir, bmadDir, options));
|
||||
continue;
|
||||
}
|
||||
|
||||
const target = handler.installerConfig?.target_dir || null;
|
||||
const claim = target ? claimedTargets.get(target) : null;
|
||||
const skipTarget = !!claim;
|
||||
|
||||
const result = await this.setup(ideName, projectDir, bmadDir, {
|
||||
...options,
|
||||
skipTarget,
|
||||
sharedWith: claim?.firstIde || null,
|
||||
sharedTarget: target,
|
||||
sharedSkillCount: claim?.skillCount || 0,
|
||||
});
|
||||
|
||||
if (target && !claim) {
|
||||
const writtenCount = result.handlerResult?.results?.skillDirectories || result.handlerResult?.results?.skills || 0;
|
||||
claimedTargets.set(target, { firstIde: ideName, skillCount: writtenCount });
|
||||
}
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup IDE configurations
|
||||
* @param {string} projectDir - Project directory
|
||||
|
|
@ -198,6 +253,8 @@ class IdeManager {
|
|||
* @param {string} projectDir - Project directory
|
||||
* @param {Array<string>} ideList - List of IDE names to clean up
|
||||
* @param {Object} [options] - Cleanup options passed through to handlers
|
||||
* options.remainingIdes - IDE names still installed after this cleanup; used
|
||||
* to skip target_dir wipe when a co-installed platform shares the dir.
|
||||
* @returns {Array} Results array
|
||||
*/
|
||||
async cleanupByList(projectDir, ideList, options = {}) {
|
||||
|
|
@ -211,13 +268,27 @@ class IdeManager {
|
|||
// Build lowercase lookup for case-insensitive matching
|
||||
const lowercaseHandlers = new Map([...this.handlers.entries()].map(([k, v]) => [k.toLowerCase(), v]));
|
||||
|
||||
// Resolve target_dirs for IDEs that will remain installed after this cleanup
|
||||
const remainingTargets = new Set();
|
||||
if (Array.isArray(options.remainingIdes)) {
|
||||
for (const remaining of options.remainingIdes) {
|
||||
const h = lowercaseHandlers.get(String(remaining).toLowerCase());
|
||||
const t = h?.installerConfig?.target_dir;
|
||||
if (t) remainingTargets.add(t);
|
||||
}
|
||||
}
|
||||
|
||||
for (const ideName of ideList) {
|
||||
const handler = lowercaseHandlers.get(ideName.toLowerCase());
|
||||
if (!handler) continue;
|
||||
|
||||
const target = handler.installerConfig?.target_dir || null;
|
||||
const skipTarget = target && remainingTargets.has(target);
|
||||
const cleanupOptions = skipTarget ? { ...options, skipTarget: true } : options;
|
||||
|
||||
try {
|
||||
await handler.cleanup(projectDir, options);
|
||||
results.push({ ide: ideName, success: true });
|
||||
await handler.cleanup(projectDir, cleanupOptions);
|
||||
results.push({ ide: ideName, success: true, skippedTarget: !!skipTarget });
|
||||
} catch (error) {
|
||||
results.push({ ide: ideName, success: false, error: error.message });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,20 @@
|
|||
# Paths verified against each tool's primary docs as of 2026-04-25.
|
||||
|
||||
platforms:
|
||||
adal:
|
||||
name: "AdaL"
|
||||
preferred: false
|
||||
installer:
|
||||
target_dir: .adal/skills
|
||||
global_target_dir: ~/.adal/skills
|
||||
|
||||
amp:
|
||||
name: "Sourcegraph Amp"
|
||||
preferred: false
|
||||
installer:
|
||||
target_dir: .agents/skills
|
||||
global_target_dir: ~/.config/agents/skills
|
||||
|
||||
antigravity:
|
||||
name: "Google Antigravity"
|
||||
preferred: false
|
||||
|
|
@ -28,6 +42,13 @@ platforms:
|
|||
target_dir: .agents/skills
|
||||
global_target_dir: ~/.agents/skills
|
||||
|
||||
bob:
|
||||
name: "IBM Bob"
|
||||
preferred: false
|
||||
installer:
|
||||
target_dir: .bob/skills
|
||||
global_target_dir: ~/.bob/skills
|
||||
|
||||
claude-code:
|
||||
name: "Claude Code"
|
||||
preferred: true
|
||||
|
|
@ -44,7 +65,7 @@ platforms:
|
|||
|
||||
codex:
|
||||
name: "Codex"
|
||||
preferred: false
|
||||
preferred: true
|
||||
installer:
|
||||
target_dir: .agents/skills
|
||||
global_target_dir: ~/.codex/skills
|
||||
|
|
@ -56,6 +77,20 @@ platforms:
|
|||
target_dir: .codebuddy/skills
|
||||
global_target_dir: ~/.codebuddy/skills
|
||||
|
||||
command-code:
|
||||
name: "Command Code"
|
||||
preferred: false
|
||||
installer:
|
||||
target_dir: .agents/skills
|
||||
global_target_dir: ~/.agents/skills
|
||||
|
||||
cortex:
|
||||
name: "Snowflake Cortex Code"
|
||||
preferred: false
|
||||
installer:
|
||||
target_dir: .cortex/skills
|
||||
global_target_dir: ~/.snowflake/cortex/skills
|
||||
|
||||
crush:
|
||||
name: "Crush"
|
||||
preferred: false
|
||||
|
|
@ -70,6 +105,20 @@ platforms:
|
|||
target_dir: .agents/skills
|
||||
global_target_dir: ~/.agents/skills
|
||||
|
||||
droid:
|
||||
name: "Factory Droid"
|
||||
preferred: false
|
||||
installer:
|
||||
target_dir: .factory/skills
|
||||
global_target_dir: ~/.factory/skills
|
||||
|
||||
firebender:
|
||||
name: "Firebender"
|
||||
preferred: false
|
||||
installer:
|
||||
target_dir: .firebender/skills
|
||||
global_target_dir: ~/.agents/skills
|
||||
|
||||
gemini:
|
||||
name: "Gemini CLI"
|
||||
preferred: false
|
||||
|
|
@ -79,11 +128,18 @@ platforms:
|
|||
|
||||
github-copilot:
|
||||
name: "GitHub Copilot"
|
||||
preferred: false
|
||||
preferred: true
|
||||
installer:
|
||||
target_dir: .agents/skills
|
||||
global_target_dir: ~/.agents/skills
|
||||
|
||||
goose:
|
||||
name: "Block Goose"
|
||||
preferred: false
|
||||
installer:
|
||||
target_dir: .agents/skills
|
||||
global_target_dir: ~/.config/agents/skills
|
||||
|
||||
iflow:
|
||||
name: "iFlow"
|
||||
preferred: false
|
||||
|
|
@ -119,12 +175,47 @@ platforms:
|
|||
target_dir: .kiro/skills
|
||||
global_target_dir: ~/.kiro/skills
|
||||
|
||||
kode:
|
||||
name: "Kode"
|
||||
preferred: false
|
||||
installer:
|
||||
target_dir: .kode/skills
|
||||
global_target_dir: ~/.kode/skills
|
||||
|
||||
mistral-vibe:
|
||||
name: "Mistral Vibe"
|
||||
preferred: false
|
||||
installer:
|
||||
target_dir: .agents/skills
|
||||
global_target_dir: ~/.vibe/skills
|
||||
|
||||
mux:
|
||||
name: "Mux"
|
||||
preferred: false
|
||||
installer:
|
||||
target_dir: .agents/skills
|
||||
global_target_dir: ~/.agents/skills
|
||||
|
||||
neovate:
|
||||
name: "Neovate"
|
||||
preferred: false
|
||||
installer:
|
||||
target_dir: .neovate/skills
|
||||
global_target_dir: ~/.neovate/skills
|
||||
|
||||
ona:
|
||||
name: "Ona"
|
||||
preferred: false
|
||||
installer:
|
||||
target_dir: .ona/skills
|
||||
|
||||
openclaw:
|
||||
name: "OpenClaw"
|
||||
preferred: false
|
||||
installer:
|
||||
target_dir: .agents/skills
|
||||
global_target_dir: ~/.agents/skills
|
||||
|
||||
opencode:
|
||||
name: "OpenCode"
|
||||
preferred: false
|
||||
|
|
@ -132,6 +223,13 @@ platforms:
|
|||
target_dir: .agents/skills
|
||||
global_target_dir: ~/.agents/skills
|
||||
|
||||
openhands:
|
||||
name: "OpenHands"
|
||||
preferred: false
|
||||
installer:
|
||||
target_dir: .agents/skills
|
||||
global_target_dir: ~/.agents/skills
|
||||
|
||||
pi:
|
||||
name: "Pi"
|
||||
preferred: false
|
||||
|
|
@ -139,6 +237,13 @@ platforms:
|
|||
target_dir: .agents/skills
|
||||
global_target_dir: ~/.agents/skills
|
||||
|
||||
pochi:
|
||||
name: "Pochi"
|
||||
preferred: false
|
||||
installer:
|
||||
target_dir: .agents/skills
|
||||
global_target_dir: ~/.agents/skills
|
||||
|
||||
qoder:
|
||||
name: "Qoder"
|
||||
preferred: false
|
||||
|
|
@ -153,6 +258,12 @@ platforms:
|
|||
target_dir: .qwen/skills
|
||||
global_target_dir: ~/.qwen/skills
|
||||
|
||||
replit:
|
||||
name: "Replit Agent"
|
||||
preferred: false
|
||||
installer:
|
||||
target_dir: .agents/skills
|
||||
|
||||
roo:
|
||||
name: "Roo Code"
|
||||
preferred: false
|
||||
|
|
@ -173,9 +284,23 @@ platforms:
|
|||
installer:
|
||||
target_dir: .trae/skills
|
||||
|
||||
warp:
|
||||
name: "Warp"
|
||||
preferred: false
|
||||
installer:
|
||||
target_dir: .agents/skills
|
||||
global_target_dir: ~/.agents/skills
|
||||
|
||||
windsurf:
|
||||
name: "Windsurf"
|
||||
preferred: false
|
||||
installer:
|
||||
target_dir: .agents/skills
|
||||
global_target_dir: ~/.agents/skills
|
||||
|
||||
zencoder:
|
||||
name: "Zencoder"
|
||||
preferred: false
|
||||
installer:
|
||||
target_dir: .zencoder/skills
|
||||
global_target_dir: ~/.zencoder/skills
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
const path = require('node:path');
|
||||
const fs = require('../../fs-native');
|
||||
const csv = require('csv-parse/sync');
|
||||
|
||||
/**
|
||||
* Read the global skill-manifest.csv and return the set of canonicalIds.
|
||||
* These define which directory entries in a target_dir are BMAD-owned, regardless
|
||||
* of whether they happen to start with "bmad-" (custom modules can ship skills
|
||||
* with any prefix, e.g. "fred-cool-skill").
|
||||
*
|
||||
* @param {string} bmadDir - Path to the _bmad install directory
|
||||
* @returns {Promise<Set<string>>} Set of canonicalIds, or empty set if manifest missing
|
||||
*/
|
||||
async function getInstalledCanonicalIds(bmadDir) {
|
||||
const ids = new Set();
|
||||
if (!bmadDir) return ids;
|
||||
|
||||
const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv');
|
||||
if (!(await fs.pathExists(csvPath))) return ids;
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(csvPath, 'utf8');
|
||||
const records = csv.parse(content, { columns: true, skip_empty_lines: true });
|
||||
for (const record of records) {
|
||||
if (record.canonicalId) ids.add(record.canonicalId);
|
||||
}
|
||||
} catch {
|
||||
// Unreadable/invalid manifest — treat as no info
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether a directory entry is BMAD-owned.
|
||||
* Prefers the manifest's canonicalIds; falls back to the legacy "bmad" prefix
|
||||
* when no manifest is available (early install, ancestor lookup with no bmad dir).
|
||||
*
|
||||
* @param {string} entry - Directory entry name
|
||||
* @param {Set<string>|null} canonicalIds - From getInstalledCanonicalIds, or null
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isBmadOwnedEntry(entry, canonicalIds) {
|
||||
if (!entry || typeof entry !== 'string') return false;
|
||||
if (entry.toLowerCase().startsWith('bmad-os-')) return false;
|
||||
if (canonicalIds && canonicalIds.size > 0) return canonicalIds.has(entry);
|
||||
return entry.toLowerCase().startsWith('bmad');
|
||||
}
|
||||
|
||||
module.exports = { getInstalledCanonicalIds, isBmadOwnedEntry };
|
||||
Loading…
Reference in New Issue