feat(installer): consolidate to .agents/skills and add global_target_dir for all platforms

Updates platform-codes.yaml against verified primary docs for all 24 supported
platforms. 14 platforms (auggie, codex, crush, cursor, gemini, github-copilot,
kilo, kimi-code, opencode, pi, roo, rovo-dev, windsurf) move their project
target_dir to the cross-tool .agents/skills/ standard. Junie moves from the
broken .agents/skills/ to its own .junie/skills/ per JetBrains docs.

Adds global_target_dir to every platform: 11 share ~/.agents/skills/, Crush
uses XDG ~/.config/agents/skills/, Codex global stays ~/.codex/skills/, the
rest are tool-specific. Ona and Trae omit global (no documented home path).

Note: installer logic does not yet dedupe writes for platforms sharing a
target_dir — users installing multiple .agents/skills/ tools together will
overwrite the same files (harmless on install, but uninstalling one clears
the dir for the others). Coordination logic is the next step.
This commit is contained in:
Brian Madison 2026-04-25 19:41:25 -05:00
parent 08a3e88b86
commit 5ff534df94
2 changed files with 63 additions and 36 deletions

View File

@ -139,7 +139,7 @@ async function runTests() {
const platformCodes = await loadPlatformCodes(); const platformCodes = await loadPlatformCodes();
const windsurfInstaller = platformCodes.platforms.windsurf?.installer; const windsurfInstaller = platformCodes.platforms.windsurf?.installer;
assert(windsurfInstaller?.target_dir === '.windsurf/skills', 'Windsurf target_dir uses native skills path'); assert(windsurfInstaller?.target_dir === '.agents/skills', 'Windsurf target_dir uses native skills path');
const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-windsurf-test-')); const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-windsurf-test-'));
const installedBmadDir = await createTestBmadFixture(); const installedBmadDir = await createTestBmadFixture();
@ -153,7 +153,7 @@ async function runTests() {
assert(result.success === true, 'Windsurf setup succeeds against temp project'); assert(result.success === true, 'Windsurf setup succeeds against temp project');
const skillFile = path.join(tempProjectDir, '.windsurf', 'skills', 'bmad-master', 'SKILL.md'); const skillFile = path.join(tempProjectDir, '.agents', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile), 'Windsurf install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile), 'Windsurf install writes SKILL.md directory output');
await fs.remove(tempProjectDir); await fs.remove(tempProjectDir);
@ -244,7 +244,7 @@ async function runTests() {
const platformCodes = await loadPlatformCodes(); const platformCodes = await loadPlatformCodes();
const auggieInstaller = platformCodes.platforms.auggie?.installer; const auggieInstaller = platformCodes.platforms.auggie?.installer;
assert(auggieInstaller?.target_dir === '.augment/skills', 'Auggie target_dir uses native skills path'); assert(auggieInstaller?.target_dir === '.agents/skills', 'Auggie target_dir uses native skills path');
assert( assert(
auggieInstaller?.ancestor_conflict_check !== true, auggieInstaller?.ancestor_conflict_check !== true,
@ -263,7 +263,7 @@ async function runTests() {
assert(result.success === true, 'Auggie setup succeeds against temp project'); assert(result.success === true, 'Auggie setup succeeds against temp project');
const skillFile = path.join(tempProjectDir, '.augment', 'skills', 'bmad-master', 'SKILL.md'); const skillFile = path.join(tempProjectDir, '.agents', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile), 'Auggie install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile), 'Auggie install writes SKILL.md directory output');
await fs.remove(tempProjectDir); await fs.remove(tempProjectDir);
@ -284,7 +284,7 @@ async function runTests() {
const platformCodes = await loadPlatformCodes(); const platformCodes = await loadPlatformCodes();
const opencodeInstaller = platformCodes.platforms.opencode?.installer; const opencodeInstaller = platformCodes.platforms.opencode?.installer;
assert(opencodeInstaller?.target_dir === '.opencode/skills', 'OpenCode target_dir uses native skills path'); assert(opencodeInstaller?.target_dir === '.agents/skills', 'OpenCode target_dir uses native skills path');
const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-opencode-test-')); const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-opencode-test-'));
const installedBmadDir = await createTestBmadFixture(); const installedBmadDir = await createTestBmadFixture();
@ -298,7 +298,7 @@ async function runTests() {
assert(result.success === true, 'OpenCode setup succeeds against temp project'); assert(result.success === true, 'OpenCode setup succeeds against temp project');
const skillFile = path.join(tempProjectDir, '.opencode', 'skills', 'bmad-master', 'SKILL.md'); const skillFile = path.join(tempProjectDir, '.agents', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile), 'OpenCode install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile), 'OpenCode install writes SKILL.md directory output');
await fs.remove(tempProjectDir); await fs.remove(tempProjectDir);
@ -403,7 +403,7 @@ async function runTests() {
const platformCodes13 = await loadPlatformCodes(); const platformCodes13 = await loadPlatformCodes();
const cursorInstaller = platformCodes13.platforms.cursor?.installer; const cursorInstaller = platformCodes13.platforms.cursor?.installer;
assert(cursorInstaller?.target_dir === '.cursor/skills', 'Cursor target_dir uses native skills path'); assert(cursorInstaller?.target_dir === '.agents/skills', 'Cursor target_dir uses native skills path');
assert(!cursorInstaller?.ancestor_conflict_check, 'Cursor installer does not enable ancestor conflict checks'); assert(!cursorInstaller?.ancestor_conflict_check, 'Cursor installer does not enable ancestor conflict checks');
@ -419,7 +419,7 @@ async function runTests() {
assert(result13c.success === true, 'Cursor setup succeeds against temp project'); assert(result13c.success === true, 'Cursor setup succeeds against temp project');
const skillFile13c = path.join(tempProjectDir13c, '.cursor', 'skills', 'bmad-master', 'SKILL.md'); const skillFile13c = path.join(tempProjectDir13c, '.agents', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile13c), 'Cursor install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile13c), 'Cursor install writes SKILL.md directory output');
// Verify name frontmatter matches directory name // Verify name frontmatter matches directory name
@ -445,7 +445,7 @@ async function runTests() {
const platformCodes13 = await loadPlatformCodes(); const platformCodes13 = await loadPlatformCodes();
const rooInstaller = platformCodes13.platforms.roo?.installer; const rooInstaller = platformCodes13.platforms.roo?.installer;
assert(rooInstaller?.target_dir === '.roo/skills', 'Roo target_dir uses native skills path'); assert(rooInstaller?.target_dir === '.agents/skills', 'Roo target_dir uses native skills path');
const tempProjectDir13 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-roo-test-')); const tempProjectDir13 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-roo-test-'));
const installedBmadDir13 = await createTestBmadFixture(); const installedBmadDir13 = await createTestBmadFixture();
@ -459,7 +459,7 @@ async function runTests() {
assert(result13.success === true, 'Roo setup succeeds against temp project'); assert(result13.success === true, 'Roo setup succeeds against temp project');
const skillFile13 = path.join(tempProjectDir13, '.roo', 'skills', 'bmad-master', 'SKILL.md'); const skillFile13 = path.join(tempProjectDir13, '.agents', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile13), 'Roo install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile13), 'Roo install writes SKILL.md directory output');
// Verify name frontmatter matches directory name (Roo constraint: lowercase alphanumeric + hyphens) // Verify name frontmatter matches directory name (Roo constraint: lowercase alphanumeric + hyphens)
@ -503,7 +503,7 @@ async function runTests() {
const platformCodes17 = await loadPlatformCodes(); const platformCodes17 = await loadPlatformCodes();
const copilotInstaller = platformCodes17.platforms['github-copilot']?.installer; const copilotInstaller = platformCodes17.platforms['github-copilot']?.installer;
assert(copilotInstaller?.target_dir === '.github/skills', 'GitHub Copilot target_dir uses native skills path'); assert(copilotInstaller?.target_dir === '.agents/skills', 'GitHub Copilot target_dir uses native skills path');
const tempProjectDir17 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-copilot-test-')); const tempProjectDir17 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-copilot-test-'));
const installedBmadDir17 = await createTestBmadFixture(); const installedBmadDir17 = await createTestBmadFixture();
@ -524,7 +524,7 @@ async function runTests() {
assert(result17.success === true, 'GitHub Copilot setup succeeds against temp project'); assert(result17.success === true, 'GitHub Copilot setup succeeds against temp project');
const skillFile17 = path.join(tempProjectDir17, '.github', 'skills', 'bmad-master', 'SKILL.md'); const skillFile17 = path.join(tempProjectDir17, '.agents', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile17), 'GitHub Copilot install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile17), 'GitHub Copilot install writes SKILL.md directory output');
// Verify name frontmatter matches directory name // Verify name frontmatter matches directory name
@ -657,7 +657,7 @@ async function runTests() {
const platformCodes20 = await loadPlatformCodes(); const platformCodes20 = await loadPlatformCodes();
const crushInstaller = platformCodes20.platforms.crush?.installer; const crushInstaller = platformCodes20.platforms.crush?.installer;
assert(crushInstaller?.target_dir === '.crush/skills', 'Crush target_dir uses native skills path'); assert(crushInstaller?.target_dir === '.agents/skills', 'Crush target_dir uses native skills path');
const tempProjectDir20 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-crush-test-')); const tempProjectDir20 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-crush-test-'));
const installedBmadDir20 = await createTestBmadFixture(); const installedBmadDir20 = await createTestBmadFixture();
@ -671,7 +671,7 @@ async function runTests() {
assert(result20.success === true, 'Crush setup succeeds against temp project'); assert(result20.success === true, 'Crush setup succeeds against temp project');
const skillFile20 = path.join(tempProjectDir20, '.crush', 'skills', 'bmad-master', 'SKILL.md'); const skillFile20 = path.join(tempProjectDir20, '.agents', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile20), 'Crush install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile20), 'Crush install writes SKILL.md directory output');
const skillContent20 = await fs.readFile(skillFile20, 'utf8'); const skillContent20 = await fs.readFile(skillFile20, 'utf8');
@ -753,7 +753,7 @@ async function runTests() {
assert(!kiloConfig22?.suspended, 'KiloCoder is not suspended'); assert(!kiloConfig22?.suspended, 'KiloCoder is not suspended');
assert(kiloConfig22?.installer?.target_dir === '.kilocode/skills', 'KiloCoder target_dir uses native skills path'); assert(kiloConfig22?.installer?.target_dir === '.agents/skills', 'KiloCoder target_dir uses native skills path');
const ideManager22 = new IdeManager(); const ideManager22 = new IdeManager();
await ideManager22.ensureInitialized(); await ideManager22.ensureInitialized();
@ -775,7 +775,7 @@ async function runTests() {
assert(result22.success === true, 'KiloCoder setup succeeds against temp project'); assert(result22.success === true, 'KiloCoder setup succeeds against temp project');
const skillFile22 = path.join(tempProjectDir22, '.kilocode', 'skills', 'bmad-master', 'SKILL.md'); const skillFile22 = path.join(tempProjectDir22, '.agents', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile22), 'KiloCoder install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile22), 'KiloCoder install writes SKILL.md directory output');
const skillContent22 = await fs.readFile(skillFile22, 'utf8'); const skillContent22 = await fs.readFile(skillFile22, 'utf8');
@ -808,7 +808,7 @@ async function runTests() {
const platformCodes23 = await loadPlatformCodes(); const platformCodes23 = await loadPlatformCodes();
const geminiInstaller = platformCodes23.platforms.gemini?.installer; const geminiInstaller = platformCodes23.platforms.gemini?.installer;
assert(geminiInstaller?.target_dir === '.gemini/skills', 'Gemini target_dir uses native skills path'); assert(geminiInstaller?.target_dir === '.agents/skills', 'Gemini target_dir uses native skills path');
const tempProjectDir23 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-gemini-test-')); const tempProjectDir23 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-gemini-test-'));
const installedBmadDir23 = await createTestBmadFixture(); const installedBmadDir23 = await createTestBmadFixture();
@ -822,7 +822,7 @@ async function runTests() {
assert(result23.success === true, 'Gemini setup succeeds against temp project'); assert(result23.success === true, 'Gemini setup succeeds against temp project');
const skillFile23 = path.join(tempProjectDir23, '.gemini', 'skills', 'bmad-master', 'SKILL.md'); const skillFile23 = path.join(tempProjectDir23, '.agents', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile23), 'Gemini install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile23), 'Gemini install writes SKILL.md directory output');
const skillContent23 = await fs.readFile(skillFile23, 'utf8'); const skillContent23 = await fs.readFile(skillFile23, 'utf8');
@ -935,7 +935,7 @@ async function runTests() {
const platformCodes26 = await loadPlatformCodes(); const platformCodes26 = await loadPlatformCodes();
const rovoInstaller = platformCodes26.platforms['rovo-dev']?.installer; const rovoInstaller = platformCodes26.platforms['rovo-dev']?.installer;
assert(rovoInstaller?.target_dir === '.rovodev/skills', 'Rovo Dev target_dir uses native skills path'); assert(rovoInstaller?.target_dir === '.agents/skills', 'Rovo Dev target_dir uses native skills path');
const tempProjectDir26 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-rovodev-test-')); const tempProjectDir26 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-rovodev-test-'));
const installedBmadDir26 = await createTestBmadFixture(); const installedBmadDir26 = await createTestBmadFixture();
@ -961,7 +961,7 @@ async function runTests() {
assert(result26.success === true, 'Rovo Dev setup succeeds against temp project'); assert(result26.success === true, 'Rovo Dev setup succeeds against temp project');
const skillFile26 = path.join(tempProjectDir26, '.rovodev', 'skills', 'bmad-master', 'SKILL.md'); const skillFile26 = path.join(tempProjectDir26, '.agents', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile26), 'Rovo Dev install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile26), 'Rovo Dev install writes SKILL.md directory output');
// Verify name frontmatter matches directory name // Verify name frontmatter matches directory name
@ -1070,7 +1070,7 @@ async function runTests() {
const platformCodes28 = await loadPlatformCodes(); const platformCodes28 = await loadPlatformCodes();
const piInstaller = platformCodes28.platforms.pi?.installer; const piInstaller = platformCodes28.platforms.pi?.installer;
assert(piInstaller?.target_dir === '.pi/skills', 'Pi target_dir uses native skills path'); assert(piInstaller?.target_dir === '.agents/skills', 'Pi target_dir uses native skills path');
tempProjectDir28 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-pi-test-')); tempProjectDir28 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-pi-test-'));
installedBmadDir28 = await createTestBmadFixture(); installedBmadDir28 = await createTestBmadFixture();
@ -1100,7 +1100,7 @@ async function runTests() {
const detectedAfter28 = await ideManager28.detectInstalledIdes(tempProjectDir28); const detectedAfter28 = await ideManager28.detectInstalledIdes(tempProjectDir28);
assert(detectedAfter28.includes('pi'), 'Pi is detected after install'); assert(detectedAfter28.includes('pi'), 'Pi is detected after install');
const skillFile28 = path.join(tempProjectDir28, '.pi', 'skills', 'bmad-master', 'SKILL.md'); const skillFile28 = path.join(tempProjectDir28, '.agents', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile28), 'Pi install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile28), 'Pi install writes SKILL.md directory output');
// Parse YAML frontmatter between --- markers // Parse YAML frontmatter between --- markers

View File

@ -5,8 +5,13 @@
# preferred: Whether shown as a recommended option on install # preferred: Whether shown as a recommended option on install
# suspended: (optional) Message explaining why install is blocked # suspended: (optional) Message explaining why install is blocked
# installer: # installer:
# target_dir: Directory where skill directories are installed # target_dir: Directory where skill directories are installed (project/workspace)
# global_target_dir: (optional) User-home directory for global install
# ancestor_conflict_check: (optional) Refuse install when ancestor dir has BMAD files # ancestor_conflict_check: (optional) Refuse install when ancestor dir has BMAD files
#
# Multiple platforms may share the same target_dir or global_target_dir — many tools
# read from the shared `.agents/skills/` and `~/.agents/skills/` cross-tool standard.
# Paths verified against each tool's primary docs as of 2026-04-25.
platforms: platforms:
antigravity: antigravity:
@ -14,90 +19,105 @@ platforms:
preferred: false preferred: false
installer: installer:
target_dir: .agent/skills target_dir: .agent/skills
global_target_dir: ~/.gemini/antigravity/skills
auggie: auggie:
name: "Auggie" name: "Auggie"
preferred: false preferred: false
installer: installer:
target_dir: .augment/skills target_dir: .agents/skills
global_target_dir: ~/.agents/skills
claude-code: claude-code:
name: "Claude Code" name: "Claude Code"
preferred: true preferred: true
installer: installer:
target_dir: .claude/skills target_dir: .claude/skills
global_target_dir: ~/.claude/skills
cline: cline:
name: "Cline" name: "Cline"
preferred: false preferred: false
installer: installer:
target_dir: .cline/skills target_dir: .cline/skills
global_target_dir: ~/.cline/skills
codex: codex:
name: "Codex" name: "Codex"
preferred: false preferred: false
installer: installer:
target_dir: .agents/skills target_dir: .agents/skills
global_target_dir: ~/.codex/skills
codebuddy: codebuddy:
name: "CodeBuddy" name: "CodeBuddy"
preferred: false preferred: false
installer: installer:
target_dir: .codebuddy/skills target_dir: .codebuddy/skills
global_target_dir: ~/.codebuddy/skills
crush: crush:
name: "Crush" name: "Crush"
preferred: false preferred: false
installer: installer:
target_dir: .crush/skills target_dir: .agents/skills
global_target_dir: ~/.config/agents/skills
cursor: cursor:
name: "Cursor" name: "Cursor"
preferred: true preferred: true
installer: installer:
target_dir: .cursor/skills target_dir: .agents/skills
global_target_dir: ~/.agents/skills
gemini: gemini:
name: "Gemini CLI" name: "Gemini CLI"
preferred: false preferred: false
installer: installer:
target_dir: .gemini/skills target_dir: .agents/skills
global_target_dir: ~/.agents/skills
github-copilot: github-copilot:
name: "GitHub Copilot" name: "GitHub Copilot"
preferred: false preferred: false
installer: installer:
target_dir: .github/skills target_dir: .agents/skills
global_target_dir: ~/.agents/skills
iflow: iflow:
name: "iFlow" name: "iFlow"
preferred: false preferred: false
installer: installer:
target_dir: .iflow/skills target_dir: .iflow/skills
global_target_dir: ~/.iflow/skills
junie: junie:
name: "Junie" name: "Junie"
preferred: false preferred: false
installer: installer:
target_dir: .agents/skills target_dir: .junie/skills
global_target_dir: ~/.junie/skills
kilo: kilo:
name: "KiloCoder" name: "KiloCoder"
preferred: false preferred: false
installer: installer:
target_dir: .kilocode/skills target_dir: .agents/skills
global_target_dir: ~/.kilocode/skills
kimi-code: kimi-code:
name: "Kimi Code" name: "Kimi Code"
preferred: false preferred: false
installer: installer:
target_dir: .kimi/skills target_dir: .agents/skills
global_target_dir: ~/.agents/skills
kiro: kiro:
name: "Kiro" name: "Kiro"
preferred: false preferred: false
installer: installer:
target_dir: .kiro/skills target_dir: .kiro/skills
global_target_dir: ~/.kiro/skills
ona: ona:
name: "Ona" name: "Ona"
@ -109,37 +129,43 @@ platforms:
name: "OpenCode" name: "OpenCode"
preferred: false preferred: false
installer: installer:
target_dir: .opencode/skills target_dir: .agents/skills
global_target_dir: ~/.agents/skills
pi: pi:
name: "Pi" name: "Pi"
preferred: false preferred: false
installer: installer:
target_dir: .pi/skills target_dir: .agents/skills
global_target_dir: ~/.agents/skills
qoder: qoder:
name: "Qoder" name: "Qoder"
preferred: false preferred: false
installer: installer:
target_dir: .qoder/skills target_dir: .qoder/skills
global_target_dir: ~/.qoder/skills
qwen: qwen:
name: "QwenCoder" name: "QwenCoder"
preferred: false preferred: false
installer: installer:
target_dir: .qwen/skills target_dir: .qwen/skills
global_target_dir: ~/.qwen/skills
roo: roo:
name: "Roo Code" name: "Roo Code"
preferred: false preferred: false
installer: installer:
target_dir: .roo/skills target_dir: .agents/skills
global_target_dir: ~/.agents/skills
rovo-dev: rovo-dev:
name: "Rovo Dev" name: "Rovo Dev"
preferred: false preferred: false
installer: installer:
target_dir: .rovodev/skills target_dir: .agents/skills
global_target_dir: ~/.agents/skills
trae: trae:
name: "Trae" name: "Trae"
@ -151,4 +177,5 @@ platforms:
name: "Windsurf" name: "Windsurf"
preferred: false preferred: false
installer: installer:
target_dir: .windsurf/skills target_dir: .agents/skills
global_target_dir: ~/.agents/skills