Compare commits

...

10 Commits

Author SHA1 Message Date
Nikolas de Hor ac96f19f88
Merge 1a85069b75 into 04cfde1454 2026-04-26 13:21:30 -03:00
Brian 04cfde1454
fix(installer): mirror launch channel as default for external modules (#2321)
* fix(installer): mirror launch channel as default for external modules

When the user runs `npx bmad-method@next install`, the installer itself
runs from a prerelease, but the interactive channel gate previously hardcoded
"(all stable)" — defaulting tea/community modules to stable while bmad-method
itself was on next. The bleeding-edge launch did not flow through.

Detect the installer's own version via semver.prerelease() and default the
gate (and per-module picker) to match — "all next" for prerelease launches,
"all stable" for stable. Users keep full control: hit "n" to customize per
module, or pass explicit --channel / --pin / --next flags to override.

* fix(installer): seed channelOptions before module picker, not gate

CodeRabbit caught a label/install mismatch in the previous approach: the
module picker resolves version labels via decideChannelForModule, which runs
before _interactiveChannelGate. With channelOptions.global still null at
picker time, labels rendered from stable tags — then the gate flipped global
to 'next' and externals installed from main HEAD. Net effect on @next launches:
"tea (v1.6.0)" in the picker, but install pulled HEAD.

Move the launch detection up into promptInstall, immediately after
parseChannelOptions. Seeding channelOptions.global = 'next' before the picker
makes labels resolve from main HEAD (matching the install) and lets the
existing gate's haveFlagIntent check skip cleanly — the @next user already
declared their intent by typing it. Per-module customization remains available
via --pin / --next / --channel flags, same as for any pre-set global.
2026-04-26 10:54:38 -05:00
Brian 7baa30c567
fix(publish): advance @next dist-tag after stable release (#2320)
* fix(publish): advance @next dist-tag after stable release

When a stable release publishes via workflow_dispatch, @latest can leapfrog
the existing @next prerelease (e.g. latest=6.5.0 while next=6.4.1-next.0),
turning `npx bmad-method@next install` into a silent downgrade until the
next qualifying push to main republishes a fresh -next.0.

- publish.yaml: after stable publish, repoint @next at the just-published
  stable version. The existing derive-prerelease step picks max(latest, next)
  as its base, so subsequent push-driven prereleases bump from there.
- bmad-cli.js: checkForUpdate was querying the @beta dist-tag (which this
  package does not use). Replace string-matching with semver.prerelease()
  and query @next for prerelease users.

* fix(publish): harden next-tag advance step and broaden path filter

- continue-on-error on the dist-tag advance: failure leaves @next stale
  until the next push-driven prerelease, which is recoverable; failing the
  job after a successful publish + git tag + GH release is not.
- Status echo so release-log triage can confirm the advance ran.
- Add removals.txt to the push-trigger path filter. Installer-affecting
  changes outside src/** (like the post-6.5.0 removals.txt fix) should
  still trigger a fresh -next.0 publish.
2026-04-26 10:30:41 -05:00
Brian 88b9a1c842
fix(installer): remove pre-v6.2.0 wrapper skills on update (closes #2309) (#2315)
Adds 32 entries to removals.txt covering the module-prefixed wrapper
skill names used pre-v6.2.0 (bmad-bmm-* and bmad-agent-bmm-*). Users
upgrading from v6.0.x / v6.1.x had these installed in their IDE skill
directories, but the v6.2.0 architecture switch dropped the module
prefix and the cleanup never knew the old names. Stale wrappers stayed
behind alongside the new self-contained skills, causing duplicates and
broken-file errors when invoked (referenced files no longer exist).

The removals.txt entries get added to the cleanup removalSet on every
install/update, so the next install run for an upgrading user removes
the stale wrappers automatically.
2026-04-25 22:08:44 -05:00
github-actions[bot] 69cbeb4d07 chore(release): v6.5.0 [skip ci] 2026-04-26 02:25:31 +00:00
Brian 1d35acfd84
docs: add v6.5.0 changelog entry (#2314) 2026-04-25 21:24:43 -05:00
Brian 01cc32540b
feat(installer): expand to 42 platforms with shared target_dir coordination (#2313)
* refactor(installer): replace legacy_targets auto-cleanup with upgrade warnings

Removes the legacy_targets YAML field and its install-time auto-migration
of pre-v6.1.0 directories (.claude/commands, .opencode/agents, etc.). On
install, surface a warning instead: read manifest version and scan 24
known legacy paths, then print rm -rf commands the user can run themselves.
Also deletes orphan tools/platform-codes.yaml (never loaded by any code)
and fixes a stale URL in the cs translation.

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

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

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

Note: installer logic does not yet dedupe writes for platforms sharing a
target_dir — users installing multiple .agents/skills/ tools together will
overwrite the same files (harmless on install, but uninstalling one clears
the dir for the others). Coordination logic is the next step.

* feat(installer): add 18 new platforms, dedup shared target_dir, ownership-aware cleanup

Adds 18 platforms from the verified Vercel list (adal, amp, bob, command-code,
cortex, droid, firebender, goose, kode, mistral-vibe, mux, neovate, openclaw,
openhands, pochi, replit, warp, zencoder). Marks codex and github-copilot as
preferred alongside claude-code and cursor.

Coordination for platforms sharing a target_dir:

- IdeManager.setupBatch dedups skill writes when multiple selected platforms
  point at the same target_dir (e.g. .agents/skills/). The first platform
  writes, peers skip the redundant wipe-and-rewrite. Result reports the same
  count and target dir for every member so the install summary is consistent.

- IdeManager.cleanupByList accepts remainingIdes; when removing one platform
  from a shared dir while another co-installed platform still owns it, the
  target_dir wipe is skipped. Platform-specific hooks (copilot markers, kilo
  modes, rovodev prompts) still run.

- _setupIdes uses setupBatch; _removeDeselectedIdes passes remainingIdes so
  partial reconfigure preserves shared skills.

Skill ownership now uses skill-manifest.csv canonicalIds, not the bmad- prefix.
This unblocks custom modules that ship skills with non-bmad names (e.g.
fred-cool-skill). Affected sites:

- _config-driven.detect: reads canonicalIds from the project's bmadDir
- _config-driven.findAncestorConflict: reads canonicalIds from the ancestor's
  own bmadDir, falling back to the prefix only when no manifest exists
- legacy-warnings.findStaleLegacyDirs: same canonicalId-based detection

Migration warnings: LEGACY_SKILL_PATHS adds 12 skill dirs that moved to the
.agents/skills/ standard (cursor, gemini, github-copilot, kimi, opencode, pi,
roo, rovodev, windsurf, plus their globals). Users with stale skills in those
locations get a one-line warning with the rm command per dir.

New shared helper tools/installer/ide/shared/installed-skills.js exposes
getInstalledCanonicalIds(bmadDir) and isBmadOwnedEntry(entry, canonicalIds).

Tests: 9 new assertions across two suites covering dedup, partial uninstall
preservation, and custom-module skill detection. All 286 tests pass.

* fix(installer): setupBatch must not claim a shared target_dir on failure

If the first platform's setup throws or returns success: false, the dedup map
previously still recorded the claim with skillCount: 0, causing every peer
sharing the target_dir to skip its install — leaving the dir empty/broken
behind a cascade of misleading "shares with X" rows.

Now the claim is only recorded when the install succeeded and wrote skills.
On failure, the next peer becomes the new first writer and recovers.

Adds Suite 40b regression test that monkey-patches cursor.setup to throw
and verifies gemini still populates the shared dir.

* fix(installer): address PR #2313 review findings

Three issues raised by augmentcode and coderabbit bot reviewers:

1. _removeDeselectedIdes silently swallowed cleanup failures after the
   refactor to cleanupByList. The old per-IDE try/catch logged a warning;
   the new path discarded the result array. Now logs a warning per failed
   ide so failures stay visible.

2. The legacy-dir cleanup hint printed `rm -rf "<path>"/bmad*` which both
   matched bmad-os-* utility skills the user should keep AND missed the
   custom-module skills (e.g. fred-cool-skill) that the new canonical-id
   detection now finds. Findings now carry the exact entry names from the
   scan, and the warning prints one precise rm line per entry.

3. warnPreNativeSkillsLegacy did unguarded fs reads at install start. A
   permission/IO error would have aborted the whole install. Wrapped the
   call site in try/catch so legacy-scan failures only emit a warning.
2026-04-25 21:14:00 -05:00
github-actions[bot] 1197122001 chore(release): v6.4.0 [skip ci] 2026-04-25 03:34:02 +00:00
Brian 314fe69d14
docs: add v6.4.0 changelog entry (#2310) 2026-04-24 22:31:01 -05:00
Nikolas de Hor 1a85069b75 fix: restore validate-workflow as native skill directory
The validate-workflow task was accidentally deleted during the
XML-to-native-skill refactor in #1864. The schema, handler, and
test fixtures still reference it, creating broken invocations.

Recreates the task as a native skill directory following the
established pattern (SKILL.md + bmad-skill-manifest.yaml + workflow.md),
with the original validation logic preserved.

Fixes #1530
2026-03-11 19:38:13 -03:00
20 changed files with 936 additions and 631 deletions

View File

@ -7,6 +7,7 @@ on:
- "src/**" - "src/**"
- "tools/installer/**" - "tools/installer/**"
- "package.json" - "package.json"
- "removals.txt"
workflow_dispatch: workflow_dispatch:
inputs: inputs:
channel: channel:
@ -135,6 +136,22 @@ jobs:
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Advance @next dist-tag to stable
if: github.event_name == 'workflow_dispatch' && inputs.channel == 'latest'
# Failure here leaves @next stale until the next push-driven prerelease
# republishes — annoying but not release-breaking. Don't fail the job
# after a successful stable publish + tag + GH release.
continue-on-error: true
run: |
# Without this, @latest can leapfrog @next (e.g. latest=6.5.0 while
# next=6.4.1-next.0) and `npx bmad-method@next install` silently
# downgrades users. Point @next at the just-published stable so
# @next >= @latest always holds; the next push-driven prerelease will
# bump from this base via the existing derive step above.
VERSION=$(node -p 'require("./package.json").version')
npm dist-tag add "bmad-method@${VERSION}" next
echo "Advanced @next dist-tag to ${VERSION}"
- name: Notify Discord - name: Notify Discord
if: github.event_name == 'workflow_dispatch' && inputs.channel == 'latest' if: github.event_name == 'workflow_dispatch' && inputs.channel == 'latest'
continue-on-error: true continue-on-error: true

View File

@ -1,5 +1,74 @@
# Changelog # Changelog
## v6.5.0 - 2026-04-26
### 🎁 Features
* Support for 18 new agent platforms: AdaL, Sourcegraph Amp, IBM Bob, Command Code, Snowflake Cortex Code, Factory Droid, Firebender, Block Goose, Kode, Mistral Vibe, Mux, Neovate, OpenClaw, OpenHands, Pochi, Replit Agent, Warp, Zencoder — bringing total supported platforms to 42 (#2313)
* All platforms that support the cross-tool `.agents/skills/` standard now use it (#2313)
## v6.4.0 - 2026-04-24
### ✨ Headline
**Full agent and workflow customization across the entire BMad Method.** Every agent and workflow in BMM, Core, CIS, GDS, and TEA can now be customized via TOML overrides in `_bmad/custom/`. Customize agents to apply tooling, version control, or behavior changes across whole groups of workflows. Drop in fine-grained per-workflow overrides where you need them. Built for power users who want BMad to fit their stack without forking.
**Stable and bleeding-edge release channels, standardized across all modules.** Pick `stable` or `next` per module, pin specific versions, and switch channels interactively or via CLI flags (`--channel`, `--all-stable`, `--all-next`, `--next=CODE`, `--pin CODE=TAG`). Same model across BMM, Core, and every external module.
### 💥 Breaking Changes
* Customization is now TOML-based; the briefly introduced YAML-based customization is no longer supported (#2284, #2283)
### 🎁 Features
**Customization framework**
* TOML-based agent and workflow customization with flat schema, structural merge rules (scalars, tables, code-keyed arrays, append arrays), and `persistent_facts` unification (#2284)
* Central `_bmad/config.toml` surface with four-file architecture (`config.toml`, `config.user.toml`, `custom/config.toml`, `custom/config.user.toml`) for agent roster and scope-partitioned install answers (#2285)
* `customize.toml` support extended to 17 bmm-skills workflows with flattened SKILL.md architecture and standardized `[workflow]` block (#2287)
* `customize.toml` extended to all six developer-execution workflows: bmad-dev-story, bmad-code-review, bmad-sprint-planning, bmad-sprint-status, bmad-quick-dev, bmad-checkpoint-preview (#2308)
* `bmad-customize` skill — guided authoring of TOML overrides in `_bmad/custom/` with stdlib-only resolver verification (#2289)
* Wire `on_complete` hook into all 23 workflow terminal steps with full customize.toml documentation (#2290)
**Release channels & installer**
* Channel-based version resolution for external modules with interactive channel management (`stable` / `next` / `pinned`) and CLI flags (`--channel`, `--all-stable`, `--all-next`, `--next=CODE`, `--pin CODE=TAG`) (#2305)
* GitHub API as primary fetch with raw CDN fallback in installer registry client to support corporate proxies (#2248)
**Other**
* Kimi Code CLI support for installing BMM skills in `.kimi/skills/` (#2302)
* `bmad-create-story` now reads every UPDATE-marked file before generating dev notes so brownfield stories preserve current behavior instead of improvising at implementation time (#2274)
* Sync `sprint-status.yaml` from quick-dev on epic-story implementation with idempotent writes tracking `in-progress` and `review` transitions (#2234)
* Enforce model parity for all code review subagents to match orchestrator session capability for improved rare-event detection (#2236)
* Set `team: software-development` on all six BMM agents for unified grouping in party-mode and retrospective skills (#2286)
### 🐛 Bug Fixes
* PRD workflow no longer silently de-scopes user requirements or invents MVP/Growth/Vision phasing; requires explicit confirmation before any scope reduction (#1927)
* Installer shows live npm version for external modules instead of stale cached metadata (#2307)
* Resolve external-module agents from cache during manifest write so agents land in `config.toml` (#2295)
* Fix installer version resolution for external modules with shared resolver preferring package.json > module.yaml > marketplace.json (#2298)
* Replace fs-extra with native `node:fs` to prevent file loss during multi-module installs from deferred retry-queue races (#2253)
* Add `move()` and overwrite support to fs-native wrapper for directory migrations during upgrades (#2253)
* Stop skill scanner from recursing into discovered skills to prevent spurious errors on nested template files (#2255)
* Source built-in modules locally in installer UI to preserve core and bmm in module list when registry is unreachable (#2251)
* Remove dead Batch-apply option from code-review patch menu and rename apply options for clarity (#2225)
### ♻️ Refactoring
* Remove 1,683 lines of dead code: three entirely dead files (agent-command-generator.js, bmad-artifacts.js, module-injections.js) and ~50 unused exports across installer modules (#2247)
* Remove dead template and agent-command pipeline from installer; SKILL.md directory copying is the sole installation path (#2244)
### 📚 Documentation
* Sync and update Vietnamese (vi-VN) docs with missing pages and refreshed translations (#2291, #2222)
* Sync French (fr-FR) translations with upstream, restore Amelia as dev agent, fix sidebar ordering (#2231)
* Add Czech (cs-CZ) `analysis-phase.md` translation; normalize typographic quotes (#2240, #2241, #2242)
* Add missing Chinese (zh-CN) translations for 3 documents (#2254)
* Update stale Analyst agent triggers and add PRFAQ link (#2238)
* Remove Bob from workflow map diagrams reflecting consolidation into Amelia in v6.3.0 (#2252)
## v6.3.0 - 2026-04-09 ## v6.3.0 - 2026-04-09
### 💥 Breaking Changes ### 💥 Breaking Changes

View File

@ -60,7 +60,7 @@ Dostupná ID nástrojů pro příznak `--tools`:
**Preferované:** `claude-code`, `cursor` **Preferované:** `claude-code`, `cursor`
Spusťte `npx bmad-method install` interaktivně jednou pro zobrazení aktuálního seznamu podporovaných nástrojů, nebo zkontrolujte [konfiguraci kódů platforem](https://github.com/bmad-code-org/BMAD-METHOD/blob/main/tools/cli/installers/lib/ide/platform-codes.yaml). Spusťte `npx bmad-method install` interaktivně jednou pro zobrazení aktuálního seznamu podporovaných nástrojů, nebo zkontrolujte [konfiguraci kódů platforem](https://github.com/bmad-code-org/BMAD-METHOD/blob/main/tools/installer/ide/platform-codes.yaml).
## Režimy instalace ## Režimy instalace

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "bmad-method", "name": "bmad-method",
"version": "6.3.0", "version": "6.5.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "bmad-method", "name": "bmad-method",
"version": "6.3.0", "version": "6.5.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@clack/core": "^1.0.0", "@clack/core": "^1.0.0",

View File

@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "bmad-method", "name": "bmad-method",
"version": "6.3.0", "version": "6.5.0",
"description": "Breakthrough Method of Agile AI-driven Development", "description": "Breakthrough Method of Agile AI-driven Development",
"keywords": [ "keywords": [
"agile", "agile",

View File

@ -15,3 +15,40 @@ bmad-quick-spec
bmad-quick-flow bmad-quick-flow
bmad-quick-dev-new-preview bmad-quick-dev-new-preview
bmad-init bmad-init
# Pre-v6.2.0 wrapper skills (module-prefixed naming, dropped in v6.2.0).
# Users upgrading from v6.0.x / v6.1.x had these installed and the cleanup
# never knew to remove them; they remained alongside the new self-contained
# skills causing duplicates and broken-file errors. See issue #2309.
bmad-agent-bmm-analyst
bmad-agent-bmm-architect
bmad-agent-bmm-dev
bmad-agent-bmm-pm
bmad-agent-bmm-qa
bmad-agent-bmm-quick-flow-solo-dev
bmad-agent-bmm-sm
bmad-agent-bmm-tech-writer
bmad-agent-bmm-ux-designer
bmad-bmm-check-implementation-readiness
bmad-bmm-code-review
bmad-bmm-correct-course
bmad-bmm-create-architecture
bmad-bmm-create-epics-and-stories
bmad-bmm-create-prd
bmad-bmm-create-product-brief
bmad-bmm-create-story
bmad-bmm-create-ux-design
bmad-bmm-dev-story
bmad-bmm-document-project
bmad-bmm-domain-research
bmad-bmm-edit-prd
bmad-bmm-generate-project-context
bmad-bmm-market-research
bmad-bmm-qa-generate-e2e-tests
bmad-bmm-quick-dev
bmad-bmm-quick-spec
bmad-bmm-retrospective
bmad-bmm-sprint-planning
bmad-bmm-sprint-status
bmad-bmm-technical-research
bmad-bmm-validate-prd

View File

@ -0,0 +1,6 @@
---
name: validate-workflow
description: "Run a checklist against a document with thorough analysis and produce a validation report"
---
Follow the instructions in [workflow.md](workflow.md).

View File

@ -0,0 +1 @@
type: skill

View File

@ -0,0 +1,88 @@
# Validate Workflow Output
**Goal:** Run a checklist against a document with thorough analysis and produce a validation report.
**Inputs:**
- **workflow** (required) — Workflow path containing `checklist.md`
- **checklist** (optional) — Checklist to validate against (defaults to the workflow's `checklist.md`)
- **document** (optional) — Document to validate (ask user if not specified)
## STEPS
### Step 1: Setup
- If checklist not provided, load `checklist.md` from the workflow location
- Try to fuzzy-match files similar to the input document name; if document not provided or unsure, ask user: "Which document should I validate?"
- Load both the checklist and document
### Step 2: Validate (CRITICAL)
**For EVERY checklist item, WITHOUT SKIPPING ANY:**
1. Read the requirement carefully
2. Search the document for evidence along with any ancillary loaded documents or artifacts (quotes with line numbers)
3. Analyze deeply — look for explicit AND implied coverage
**Mark each item as:**
- **PASS** `✓` — Requirement fully met (provide evidence)
- **PARTIAL** `⚠` — Some coverage but incomplete (explain gaps)
- **FAIL** `✗` — Not met or severely deficient (explain why)
- **N/A** `` — Not applicable (explain reason)
**DO NOT SKIP ANY SECTIONS OR ITEMS.**
### Step 3: Generate Report
Create `validation-report-{timestamp}.md` in the document's folder with the following format:
```markdown
# Validation Report
**Document:** {document-path}
**Checklist:** {checklist-path}
**Date:** {timestamp}
## Summary
- Overall: X/Y passed (Z%)
- Critical Issues: {count}
## Section Results
### {Section Name}
Pass Rate: X/Y (Z%)
[MARK] {Item description}
Evidence: {Quote with line# or explanation}
{If FAIL/PARTIAL: Impact: {why this matters}}
## Failed Items
{All ✗ items with recommendations}
## Partial Items
{All ⚠ items with what's missing}
## Recommendations
1. Must Fix: {critical failures}
2. Should Improve: {important gaps}
3. Consider: {minor improvements}
```
### Step 4: Summary for User
- Present section-by-section summary
- Highlight all critical issues
- Provide path to saved report
- **HALT** — do not continue unless user asks
## HALT CONDITIONS
- HALT after presenting summary in Step 4
- HALT with error if no checklist is found and none is provided
- HALT with error if no document is found and user does not specify one

View File

@ -139,19 +139,10 @@ 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');
assert(
Array.isArray(windsurfInstaller?.legacy_targets) && windsurfInstaller.legacy_targets.includes('.windsurf/workflows'),
'Windsurf installer cleans legacy workflow output',
);
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();
const legacyDir = path.join(tempProjectDir, '.windsurf', 'workflows', 'bmad-legacy-dir');
await fs.ensureDir(legacyDir);
await fs.writeFile(path.join(tempProjectDir, '.windsurf', 'workflows', 'bmad-legacy.md'), 'legacy\n');
await fs.writeFile(path.join(legacyDir, 'SKILL.md'), 'legacy\n');
const ideManager = new IdeManager(); const ideManager = new IdeManager();
await ideManager.ensureInitialized(); await ideManager.ensureInitialized();
@ -162,11 +153,9 @@ 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');
assert(!(await fs.pathExists(path.join(tempProjectDir, '.windsurf', 'workflows'))), 'Windsurf setup removes legacy workflows dir');
await fs.remove(tempProjectDir); await fs.remove(tempProjectDir);
await fs.remove(path.dirname(installedBmadDir)); await fs.remove(path.dirname(installedBmadDir));
} catch (error) { } catch (error) {
@ -187,17 +176,8 @@ async function runTests() {
assert(kiroInstaller?.target_dir === '.kiro/skills', 'Kiro target_dir uses native skills path'); assert(kiroInstaller?.target_dir === '.kiro/skills', 'Kiro target_dir uses native skills path');
assert(
Array.isArray(kiroInstaller?.legacy_targets) && kiroInstaller.legacy_targets.includes('.kiro/steering'),
'Kiro installer cleans legacy steering output',
);
const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-kiro-test-')); const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-kiro-test-'));
const installedBmadDir = await createTestBmadFixture(); const installedBmadDir = await createTestBmadFixture();
const legacyDir = path.join(tempProjectDir, '.kiro', 'steering', 'bmad-legacy-dir');
await fs.ensureDir(legacyDir);
await fs.writeFile(path.join(tempProjectDir, '.kiro', 'steering', 'bmad-legacy.md'), 'legacy\n');
await fs.writeFile(path.join(legacyDir, 'SKILL.md'), 'legacy\n');
const ideManager = new IdeManager(); const ideManager = new IdeManager();
await ideManager.ensureInitialized(); await ideManager.ensureInitialized();
@ -211,8 +191,6 @@ async function runTests() {
const skillFile = path.join(tempProjectDir, '.kiro', 'skills', 'bmad-master', 'SKILL.md'); const skillFile = path.join(tempProjectDir, '.kiro', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile), 'Kiro install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile), 'Kiro install writes SKILL.md directory output');
assert(!(await fs.pathExists(path.join(tempProjectDir, '.kiro', 'steering'))), 'Kiro setup removes legacy steering dir');
await fs.remove(tempProjectDir); await fs.remove(tempProjectDir);
await fs.remove(path.dirname(installedBmadDir)); await fs.remove(path.dirname(installedBmadDir));
} catch (error) { } catch (error) {
@ -233,17 +211,8 @@ async function runTests() {
assert(antigravityInstaller?.target_dir === '.agent/skills', 'Antigravity target_dir uses native skills path'); assert(antigravityInstaller?.target_dir === '.agent/skills', 'Antigravity target_dir uses native skills path');
assert(
Array.isArray(antigravityInstaller?.legacy_targets) && antigravityInstaller.legacy_targets.includes('.agent/workflows'),
'Antigravity installer cleans legacy workflow output',
);
const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-antigravity-test-')); const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-antigravity-test-'));
const installedBmadDir = await createTestBmadFixture(); const installedBmadDir = await createTestBmadFixture();
const legacyDir = path.join(tempProjectDir, '.agent', 'workflows', 'bmad-legacy-dir');
await fs.ensureDir(legacyDir);
await fs.writeFile(path.join(tempProjectDir, '.agent', 'workflows', 'bmad-legacy.md'), 'legacy\n');
await fs.writeFile(path.join(legacyDir, 'SKILL.md'), 'legacy\n');
const ideManager = new IdeManager(); const ideManager = new IdeManager();
await ideManager.ensureInitialized(); await ideManager.ensureInitialized();
@ -257,8 +226,6 @@ async function runTests() {
const skillFile = path.join(tempProjectDir, '.agent', 'skills', 'bmad-master', 'SKILL.md'); const skillFile = path.join(tempProjectDir, '.agent', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile), 'Antigravity install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile), 'Antigravity install writes SKILL.md directory output');
assert(!(await fs.pathExists(path.join(tempProjectDir, '.agent', 'workflows'))), 'Antigravity setup removes legacy workflows dir');
await fs.remove(tempProjectDir); await fs.remove(tempProjectDir);
await fs.remove(path.dirname(installedBmadDir)); await fs.remove(path.dirname(installedBmadDir));
} catch (error) { } catch (error) {
@ -277,12 +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(
Array.isArray(auggieInstaller?.legacy_targets) && auggieInstaller.legacy_targets.includes('.augment/commands'),
'Auggie installer cleans legacy command output',
);
assert( assert(
auggieInstaller?.ancestor_conflict_check !== true, auggieInstaller?.ancestor_conflict_check !== true,
@ -291,10 +253,6 @@ async function runTests() {
const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-auggie-test-')); const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-auggie-test-'));
const installedBmadDir = await createTestBmadFixture(); const installedBmadDir = await createTestBmadFixture();
const legacyDir = path.join(tempProjectDir, '.augment', 'commands', 'bmad-legacy-dir');
await fs.ensureDir(legacyDir);
await fs.writeFile(path.join(tempProjectDir, '.augment', 'commands', 'bmad-legacy.md'), 'legacy\n');
await fs.writeFile(path.join(legacyDir, 'SKILL.md'), 'legacy\n');
const ideManager = new IdeManager(); const ideManager = new IdeManager();
await ideManager.ensureInitialized(); await ideManager.ensureInitialized();
@ -305,11 +263,9 @@ 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');
assert(!(await fs.pathExists(path.join(tempProjectDir, '.augment', 'commands'))), 'Auggie setup removes legacy commands dir');
await fs.remove(tempProjectDir); await fs.remove(tempProjectDir);
await fs.remove(path.dirname(installedBmadDir)); await fs.remove(path.dirname(installedBmadDir));
} catch (error) { } catch (error) {
@ -328,30 +284,10 @@ 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');
assert(
Array.isArray(opencodeInstaller?.legacy_targets) &&
['.opencode/agents', '.opencode/commands', '.opencode/agent', '.opencode/command'].every((legacyTarget) =>
opencodeInstaller.legacy_targets.includes(legacyTarget),
),
'OpenCode installer cleans split legacy agent and command output',
);
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();
const legacyDirs = [
path.join(tempProjectDir, '.opencode', 'agents', 'bmad-legacy-agent'),
path.join(tempProjectDir, '.opencode', 'commands', 'bmad-legacy-command'),
path.join(tempProjectDir, '.opencode', 'agent', 'bmad-legacy-agent-singular'),
path.join(tempProjectDir, '.opencode', 'command', 'bmad-legacy-command-singular'),
];
for (const legacyDir of legacyDirs) {
await fs.ensureDir(legacyDir);
await fs.writeFile(path.join(legacyDir, 'SKILL.md'), 'legacy\n');
await fs.writeFile(path.join(path.dirname(legacyDir), `${path.basename(legacyDir)}.md`), 'legacy\n');
}
const ideManager = new IdeManager(); const ideManager = new IdeManager();
await ideManager.ensureInitialized(); await ideManager.ensureInitialized();
@ -362,16 +298,9 @@ 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');
for (const legacyDir of ['agents', 'commands', 'agent', 'command']) {
assert(
!(await fs.pathExists(path.join(tempProjectDir, '.opencode', legacyDir))),
`OpenCode setup removes legacy .opencode/${legacyDir} dir`,
);
}
await fs.remove(tempProjectDir); await fs.remove(tempProjectDir);
await fs.remove(path.dirname(installedBmadDir)); await fs.remove(path.dirname(installedBmadDir));
} catch (error) { } catch (error) {
@ -392,16 +321,8 @@ async function runTests() {
assert(claudeInstaller?.target_dir === '.claude/skills', 'Claude Code target_dir uses native skills path'); assert(claudeInstaller?.target_dir === '.claude/skills', 'Claude Code target_dir uses native skills path');
assert(
Array.isArray(claudeInstaller?.legacy_targets) && claudeInstaller.legacy_targets.includes('.claude/commands'),
'Claude Code installer cleans legacy command output',
);
const tempProjectDir9 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-claude-code-test-')); const tempProjectDir9 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-claude-code-test-'));
const installedBmadDir9 = await createTestBmadFixture(); const installedBmadDir9 = await createTestBmadFixture();
const legacyDir9 = path.join(tempProjectDir9, '.claude', 'commands');
await fs.ensureDir(legacyDir9);
await fs.writeFile(path.join(legacyDir9, 'bmad-legacy.md'), 'legacy\n');
const ideManager9 = new IdeManager(); const ideManager9 = new IdeManager();
await ideManager9.ensureInitialized(); await ideManager9.ensureInitialized();
@ -420,8 +341,6 @@ async function runTests() {
const nameMatch9 = skillContent9.match(/^name:\s*(.+)$/m); const nameMatch9 = skillContent9.match(/^name:\s*(.+)$/m);
assert(nameMatch9 && nameMatch9[1].trim() === 'bmad-master', 'Claude Code skill name frontmatter matches directory name exactly'); assert(nameMatch9 && nameMatch9[1].trim() === 'bmad-master', 'Claude Code skill name frontmatter matches directory name exactly');
assert(!(await fs.pathExists(legacyDir9)), 'Claude Code setup removes legacy commands dir');
await fs.remove(tempProjectDir9); await fs.remove(tempProjectDir9);
await fs.remove(path.dirname(installedBmadDir9)); await fs.remove(path.dirname(installedBmadDir9));
} catch (error) { } catch (error) {
@ -444,16 +363,8 @@ async function runTests() {
assert(codexInstaller?.target_dir === '.agents/skills', 'Codex target_dir uses native skills path'); assert(codexInstaller?.target_dir === '.agents/skills', 'Codex target_dir uses native skills path');
assert(
Array.isArray(codexInstaller?.legacy_targets) && codexInstaller.legacy_targets.includes('.codex/prompts'),
'Codex installer cleans legacy prompt output',
);
const tempProjectDir11 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-codex-test-')); const tempProjectDir11 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-codex-test-'));
const installedBmadDir11 = await createTestBmadFixture(); const installedBmadDir11 = await createTestBmadFixture();
const legacyDir11 = path.join(tempProjectDir11, '.codex', 'prompts');
await fs.ensureDir(legacyDir11);
await fs.writeFile(path.join(legacyDir11, 'bmad-legacy.md'), 'legacy\n');
const ideManager11 = new IdeManager(); const ideManager11 = new IdeManager();
await ideManager11.ensureInitialized(); await ideManager11.ensureInitialized();
@ -472,8 +383,6 @@ async function runTests() {
const nameMatch11 = skillContent11.match(/^name:\s*(.+)$/m); const nameMatch11 = skillContent11.match(/^name:\s*(.+)$/m);
assert(nameMatch11 && nameMatch11[1].trim() === 'bmad-master', 'Codex skill name frontmatter matches directory name exactly'); assert(nameMatch11 && nameMatch11[1].trim() === 'bmad-master', 'Codex skill name frontmatter matches directory name exactly');
assert(!(await fs.pathExists(legacyDir11)), 'Codex setup removes legacy prompts dir');
await fs.remove(tempProjectDir11); await fs.remove(tempProjectDir11);
await fs.remove(path.dirname(installedBmadDir11)); await fs.remove(path.dirname(installedBmadDir11));
} catch (error) { } catch (error) {
@ -494,20 +403,12 @@ 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(
Array.isArray(cursorInstaller?.legacy_targets) && cursorInstaller.legacy_targets.includes('.cursor/commands'),
'Cursor installer cleans legacy command output',
);
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');
const tempProjectDir13c = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-cursor-test-')); const tempProjectDir13c = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-cursor-test-'));
const installedBmadDir13c = await createTestBmadFixture(); const installedBmadDir13c = await createTestBmadFixture();
const legacyDir13c = path.join(tempProjectDir13c, '.cursor', 'commands');
await fs.ensureDir(legacyDir13c);
await fs.writeFile(path.join(legacyDir13c, 'bmad-legacy.md'), 'legacy\n');
const ideManager13c = new IdeManager(); const ideManager13c = new IdeManager();
await ideManager13c.ensureInitialized(); await ideManager13c.ensureInitialized();
@ -518,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
@ -526,8 +427,6 @@ async function runTests() {
const nameMatch13c = skillContent13c.match(/^name:\s*(.+)$/m); const nameMatch13c = skillContent13c.match(/^name:\s*(.+)$/m);
assert(nameMatch13c && nameMatch13c[1].trim() === 'bmad-master', 'Cursor skill name frontmatter matches directory name exactly'); assert(nameMatch13c && nameMatch13c[1].trim() === 'bmad-master', 'Cursor skill name frontmatter matches directory name exactly');
assert(!(await fs.pathExists(legacyDir13c)), 'Cursor setup removes legacy commands dir');
await fs.remove(tempProjectDir13c); await fs.remove(tempProjectDir13c);
await fs.remove(path.dirname(installedBmadDir13c)); await fs.remove(path.dirname(installedBmadDir13c));
} catch (error) { } catch (error) {
@ -546,19 +445,10 @@ 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');
assert(
Array.isArray(rooInstaller?.legacy_targets) && rooInstaller.legacy_targets.includes('.roo/commands'),
'Roo installer cleans legacy command output',
);
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();
const legacyDir13 = path.join(tempProjectDir13, '.roo', 'commands', 'bmad-legacy-dir');
await fs.ensureDir(legacyDir13);
await fs.writeFile(path.join(tempProjectDir13, '.roo', 'commands', 'bmad-legacy.md'), 'legacy\n');
await fs.writeFile(path.join(legacyDir13, 'SKILL.md'), 'legacy\n');
const ideManager13 = new IdeManager(); const ideManager13 = new IdeManager();
await ideManager13.ensureInitialized(); await ideManager13.ensureInitialized();
@ -569,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)
@ -580,8 +470,6 @@ async function runTests() {
'Roo skill name frontmatter matches directory name exactly (lowercase alphanumeric + hyphens)', 'Roo skill name frontmatter matches directory name exactly (lowercase alphanumeric + hyphens)',
); );
assert(!(await fs.pathExists(path.join(tempProjectDir13, '.roo', 'commands'))), 'Roo setup removes legacy commands dir');
// Reinstall/upgrade: run setup again over existing skills output // Reinstall/upgrade: run setup again over existing skills output
const result13b = await ideManager13.setup('roo', tempProjectDir13, installedBmadDir13, { const result13b = await ideManager13.setup('roo', tempProjectDir13, installedBmadDir13, {
silent: true, silent: true,
@ -615,31 +503,13 @@ 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');
assert(
Array.isArray(copilotInstaller?.legacy_targets) && copilotInstaller.legacy_targets.includes('.github/agents'),
'GitHub Copilot installer cleans legacy agents output',
);
assert(
Array.isArray(copilotInstaller?.legacy_targets) && copilotInstaller.legacy_targets.includes('.github/prompts'),
'GitHub Copilot installer cleans legacy prompts output',
);
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();
// Create legacy .github/agents/ and .github/prompts/ files
const legacyAgentsDir17 = path.join(tempProjectDir17, '.github', 'agents');
const legacyPromptsDir17 = path.join(tempProjectDir17, '.github', 'prompts');
await fs.ensureDir(legacyAgentsDir17);
await fs.ensureDir(legacyPromptsDir17);
await fs.writeFile(path.join(legacyAgentsDir17, 'bmad-legacy.agent.md'), 'legacy agent\n');
await fs.writeFile(path.join(legacyPromptsDir17, 'bmad-legacy.prompt.md'), 'legacy prompt\n');
// Create legacy copilot-instructions.md with BMAD markers
const copilotInstructionsPath17 = path.join(tempProjectDir17, '.github', 'copilot-instructions.md'); const copilotInstructionsPath17 = path.join(tempProjectDir17, '.github', 'copilot-instructions.md');
await fs.ensureDir(path.dirname(copilotInstructionsPath17));
await fs.writeFile( await fs.writeFile(
copilotInstructionsPath17, copilotInstructionsPath17,
'User content before\n<!-- BMAD:START -->\nBMAD generated content\n<!-- BMAD:END -->\nUser content after\n', 'User content before\n<!-- BMAD:START -->\nBMAD generated content\n<!-- BMAD:END -->\nUser content after\n',
@ -654,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
@ -662,10 +532,6 @@ async function runTests() {
const nameMatch17 = skillContent17.match(/^name:\s*(.+)$/m); const nameMatch17 = skillContent17.match(/^name:\s*(.+)$/m);
assert(nameMatch17 && nameMatch17[1].trim() === 'bmad-master', 'GitHub Copilot skill name frontmatter matches directory name exactly'); assert(nameMatch17 && nameMatch17[1].trim() === 'bmad-master', 'GitHub Copilot skill name frontmatter matches directory name exactly');
assert(!(await fs.pathExists(legacyAgentsDir17)), 'GitHub Copilot setup removes legacy agents dir');
assert(!(await fs.pathExists(legacyPromptsDir17)), 'GitHub Copilot setup removes legacy prompts dir');
// Verify copilot-instructions.md BMAD markers were stripped but user content preserved // Verify copilot-instructions.md BMAD markers were stripped but user content preserved
const cleanedInstructions17 = await fs.readFile(copilotInstructionsPath17, 'utf8'); const cleanedInstructions17 = await fs.readFile(copilotInstructionsPath17, 'utf8');
assert( assert(
@ -697,17 +563,8 @@ async function runTests() {
assert(clineInstaller?.target_dir === '.cline/skills', 'Cline target_dir uses native skills path'); assert(clineInstaller?.target_dir === '.cline/skills', 'Cline target_dir uses native skills path');
assert(
Array.isArray(clineInstaller?.legacy_targets) && clineInstaller.legacy_targets.includes('.clinerules/workflows'),
'Cline installer cleans legacy workflow output',
);
const tempProjectDir18 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-cline-test-')); const tempProjectDir18 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-cline-test-'));
const installedBmadDir18 = await createTestBmadFixture(); const installedBmadDir18 = await createTestBmadFixture();
const legacyDir18 = path.join(tempProjectDir18, '.clinerules', 'workflows', 'bmad-legacy-dir');
await fs.ensureDir(legacyDir18);
await fs.writeFile(path.join(tempProjectDir18, '.clinerules', 'workflows', 'bmad-legacy.md'), 'legacy\n');
await fs.writeFile(path.join(legacyDir18, 'SKILL.md'), 'legacy\n');
const ideManager18 = new IdeManager(); const ideManager18 = new IdeManager();
await ideManager18.ensureInitialized(); await ideManager18.ensureInitialized();
@ -726,8 +583,6 @@ async function runTests() {
const nameMatch18 = skillContent18.match(/^name:\s*(.+)$/m); const nameMatch18 = skillContent18.match(/^name:\s*(.+)$/m);
assert(nameMatch18 && nameMatch18[1].trim() === 'bmad-master', 'Cline skill name frontmatter matches directory name exactly'); assert(nameMatch18 && nameMatch18[1].trim() === 'bmad-master', 'Cline skill name frontmatter matches directory name exactly');
assert(!(await fs.pathExists(path.join(tempProjectDir18, '.clinerules', 'workflows'))), 'Cline setup removes legacy workflows dir');
// Reinstall/upgrade: run setup again over existing skills output // Reinstall/upgrade: run setup again over existing skills output
const result18b = await ideManager18.setup('cline', tempProjectDir18, installedBmadDir18, { const result18b = await ideManager18.setup('cline', tempProjectDir18, installedBmadDir18, {
silent: true, silent: true,
@ -757,17 +612,8 @@ async function runTests() {
assert(codebuddyInstaller?.target_dir === '.codebuddy/skills', 'CodeBuddy target_dir uses native skills path'); assert(codebuddyInstaller?.target_dir === '.codebuddy/skills', 'CodeBuddy target_dir uses native skills path');
assert(
Array.isArray(codebuddyInstaller?.legacy_targets) && codebuddyInstaller.legacy_targets.includes('.codebuddy/commands'),
'CodeBuddy installer cleans legacy command output',
);
const tempProjectDir19 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-codebuddy-test-')); const tempProjectDir19 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-codebuddy-test-'));
const installedBmadDir19 = await createTestBmadFixture(); const installedBmadDir19 = await createTestBmadFixture();
const legacyDir19 = path.join(tempProjectDir19, '.codebuddy', 'commands', 'bmad-legacy-dir');
await fs.ensureDir(legacyDir19);
await fs.writeFile(path.join(tempProjectDir19, '.codebuddy', 'commands', 'bmad-legacy.md'), 'legacy\n');
await fs.writeFile(path.join(legacyDir19, 'SKILL.md'), 'legacy\n');
const ideManager19 = new IdeManager(); const ideManager19 = new IdeManager();
await ideManager19.ensureInitialized(); await ideManager19.ensureInitialized();
@ -785,8 +631,6 @@ async function runTests() {
const nameMatch19 = skillContent19.match(/^name:\s*(.+)$/m); const nameMatch19 = skillContent19.match(/^name:\s*(.+)$/m);
assert(nameMatch19 && nameMatch19[1].trim() === 'bmad-master', 'CodeBuddy skill name frontmatter matches directory name exactly'); assert(nameMatch19 && nameMatch19[1].trim() === 'bmad-master', 'CodeBuddy skill name frontmatter matches directory name exactly');
assert(!(await fs.pathExists(path.join(tempProjectDir19, '.codebuddy', 'commands'))), 'CodeBuddy setup removes legacy commands dir');
const result19b = await ideManager19.setup('codebuddy', tempProjectDir19, installedBmadDir19, { const result19b = await ideManager19.setup('codebuddy', tempProjectDir19, installedBmadDir19, {
silent: true, silent: true,
selectedModules: ['bmm'], selectedModules: ['bmm'],
@ -813,19 +657,10 @@ 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');
assert(
Array.isArray(crushInstaller?.legacy_targets) && crushInstaller.legacy_targets.includes('.crush/commands'),
'Crush installer cleans legacy command output',
);
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();
const legacyDir20 = path.join(tempProjectDir20, '.crush', 'commands', 'bmad-legacy-dir');
await fs.ensureDir(legacyDir20);
await fs.writeFile(path.join(tempProjectDir20, '.crush', 'commands', 'bmad-legacy.md'), 'legacy\n');
await fs.writeFile(path.join(legacyDir20, 'SKILL.md'), 'legacy\n');
const ideManager20 = new IdeManager(); const ideManager20 = new IdeManager();
await ideManager20.ensureInitialized(); await ideManager20.ensureInitialized();
@ -836,15 +671,13 @@ 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');
const nameMatch20 = skillContent20.match(/^name:\s*(.+)$/m); const nameMatch20 = skillContent20.match(/^name:\s*(.+)$/m);
assert(nameMatch20 && nameMatch20[1].trim() === 'bmad-master', 'Crush skill name frontmatter matches directory name exactly'); assert(nameMatch20 && nameMatch20[1].trim() === 'bmad-master', 'Crush skill name frontmatter matches directory name exactly');
assert(!(await fs.pathExists(path.join(tempProjectDir20, '.crush', 'commands'))), 'Crush setup removes legacy commands dir');
const result20b = await ideManager20.setup('crush', tempProjectDir20, installedBmadDir20, { const result20b = await ideManager20.setup('crush', tempProjectDir20, installedBmadDir20, {
silent: true, silent: true,
selectedModules: ['bmm'], selectedModules: ['bmm'],
@ -873,16 +706,8 @@ async function runTests() {
assert(traeInstaller?.target_dir === '.trae/skills', 'Trae target_dir uses native skills path'); assert(traeInstaller?.target_dir === '.trae/skills', 'Trae target_dir uses native skills path');
assert(
Array.isArray(traeInstaller?.legacy_targets) && traeInstaller.legacy_targets.includes('.trae/rules'),
'Trae installer cleans legacy rules output',
);
const tempProjectDir21 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-trae-test-')); const tempProjectDir21 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-trae-test-'));
const installedBmadDir21 = await createTestBmadFixture(); const installedBmadDir21 = await createTestBmadFixture();
const legacyDir21 = path.join(tempProjectDir21, '.trae', 'rules');
await fs.ensureDir(legacyDir21);
await fs.writeFile(path.join(legacyDir21, 'bmad-legacy.md'), 'legacy\n');
const ideManager21 = new IdeManager(); const ideManager21 = new IdeManager();
await ideManager21.ensureInitialized(); await ideManager21.ensureInitialized();
@ -900,8 +725,6 @@ async function runTests() {
const nameMatch21 = skillContent21.match(/^name:\s*(.+)$/m); const nameMatch21 = skillContent21.match(/^name:\s*(.+)$/m);
assert(nameMatch21 && nameMatch21[1].trim() === 'bmad-master', 'Trae skill name frontmatter matches directory name exactly'); assert(nameMatch21 && nameMatch21[1].trim() === 'bmad-master', 'Trae skill name frontmatter matches directory name exactly');
assert(!(await fs.pathExists(path.join(tempProjectDir21, '.trae', 'rules'))), 'Trae setup removes legacy rules dir');
const result21b = await ideManager21.setup('trae', tempProjectDir21, installedBmadDir21, { const result21b = await ideManager21.setup('trae', tempProjectDir21, installedBmadDir21, {
silent: true, silent: true,
selectedModules: ['bmm'], selectedModules: ['bmm'],
@ -930,12 +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');
assert(
Array.isArray(kiloConfig22?.installer?.legacy_targets) && kiloConfig22.installer.legacy_targets.includes('.kilocode/workflows'),
'KiloCoder installer cleans legacy workflows output',
);
const ideManager22 = new IdeManager(); const ideManager22 = new IdeManager();
await ideManager22.ensureInitialized(); await ideManager22.ensureInitialized();
@ -950,11 +768,6 @@ async function runTests() {
const tempProjectDir22 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-kilo-test-')); const tempProjectDir22 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-kilo-test-'));
const installedBmadDir22 = await createTestBmadFixture(); const installedBmadDir22 = await createTestBmadFixture();
// Pre-populate legacy Kilo artifacts that should be cleaned up
const legacyDir22 = path.join(tempProjectDir22, '.kilocode', 'workflows');
await fs.ensureDir(legacyDir22);
await fs.writeFile(path.join(legacyDir22, 'bmad-legacy.md'), 'legacy\n');
const result22 = await ideManager22.setup('kilo', tempProjectDir22, installedBmadDir22, { const result22 = await ideManager22.setup('kilo', tempProjectDir22, installedBmadDir22, {
silent: true, silent: true,
selectedModules: ['bmm'], selectedModules: ['bmm'],
@ -962,15 +775,13 @@ 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');
const nameMatch22 = skillContent22.match(/^name:\s*(.+)$/m); const nameMatch22 = skillContent22.match(/^name:\s*(.+)$/m);
assert(nameMatch22 && nameMatch22[1].trim() === 'bmad-master', 'KiloCoder skill name frontmatter matches directory name exactly'); assert(nameMatch22 && nameMatch22[1].trim() === 'bmad-master', 'KiloCoder skill name frontmatter matches directory name exactly');
assert(!(await fs.pathExists(path.join(tempProjectDir22, '.kilocode', 'workflows'))), 'KiloCoder setup removes legacy workflows dir');
const result22b = await ideManager22.setup('kilo', tempProjectDir22, installedBmadDir22, { const result22b = await ideManager22.setup('kilo', tempProjectDir22, installedBmadDir22, {
silent: true, silent: true,
selectedModules: ['bmm'], selectedModules: ['bmm'],
@ -997,18 +808,10 @@ 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');
assert(
Array.isArray(geminiInstaller?.legacy_targets) && geminiInstaller.legacy_targets.includes('.gemini/commands'),
'Gemini installer cleans legacy commands output',
);
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();
const legacyDir23 = path.join(tempProjectDir23, '.gemini', 'commands');
await fs.ensureDir(legacyDir23);
await fs.writeFile(path.join(legacyDir23, 'bmad-legacy.toml'), 'legacy\n');
const ideManager23 = new IdeManager(); const ideManager23 = new IdeManager();
await ideManager23.ensureInitialized(); await ideManager23.ensureInitialized();
@ -1019,15 +822,13 @@ 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');
const nameMatch23 = skillContent23.match(/^name:\s*(.+)$/m); const nameMatch23 = skillContent23.match(/^name:\s*(.+)$/m);
assert(nameMatch23 && nameMatch23[1].trim() === 'bmad-master', 'Gemini skill name frontmatter matches directory name exactly'); assert(nameMatch23 && nameMatch23[1].trim() === 'bmad-master', 'Gemini skill name frontmatter matches directory name exactly');
assert(!(await fs.pathExists(path.join(tempProjectDir23, '.gemini', 'commands'))), 'Gemini setup removes legacy commands dir');
const result23b = await ideManager23.setup('gemini', tempProjectDir23, installedBmadDir23, { const result23b = await ideManager23.setup('gemini', tempProjectDir23, installedBmadDir23, {
silent: true, silent: true,
selectedModules: ['bmm'], selectedModules: ['bmm'],
@ -1055,16 +856,9 @@ async function runTests() {
const iflowInstaller = platformCodes24.platforms.iflow?.installer; const iflowInstaller = platformCodes24.platforms.iflow?.installer;
assert(iflowInstaller?.target_dir === '.iflow/skills', 'iFlow target_dir uses native skills path'); assert(iflowInstaller?.target_dir === '.iflow/skills', 'iFlow target_dir uses native skills path');
assert(
Array.isArray(iflowInstaller?.legacy_targets) && iflowInstaller.legacy_targets.includes('.iflow/commands'),
'iFlow installer cleans legacy commands output',
);
const tempProjectDir24 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-iflow-test-')); const tempProjectDir24 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-iflow-test-'));
const installedBmadDir24 = await createTestBmadFixture(); const installedBmadDir24 = await createTestBmadFixture();
const legacyDir24 = path.join(tempProjectDir24, '.iflow', 'commands');
await fs.ensureDir(legacyDir24);
await fs.writeFile(path.join(legacyDir24, 'bmad-legacy.md'), 'legacy\n');
const ideManager24 = new IdeManager(); const ideManager24 = new IdeManager();
await ideManager24.ensureInitialized(); await ideManager24.ensureInitialized();
@ -1083,8 +877,6 @@ async function runTests() {
const nameMatch24 = skillContent24.match(/^name:\s*(.+)$/m); const nameMatch24 = skillContent24.match(/^name:\s*(.+)$/m);
assert(nameMatch24 && nameMatch24[1].trim() === 'bmad-master', 'iFlow skill name frontmatter matches directory name exactly'); assert(nameMatch24 && nameMatch24[1].trim() === 'bmad-master', 'iFlow skill name frontmatter matches directory name exactly');
assert(!(await fs.pathExists(path.join(tempProjectDir24, '.iflow', 'commands'))), 'iFlow setup removes legacy commands dir');
await fs.remove(tempProjectDir24); await fs.remove(tempProjectDir24);
await fs.remove(path.dirname(installedBmadDir24)); await fs.remove(path.dirname(installedBmadDir24));
} catch (error) { } catch (error) {
@ -1104,16 +896,9 @@ async function runTests() {
const qwenInstaller = platformCodes25.platforms.qwen?.installer; const qwenInstaller = platformCodes25.platforms.qwen?.installer;
assert(qwenInstaller?.target_dir === '.qwen/skills', 'QwenCoder target_dir uses native skills path'); assert(qwenInstaller?.target_dir === '.qwen/skills', 'QwenCoder target_dir uses native skills path');
assert(
Array.isArray(qwenInstaller?.legacy_targets) && qwenInstaller.legacy_targets.includes('.qwen/commands'),
'QwenCoder installer cleans legacy commands output',
);
const tempProjectDir25 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-qwen-test-')); const tempProjectDir25 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-qwen-test-'));
const installedBmadDir25 = await createTestBmadFixture(); const installedBmadDir25 = await createTestBmadFixture();
const legacyDir25 = path.join(tempProjectDir25, '.qwen', 'commands');
await fs.ensureDir(legacyDir25);
await fs.writeFile(path.join(legacyDir25, 'bmad-legacy.md'), 'legacy\n');
const ideManager25 = new IdeManager(); const ideManager25 = new IdeManager();
await ideManager25.ensureInitialized(); await ideManager25.ensureInitialized();
@ -1132,8 +917,6 @@ async function runTests() {
const nameMatch25 = skillContent25.match(/^name:\s*(.+)$/m); const nameMatch25 = skillContent25.match(/^name:\s*(.+)$/m);
assert(nameMatch25 && nameMatch25[1].trim() === 'bmad-master', 'QwenCoder skill name frontmatter matches directory name exactly'); assert(nameMatch25 && nameMatch25[1].trim() === 'bmad-master', 'QwenCoder skill name frontmatter matches directory name exactly');
assert(!(await fs.pathExists(path.join(tempProjectDir25, '.qwen', 'commands'))), 'QwenCoder setup removes legacy commands dir');
await fs.remove(tempProjectDir25); await fs.remove(tempProjectDir25);
await fs.remove(path.dirname(installedBmadDir25)); await fs.remove(path.dirname(installedBmadDir25));
} catch (error) { } catch (error) {
@ -1152,17 +935,10 @@ 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');
assert(
Array.isArray(rovoInstaller?.legacy_targets) && rovoInstaller.legacy_targets.includes('.rovodev/workflows'),
'Rovo Dev installer cleans legacy workflows output',
);
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();
const legacyDir26 = path.join(tempProjectDir26, '.rovodev', 'workflows');
await fs.ensureDir(legacyDir26);
await fs.writeFile(path.join(legacyDir26, 'bmad-legacy.md'), 'legacy\n');
// Create a prompts.yml with BMAD entries and a user entry // Create a prompts.yml with BMAD entries and a user entry
const yaml26 = require('yaml'); const yaml26 = require('yaml');
@ -1173,6 +949,7 @@ async function runTests() {
{ name: 'my-custom-prompt', description: 'User prompt', content_file: 'custom.md' }, { name: 'my-custom-prompt', description: 'User prompt', content_file: 'custom.md' },
], ],
}); });
await fs.ensureDir(path.dirname(promptsPath26));
await fs.writeFile(promptsPath26, promptsContent26); await fs.writeFile(promptsPath26, promptsContent26);
const ideManager26 = new IdeManager(); const ideManager26 = new IdeManager();
@ -1184,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
@ -1192,8 +969,6 @@ async function runTests() {
const nameMatch26 = skillContent26.match(/^name:\s*(.+)$/m); const nameMatch26 = skillContent26.match(/^name:\s*(.+)$/m);
assert(nameMatch26 && nameMatch26[1].trim() === 'bmad-master', 'Rovo Dev skill name frontmatter matches directory name exactly'); assert(nameMatch26 && nameMatch26[1].trim() === 'bmad-master', 'Rovo Dev skill name frontmatter matches directory name exactly');
assert(!(await fs.pathExists(path.join(tempProjectDir26, '.rovodev', 'workflows'))), 'Rovo Dev setup removes legacy workflows dir');
// Verify prompts.yml cleanup: BMAD entries removed, user entry preserved // Verify prompts.yml cleanup: BMAD entries removed, user entry preserved
const cleanedPrompts26 = yaml26.parse(await fs.readFile(promptsPath26, 'utf8')); const cleanedPrompts26 = yaml26.parse(await fs.readFile(promptsPath26, 'utf8'));
assert( assert(
@ -1295,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();
@ -1325,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
@ -1607,7 +1382,7 @@ async function runTests() {
}); });
assert(result.success === true, 'Antigravity setup succeeds with overlapping skill names'); 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.skillDirectories === 1, 'Result exposes unique skill directory count');
assert(result.handlerResult.results.skills === 1, 'Result retains verbatim skill count'); assert(result.handlerResult.results.skills === 1, 'Result retains verbatim skill count');
assert( assert(
@ -2847,6 +2622,157 @@ async function runTests() {
console.log(''); 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 40b: setupBatch — failed first writer does not poison peers
// ============================================================
console.log(`${colors.yellow}Test Suite 40b: setupBatch resilience to first-writer failure${colors.reset}\n`);
try {
const tempProjectDir40b = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-batch-fail-'));
const installedBmadDir40b = await createTestBmadFixture();
const ideManager40b = new IdeManager();
await ideManager40b.ensureInitialized();
// Force cursor's setup() to fail. With the bug, gemini would see the
// claimed target and skip — leaving .agents/skills/ empty.
const cursorHandler40b = ideManager40b.handlers.get('cursor');
const originalSetup = cursorHandler40b.setup.bind(cursorHandler40b);
cursorHandler40b.setup = async () => {
throw new Error('Simulated cursor failure');
};
const batchResults40b = await ideManager40b.setupBatch(['cursor', 'gemini'], tempProjectDir40b, installedBmadDir40b, {
silent: true,
selectedModules: ['core'],
});
// Restore so other tests aren't affected.
cursorHandler40b.setup = originalSetup;
assert(batchResults40b[0].success === false, 'Cursor reports failure');
assert(batchResults40b[1].success === true, 'Gemini still succeeds despite cursor failure');
assert(
batchResults40b[1].handlerResult?.results?.sharedTargetHandledByPeer !== true,
'Gemini does NOT skip its own write — it becomes the new first writer',
);
const sharedDir40b = path.join(tempProjectDir40b, '.agents', 'skills');
const entries40b = await fs.readdir(sharedDir40b);
assert(entries40b.includes('bmad-master'), 'Shared dir is populated by gemini after cursor failure');
await fs.remove(tempProjectDir40b).catch(() => {});
await fs.remove(path.dirname(installedBmadDir40b)).catch(() => {});
} catch (error) {
console.log(`${colors.red}Test Suite 40b 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 // Summary
// ============================================================ // ============================================================

View File

@ -222,7 +222,6 @@ Support assumption: full Agent Skills support. Gemini CLI docs confirm workspace
- [x] Confirm Gemini CLI native skills path is `.gemini/skills/{skill-name}/SKILL.md` (per [geminicli.com/docs/cli/skills](https://geminicli.com/docs/cli/skills/)) - [x] Confirm Gemini CLI native skills path is `.gemini/skills/{skill-name}/SKILL.md` (per [geminicli.com/docs/cli/skills](https://geminicli.com/docs/cli/skills/))
- [x] Implement native skills output — target_dir `.gemini/skills`, skill_format true, template_type default (replaces TOML templates) - [x] Implement native skills output — target_dir `.gemini/skills`, skill_format true, template_type default (replaces TOML templates)
- [x] Add legacy cleanup for `.gemini/commands` (via `legacy_targets`)
- [x] Test fresh install — skills written to `.gemini/skills/bmad-master/SKILL.md` with correct frontmatter - [x] Test fresh install — skills written to `.gemini/skills/bmad-master/SKILL.md` with correct frontmatter
- [x] Test reinstall/upgrade from legacy TOML command output — legacy dir removed, skills installed - [x] Test reinstall/upgrade from legacy TOML command output — legacy dir removed, skills installed
- [x] Confirm no ancestor conflict protection is needed — Gemini CLI uses workspace > user > extension precedence, no ancestor directory inheritance - [x] Confirm no ancestor conflict protection is needed — Gemini CLI uses workspace > user > extension precedence, no ancestor directory inheritance
@ -236,7 +235,6 @@ Support assumption: full Agent Skills support. iFlow docs confirm workspace skil
- [x] Confirm iFlow native skills path is `.iflow/skills/{skill-name}/SKILL.md` - [x] Confirm iFlow native skills path is `.iflow/skills/{skill-name}/SKILL.md`
- [x] Implement native skills output — target_dir `.iflow/skills`, skill_format true, template_type default - [x] Implement native skills output — target_dir `.iflow/skills`, skill_format true, template_type default
- [x] Add legacy cleanup for `.iflow/commands` (via `legacy_targets`)
- [x] Test fresh install — skills written to `.iflow/skills/bmad-master/SKILL.md` - [x] Test fresh install — skills written to `.iflow/skills/bmad-master/SKILL.md`
- [x] Test legacy cleanup — legacy commands dir removed - [x] Test legacy cleanup — legacy commands dir removed
- [x] Implement/extend automated tests — 6 assertions in test suite 24 - [x] Implement/extend automated tests — 6 assertions in test suite 24
@ -249,7 +247,6 @@ Support assumption: full Agent Skills support. Qwen Code supports workspace skil
- [x] Confirm QwenCoder native skills path is `.qwen/skills/{skill-name}/SKILL.md` - [x] Confirm QwenCoder native skills path is `.qwen/skills/{skill-name}/SKILL.md`
- [x] Implement native skills output — target_dir `.qwen/skills`, skill_format true, template_type default - [x] Implement native skills output — target_dir `.qwen/skills`, skill_format true, template_type default
- [x] Add legacy cleanup for `.qwen/commands` (via `legacy_targets`)
- [x] Test fresh install — skills written to `.qwen/skills/bmad-master/SKILL.md` - [x] Test fresh install — skills written to `.qwen/skills/bmad-master/SKILL.md`
- [x] Test legacy cleanup — legacy commands dir removed - [x] Test legacy cleanup — legacy commands dir removed
- [x] Implement/extend automated tests — 6 assertions in test suite 25 - [x] Implement/extend automated tests — 6 assertions in test suite 25
@ -262,7 +259,6 @@ Support assumption: full Agent Skills support. Rovo Dev now supports workspace s
- [x] Confirm Rovo Dev native skills path is `.rovodev/skills/{skill-name}/SKILL.md` (per Atlassian blog) - [x] Confirm Rovo Dev native skills path is `.rovodev/skills/{skill-name}/SKILL.md` (per Atlassian blog)
- [x] Replace 257-line custom `rovodev.js` with config-driven entry in `platform-codes.yaml` - [x] Replace 257-line custom `rovodev.js` with config-driven entry in `platform-codes.yaml`
- [x] Add legacy cleanup for `.rovodev/workflows` (via `legacy_targets`) and BMAD entries in `prompts.yml` (via `cleanupRovoDevPrompts()` in `_config-driven.js`)
- [x] Test fresh install — skills written to `.rovodev/skills/bmad-master/SKILL.md` - [x] Test fresh install — skills written to `.rovodev/skills/bmad-master/SKILL.md`
- [x] Test legacy cleanup — legacy workflows dir removed, `prompts.yml` BMAD entries stripped while preserving user entries - [x] Test legacy cleanup — legacy workflows dir removed, `prompts.yml` BMAD entries stripped while preserving user entries
- [x] Implement/extend automated tests — 8 assertions in test suite 26 - [x] Implement/extend automated tests — 8 assertions in test suite 26

View File

@ -23,13 +23,10 @@ checkForUpdate().catch(() => {
async function checkForUpdate() { async function checkForUpdate() {
try { try {
// For beta versions, check the beta tag; otherwise check latest // Prereleases (e.g. 6.5.1-next.0) live on the `next` dist-tag; stable
const isBeta = // releases live on `latest`. semver.prerelease() returns null for stable,
packageJson.version.includes('Beta') || // so this correctly routes pre-1.0-next/rc/etc. without string matching.
packageJson.version.includes('beta') || const tag = semver.prerelease(packageJson.version) ? 'next' : 'latest';
packageJson.version.includes('alpha') ||
packageJson.version.includes('rc');
const tag = isBeta ? 'beta' : 'latest';
const result = execSync(`npm view ${packageName}@${tag} version`, { const result = execSync(`npm view ${packageName}@${tag} version`, {
encoding: 'utf8', encoding: 'utf8',

View File

@ -14,6 +14,7 @@ const { ExternalModuleManager } = require('../modules/external-manager');
const { resolveModuleVersion } = require('../modules/version-resolver'); const { resolveModuleVersion } = require('../modules/version-resolver');
const { ExistingInstall } = require('./existing-install'); const { ExistingInstall } = require('./existing-install');
const { warnPreNativeSkillsLegacy } = require('./legacy-warnings');
class Installer { class Installer {
constructor() { constructor() {
@ -41,6 +42,16 @@ class Installer {
const officialModules = await OfficialModules.build(config, paths); const officialModules = await OfficialModules.build(config, paths);
const existingInstall = await ExistingInstall.detect(paths.bmadDir); const existingInstall = await ExistingInstall.detect(paths.bmadDir);
try {
await warnPreNativeSkillsLegacy({
projectRoot: paths.projectRoot,
existingVersion: existingInstall.installed ? existingInstall.version : null,
});
} catch (error) {
// Legacy-dir scan is informational; never let it abort install.
await prompts.log.warn(`Warning: Could not check for legacy BMAD entries: ${error.message}`);
}
if (existingInstall.installed) { if (existingInstall.installed) {
await this._removeDeselectedModules(existingInstall, config, paths); await this._removeDeselectedModules(existingInstall, config, paths);
updateState = await this._prepareUpdateState(paths, config, existingInstall, officialModules); updateState = await this._prepareUpdateState(paths, config, existingInstall, officialModules);
@ -183,15 +194,16 @@ class Installer {
if (toRemove.length === 0) return; if (toRemove.length === 0) return;
await this.ideManager.ensureInitialized(); // Pass the newly-selected list as remainingIdes so cleanupByList skips
for (const ide of toRemove) { // target_dir wipes for IDEs whose directory is still owned by a peer
try { // (e.g. removing 'cursor' while 'gemini' remains — both share .agents/skills).
const handler = this.ideManager.handlers.get(ide); const results = await this.ideManager.cleanupByList(paths.projectRoot, toRemove, {
if (handler) { remainingIdes: [...newlySelected],
await handler.cleanup(paths.projectRoot); });
}
} catch (error) { for (const result of results || []) {
await prompts.log.warn(`Warning: Failed to remove ${ide}: ${error.message}`); if (result && result.success === false) {
await prompts.log.warn(`Warning: Failed to remove ${result.ide}: ${result.error || 'unknown error'}`);
} }
} }
} }
@ -342,13 +354,14 @@ class Installer {
return; return;
} }
for (const ide of validIdes) { const setupResults = await this.ideManager.setupBatch(validIdes, paths.projectRoot, paths.bmadDir, {
const setupResult = await this.ideManager.setup(ide, paths.projectRoot, paths.bmadDir, { selectedModules: allModules || [],
selectedModules: allModules || [], verbose: config.verbose,
verbose: config.verbose, previousSkillIds,
previousSkillIds, });
});
for (const setupResult of setupResults) {
const ide = setupResult.ide;
if (setupResult.success) { if (setupResult.success) {
addResult(ide, 'ok', setupResult.detail || ''); addResult(ide, 'ok', setupResult.detail || '');
} else { } else {

View File

@ -0,0 +1,151 @@
const os = require('node:os');
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';
// 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',
'.clinerules/workflows',
'.codex/prompts',
'~/.codex/prompts',
'.codebuddy/commands',
'.crush/commands',
'.cursor/commands',
'.gemini/commands',
'.github/agents',
'.github/prompts',
'.iflow/commands',
'.kilocode/workflows',
'.kiro/steering',
'.opencode/agents',
'.opencode/commands',
'.opencode/agent',
'.opencode/command',
'.qwen/commands',
'.roo/commands',
'.rovodev/workflows',
'.trae/rules',
'.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));
return p;
}
function resolveLegacyPath(projectRoot, p) {
if (path.isAbsolute(p) || p.startsWith('~')) return expandPath(p);
return path.join(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) => isBmadOwnedEntry(e, canonicalIds));
if (bmadEntries.length > 0) {
findings.push({ path: resolved, displayPath: legacyPath, count: bmadEntries.length, entries: bmadEntries });
}
} catch {
// Unreadable dir — skip
}
}
return findings;
}
function isPreNativeSkillsVersion(version) {
if (!version) return false;
const coerced = semver.valid(version) || semver.valid(semver.coerce(version));
if (!coerced) return false;
return semver.lt(coerced, MIN_NATIVE_SKILLS_VERSION);
}
async function warnPreNativeSkillsLegacy({ projectRoot, existingVersion } = {}) {
const versionTriggered = isPreNativeSkillsVersion(existingVersion);
const staleDirs = await findStaleLegacyDirs(projectRoot);
if (!versionTriggered && staleDirs.length === 0) return;
if (versionTriggered) {
await prompts.log.warn(
`Detected previous BMAD install v${existingVersion} (pre-${MIN_NATIVE_SKILLS_VERSION}). ` +
`BMAD switched to native skills format in v${MIN_NATIVE_SKILLS_VERSION}; old command/workflow directories from your prior install may still be present.`,
);
}
if (staleDirs.length > 0) {
await prompts.log.warn(
`Found stale BMAD entries in ${staleDirs.length} legacy location(s) that the new installer no longer manages. ` +
`Your AI tool may load these alongside the new skills, causing duplicates. Remove them manually:`,
);
for (const finding of staleDirs) {
// Print each entry by exact name. A `bmad*` glob would (a) miss
// custom-module skills the canonicalId scan now picks up, and
// (b) match bmad-os-* utility skills the user should keep.
const entries = finding.entries || [];
for (const entry of entries) {
await prompts.log.message(` rm -rf "${path.join(finding.path, entry)}"`);
}
}
} else if (versionTriggered) {
await prompts.log.message(
' 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).',
);
}
}
module.exports = {
warnPreNativeSkillsLegacy,
findStaleLegacyDirs,
isPreNativeSkillsVersion,
LEGACY_PATHS,
MIN_NATIVE_SKILLS_VERSION,
};

View File

@ -1,10 +1,10 @@
const os = require('node:os');
const path = require('node:path'); const path = require('node:path');
const fs = require('../fs-native'); const fs = require('../fs-native');
const yaml = require('yaml'); const yaml = require('yaml');
const prompts = require('../prompts'); const prompts = require('../prompts');
const csv = require('csv-parse/sync'); const csv = require('csv-parse/sync');
const { BMAD_FOLDER_NAME } = require('./shared/path-utils'); const { BMAD_FOLDER_NAME } = require('./shared/path-utils');
const { getInstalledCanonicalIds, isBmadOwnedEntry } = require('./shared/installed-skills');
/** /**
* Config-driven IDE setup handler * Config-driven IDE setup handler
@ -16,7 +16,7 @@ const { BMAD_FOLDER_NAME } = require('./shared/path-utils');
* Features: * Features:
* - Config-driven from platform-codes.yaml * - Config-driven from platform-codes.yaml
* - Verbatim skill installation from skill-manifest.csv * - Verbatim skill installation from skill-manifest.csv
* - Legacy directory cleanup and IDE-specific marker removal * - IDE-specific marker removal (copilot-instructions, kilo modes, rovodev prompts)
*/ */
class ConfigDrivenIdeSetup { class ConfigDrivenIdeSetup {
constructor(platformCode, platformConfig) { constructor(platformCode, platformConfig) {
@ -44,16 +44,20 @@ class ConfigDrivenIdeSetup {
async detect(projectDir) { async detect(projectDir) {
if (!this.configDir) return false; if (!this.configDir) return false;
const dir = path.join(projectDir || process.cwd(), this.configDir); const root = projectDir || process.cwd();
if (await fs.pathExists(dir)) { const dir = path.join(root, this.configDir);
try { if (!(await fs.pathExists(dir))) return false;
const entries = await fs.readdir(dir);
return entries.some((e) => typeof e === 'string' && e.startsWith('bmad')); let entries;
} catch { try {
return false; 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));
} }
/** /**
@ -92,6 +96,12 @@ class ConfigDrivenIdeSetup {
return { success: false, reason: 'no-config' }; 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) { if (this.installerConfig.target_dir) {
return this.installToTarget(projectDir, bmadDir, this.installerConfig, options); return this.installToTarget(projectDir, bmadDir, this.installerConfig, options);
} }
@ -222,27 +232,6 @@ class ConfigDrivenIdeSetup {
removalSet = new Set(); removalSet = new Set();
} }
// Migrate legacy target directories (e.g. .opencode/agent → .opencode/agents)
// Legacy dirs are abandoned entirely, so use prefix matching (null removalSet)
if (this.installerConfig?.legacy_targets) {
const legacyDirsExist = await Promise.all(
this.installerConfig.legacy_targets.map((d) =>
this.isGlobalPath(d) ? fs.pathExists(d.replace(/^~/, os.homedir())) : fs.pathExists(path.join(projectDir, d)),
),
);
if (legacyDirsExist.some(Boolean)) {
if (!options.silent) await prompts.log.message(' Migrating legacy directories...');
for (const legacyDir of this.installerConfig.legacy_targets) {
if (this.isGlobalPath(legacyDir)) {
await this.warnGlobalLegacy(legacyDir, options);
} else {
await this.cleanupTarget(projectDir, legacyDir, options, null);
await this.removeEmptyParents(projectDir, legacyDir);
}
}
}
}
// Strip BMAD markers from copilot-instructions.md if present // Strip BMAD markers from copilot-instructions.md if present
if (this.name === 'github-copilot') { if (this.name === 'github-copilot') {
await this.cleanupCopilotInstructions(projectDir, options); await this.cleanupCopilotInstructions(projectDir, options);
@ -258,47 +247,17 @@ class ConfigDrivenIdeSetup {
await this.cleanupRovoDevPrompts(projectDir, options); 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 // Clean current target directory
if (this.installerConfig?.target_dir) { if (this.installerConfig?.target_dir) {
await this.cleanupTarget(projectDir, this.installerConfig.target_dir, options, removalSet); await this.cleanupTarget(projectDir, this.installerConfig.target_dir, options, removalSet);
} }
} }
/**
* Check if a path is global (starts with ~ or is absolute)
* @param {string} p - Path to check
* @returns {boolean}
*/
isGlobalPath(p) {
return p.startsWith('~') || path.isAbsolute(p);
}
/**
* Warn about stale BMAD files in a global legacy directory (never auto-deletes)
* @param {string} legacyDir - Legacy directory path (may start with ~)
* @param {Object} options - Options (silent, etc.)
*/
async warnGlobalLegacy(legacyDir, options = {}) {
try {
const expanded = legacyDir.startsWith('~/')
? path.join(os.homedir(), legacyDir.slice(2))
: legacyDir === '~'
? os.homedir()
: legacyDir;
if (!(await fs.pathExists(expanded))) return;
const entries = await fs.readdir(expanded);
const bmadFiles = entries.filter((e) => typeof e === 'string' && e.startsWith('bmad'));
if (bmadFiles.length > 0 && !options.silent) {
await prompts.log.warn(`Found ${bmadFiles.length} stale BMAD file(s) in ${expanded}. Remove manually: rm ${expanded}/bmad-*`);
}
} catch {
// Errors reading global paths are silently ignored
}
}
/** /**
* Find the _bmad directory in a project * Find the _bmad directory in a project
* @param {string} projectDir - Project directory * @param {string} projectDir - Project directory
@ -426,8 +385,8 @@ class ConfigDrivenIdeSetup {
// Always preserve bmad-os-* utility skills regardless of cleanup mode // Always preserve bmad-os-* utility skills regardless of cleanup mode
if (entry.startsWith('bmad-os-')) continue; if (entry.startsWith('bmad-os-')) continue;
// Surgical removal from set, or legacy prefix matching when set is null // Surgical removal from set, or fallback to manifest+prefix detection when null
const shouldRemove = removalSet ? removalSet.has(entry) : entry.startsWith('bmad'); const shouldRemove = removalSet ? removalSet.has(entry) : isBmadOwnedEntry(entry, null);
if (shouldRemove) { if (shouldRemove) {
try { try {
@ -590,10 +549,9 @@ class ConfigDrivenIdeSetup {
try { try {
if (await fs.pathExists(candidatePath)) { if (await fs.pathExists(candidatePath)) {
const entries = await fs.readdir(candidatePath); const entries = await fs.readdir(candidatePath);
const hasBmad = entries.some( const ancestorBmadDir = await this._findBmadDir(current);
(e) => typeof e === 'string' && e.toLowerCase().startsWith('bmad') && !e.toLowerCase().startsWith('bmad-os-'), const canonicalIds = await getInstalledCanonicalIds(ancestorBmadDir);
); if (entries.some((e) => isBmadOwnedEntry(e, canonicalIds))) {
if (hasBmad) {
return candidatePath; return candidatePath;
} }
} }
@ -605,43 +563,6 @@ class ConfigDrivenIdeSetup {
return null; return null;
} }
/**
* Walk up ancestor directories from relativeDir toward projectDir, removing each if empty
* Stops at projectDir boundary never removes projectDir itself
* @param {string} projectDir - Project root (boundary)
* @param {string} relativeDir - Relative directory to start from
*/
async removeEmptyParents(projectDir, relativeDir) {
const resolvedProject = path.resolve(projectDir);
let current = relativeDir;
let last = null;
while (current && current !== '.' && current !== last) {
last = current;
const fullPath = path.resolve(projectDir, current);
// Boundary guard: never traverse outside projectDir
if (!fullPath.startsWith(resolvedProject + path.sep) && fullPath !== resolvedProject) break;
try {
if (!(await fs.pathExists(fullPath))) {
// Dir already gone — advance current; last is reset at top of next iteration
current = path.dirname(current);
continue;
}
const remaining = await fs.readdir(fullPath);
if (remaining.length > 0) break;
await fs.rmdir(fullPath);
} catch (error) {
// ENOTEMPTY: TOCTOU race (file added between readdir and rmdir) — skip level, continue upward
// ENOENT: dir removed by another process between pathExists and rmdir — skip level, continue upward
if (error.code === 'ENOTEMPTY' || error.code === 'ENOENT') {
current = path.dirname(current);
continue;
}
break; // fatal error (e.g. EACCES) — stop upward walk
}
current = path.dirname(current);
}
}
} }
module.exports = { ConfigDrivenIdeSetup }; module.exports = { ConfigDrivenIdeSetup };

View File

@ -160,8 +160,18 @@ class IdeManager {
let detail = ''; let detail = '';
if (handlerResult && handlerResult.results) { if (handlerResult && handlerResult.results) {
const r = handlerResult.results; const r = handlerResult.results;
const count = r.skillDirectories || r.skills || 0; let count = r.skillDirectories || r.skills || 0;
if (count > 0) detail = `${count} skills`; // 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) // Propagate handler's success status (default true for backward compat)
const success = handlerResult?.success !== false; const success = handlerResult?.success !== false;
@ -172,6 +182,57 @@ 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;
// Only claim the target when the install actually succeeded and wrote skills.
// If the first platform fails (ancestor conflict, exception, etc.), leave the
// dir unclaimed so the next peer becomes the new first writer instead of
// silently skipping into a broken/empty target_dir.
if (result.success && writtenCount > 0) {
claimedTargets.set(target, { firstIde: ideName, skillCount: writtenCount });
}
}
results.push(result);
}
return results;
}
/** /**
* Cleanup IDE configurations * Cleanup IDE configurations
* @param {string} projectDir - Project directory * @param {string} projectDir - Project directory
@ -198,6 +259,8 @@ class IdeManager {
* @param {string} projectDir - Project directory * @param {string} projectDir - Project directory
* @param {Array<string>} ideList - List of IDE names to clean up * @param {Array<string>} ideList - List of IDE names to clean up
* @param {Object} [options] - Cleanup options passed through to handlers * @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 * @returns {Array} Results array
*/ */
async cleanupByList(projectDir, ideList, options = {}) { async cleanupByList(projectDir, ideList, options = {}) {
@ -211,13 +274,27 @@ class IdeManager {
// Build lowercase lookup for case-insensitive matching // Build lowercase lookup for case-insensitive matching
const lowercaseHandlers = new Map([...this.handlers.entries()].map(([k, v]) => [k.toLowerCase(), v])); 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) { for (const ideName of ideList) {
const handler = lowercaseHandlers.get(ideName.toLowerCase()); const handler = lowercaseHandlers.get(ideName.toLowerCase());
if (!handler) continue; if (!handler) continue;
const target = handler.installerConfig?.target_dir || null;
const skipTarget = target && remainingTargets.has(target);
const cleanupOptions = skipTarget ? { ...options, skipTarget: true } : options;
try { try {
await handler.cleanup(projectDir, options); await handler.cleanup(projectDir, cleanupOptions);
results.push({ ide: ideName, success: true }); results.push({ ide: ideName, success: true, skippedTarget: !!skipTarget });
} catch (error) { } catch (error) {
results.push({ ide: ideName, success: false, error: error.message }); results.push({ ide: ideName, success: false, error: error.message });
} }

View File

@ -5,128 +5,203 @@
# 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)
# legacy_targets: (optional) Old target dirs to clean up on reinstall # 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:
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: antigravity:
name: "Google Antigravity" name: "Google Antigravity"
preferred: false preferred: false
installer: installer:
legacy_targets:
- .agent/workflows
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:
legacy_targets: target_dir: .agents/skills
- .augment/commands global_target_dir: ~/.agents/skills
target_dir: .augment/skills
bob:
name: "IBM Bob"
preferred: false
installer:
target_dir: .bob/skills
global_target_dir: ~/.bob/skills
claude-code: claude-code:
name: "Claude Code" name: "Claude Code"
preferred: true preferred: true
installer: installer:
legacy_targets:
- .claude/commands
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:
legacy_targets:
- .clinerules/workflows
target_dir: .cline/skills target_dir: .cline/skills
global_target_dir: ~/.cline/skills
codex: codex:
name: "Codex" name: "Codex"
preferred: false preferred: true
installer: installer:
legacy_targets:
- .codex/prompts
- ~/.codex/prompts
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:
legacy_targets:
- .codebuddy/commands
target_dir: .codebuddy/skills 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: crush:
name: "Crush" name: "Crush"
preferred: false preferred: false
installer: installer:
legacy_targets: target_dir: .agents/skills
- .crush/commands global_target_dir: ~/.config/agents/skills
target_dir: .crush/skills
cursor: cursor:
name: "Cursor" name: "Cursor"
preferred: true preferred: true
installer: installer:
legacy_targets: target_dir: .agents/skills
- .cursor/commands global_target_dir: ~/.agents/skills
target_dir: .cursor/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: gemini:
name: "Gemini CLI" name: "Gemini CLI"
preferred: false preferred: false
installer: installer:
legacy_targets: target_dir: .agents/skills
- .gemini/commands global_target_dir: ~/.agents/skills
target_dir: .gemini/skills
github-copilot: github-copilot:
name: "GitHub Copilot" name: "GitHub Copilot"
preferred: true
installer:
target_dir: .agents/skills
global_target_dir: ~/.agents/skills
goose:
name: "Block Goose"
preferred: false preferred: false
installer: installer:
legacy_targets: target_dir: .agents/skills
- .github/agents global_target_dir: ~/.config/agents/skills
- .github/prompts
target_dir: .github/skills
iflow: iflow:
name: "iFlow" name: "iFlow"
preferred: false preferred: false
installer: installer:
legacy_targets:
- .iflow/commands
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:
legacy_targets: target_dir: .agents/skills
- .kilocode/workflows global_target_dir: ~/.kilocode/skills
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:
legacy_targets:
- .kiro/steering
target_dir: .kiro/skills 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: ona:
name: "Ona" name: "Ona"
@ -134,65 +209,98 @@ platforms:
installer: installer:
target_dir: .ona/skills target_dir: .ona/skills
openclaw:
name: "OpenClaw"
preferred: false
installer:
target_dir: .agents/skills
global_target_dir: ~/.agents/skills
opencode: opencode:
name: "OpenCode" name: "OpenCode"
preferred: false preferred: false
installer: installer:
legacy_targets: target_dir: .agents/skills
- .opencode/agents global_target_dir: ~/.agents/skills
- .opencode/commands
- .opencode/agent openhands:
- .opencode/command name: "OpenHands"
target_dir: .opencode/skills preferred: false
installer:
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
pochi:
name: "Pochi"
preferred: false
installer:
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:
legacy_targets:
- .qwen/commands
target_dir: .qwen/skills target_dir: .qwen/skills
global_target_dir: ~/.qwen/skills
replit:
name: "Replit Agent"
preferred: false
installer:
target_dir: .agents/skills
roo: roo:
name: "Roo Code" name: "Roo Code"
preferred: false preferred: false
installer: installer:
legacy_targets: target_dir: .agents/skills
- .roo/commands global_target_dir: ~/.agents/skills
target_dir: .roo/skills
rovo-dev: rovo-dev:
name: "Rovo Dev" name: "Rovo Dev"
preferred: false preferred: false
installer: installer:
legacy_targets: target_dir: .agents/skills
- .rovodev/workflows global_target_dir: ~/.agents/skills
target_dir: .rovodev/skills
trae: trae:
name: "Trae" name: "Trae"
preferred: false preferred: false
installer: installer:
legacy_targets:
- .trae/rules
target_dir: .trae/skills target_dir: .trae/skills
warp:
name: "Warp"
preferred: false
installer:
target_dir: .agents/skills
global_target_dir: ~/.agents/skills
windsurf: windsurf:
name: "Windsurf" name: "Windsurf"
preferred: false preferred: false
installer: installer:
legacy_targets: target_dir: .agents/skills
- .windsurf/workflows global_target_dir: ~/.agents/skills
target_dir: .windsurf/skills
zencoder:
name: "Zencoder"
preferred: false
installer:
target_dir: .zencoder/skills
global_target_dir: ~/.zencoder/skills

View File

@ -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 };

View File

@ -2,6 +2,7 @@ const path = require('node:path');
const os = require('node:os'); const os = require('node:os');
const semver = require('semver'); const semver = require('semver');
const fs = require('./fs-native'); const fs = require('./fs-native');
const installerPackageJson = require('../../package.json');
const { CLIUtils } = require('./cli-utils'); const { CLIUtils } = require('./cli-utils');
const { ExternalModuleManager } = require('./modules/external-manager'); const { ExternalModuleManager } = require('./modules/external-manager');
const { resolveModuleVersion } = require('./modules/version-resolver'); const { resolveModuleVersion } = require('./modules/version-resolver');
@ -128,6 +129,24 @@ class UI {
await prompts.log.warn(warning); await prompts.log.warn(warning);
} }
// When the user launched the installer from a prerelease (npx bmad-method@next),
// mirror that intent for external modules: seed the global channel to 'next' so
// the module picker's version labels resolve from main HEAD (matching what
// actually gets installed) and the interactive channel gate skips — the user
// already declared "next" intent by typing @next. Explicit channel flags
// override this seed.
if (
semver.prerelease(installerPackageJson.version) !== null &&
!channelOptions.global &&
channelOptions.nextSet.size === 0 &&
channelOptions.pins.size === 0
) {
channelOptions.global = 'next';
await prompts.log.info(
'Launched from a prerelease — installing all external modules from main HEAD (next channel). Pass --all-stable or --pin to override.',
);
}
// Get directory from options or prompt // Get directory from options or prompt
let confirmedDirectory; let confirmedDirectory;
if (options.directory) { if (options.directory) {
@ -332,8 +351,10 @@ class UI {
// Interactive channel gate: "Ready to install (all stable)? [Y/n]" // Interactive channel gate: "Ready to install (all stable)? [Y/n]"
// Only shown for fresh installs with no channel flags and an external module // Only shown for fresh installs with no channel flags and an external module
// selected. Non-interactive installs skip this and fall through to the // selected. Skipped for prerelease launches because channelOptions.global
// registry default (stable) or whatever flags were supplied. // was already seeded to 'next' upstream. Non-interactive installs skip this
// and fall through to the registry default (stable) or whatever flags were
// supplied.
await this._interactiveChannelGate({ options, channelOptions, selectedModules }); await this._interactiveChannelGate({ options, channelOptions, selectedModules });
let toolSelection = await this.promptToolSelection(confirmedDirectory, options); let toolSelection = await this.promptToolSelection(confirmedDirectory, options);
@ -1783,7 +1804,9 @@ class UI {
* *
* Skipped when: * Skipped when:
* - running non-interactively (--yes) * - running non-interactively (--yes)
* - the user already passed channel flags (--channel / --pin / --next) * - the user already passed channel flags (--channel / --pin / --next), OR
* the installer was launched from a prerelease (which seeds
* channelOptions.global = 'next' upstream in promptInstall)
* - no externals/community modules are selected * - no externals/community modules are selected
* *
* Mutates channelOptions.pins and channelOptions.nextSet to reflect picker choices. * Mutates channelOptions.pins and channelOptions.nextSet to reflect picker choices.

View File

@ -1,175 +0,0 @@
# BMAD Platform Codes Configuration
# Central configuration for all platform/IDE codes used in the BMAD system
#
# This file defines the standardized platform codes that are used throughout
# the installation system to identify different platforms (IDEs, tools, etc.)
#
# Format:
# code: Platform identifier used internally
# name: Display name shown to users
# preferred: Whether this platform is shown as a recommended option on install
# category: Type of platform (ide, tool, service, etc.)
platforms:
# Recommended Platforms
claude-code:
name: "Claude Code"
preferred: true
category: cli
description: "Anthropic's official CLI for Claude"
cursor:
name: "Cursor"
preferred: true
category: ide
description: "AI-first code editor"
# Other IDEs and Tools
cline:
name: "Cline"
preferred: false
category: ide
description: "AI coding assistant"
opencode:
name: "OpenCode"
preferred: false
category: ide
description: "OpenCode terminal coding assistant"
codebuddy:
name: "CodeBuddy"
preferred: false
category: ide
description: "Tencent Cloud Code Assistant - AI-powered coding companion"
auggie:
name: "Auggie"
preferred: false
category: cli
description: "AI development tool"
roo:
name: "Roo Code"
preferred: false
category: ide
description: "Enhanced Cline fork"
rovo-dev:
name: "Rovo Dev"
preferred: false
category: ide
description: "Atlassian's Rovo development environment"
kiro:
name: "Kiro"
preferred: false
category: ide
description: "Amazon's AI-powered IDE"
github-copilot:
name: "GitHub Copilot"
preferred: false
category: ide
description: "GitHub's AI pair programmer"
codex:
name: "Codex"
preferred: false
category: cli
description: "OpenAI Codex integration"
qwen:
name: "QwenCoder"
preferred: false
category: ide
description: "Qwen AI coding assistant"
gemini:
name: "Gemini CLI"
preferred: false
category: cli
description: "Google's CLI for Gemini"
iflow:
name: "iFlow"
preferred: false
category: ide
description: "AI workflow automation"
kilo:
name: "KiloCoder"
preferred: false
category: ide
description: "AI coding platform"
kimi-code:
name: "Kimi Code"
preferred: false
category: cli
description: "Moonshot AI's Kimi Code CLI"
crush:
name: "Crush"
preferred: false
category: ide
description: "AI development assistant"
antigravity:
name: "Google Antigravity"
preferred: false
category: ide
description: "Google's AI development environment"
trae:
name: "Trae"
preferred: false
category: ide
description: "AI coding tool"
windsurf:
name: "Windsurf"
preferred: false
category: ide
description: "AI-powered IDE with cascade flows"
junie:
name: "Junie"
preferred: false
category: cli
description: "AI coding agent by JetBrains"
ona:
name: "Ona"
preferred: false
category: ide
description: "Ona AI development environment"
# Platform categories
categories:
ide:
name: "Integrated Development Environment"
description: "Full-featured code editors with AI assistance"
cli:
name: "Command Line Interface"
description: "Terminal-based tools"
tool:
name: "Development Tool"
description: "Standalone development utilities"
service:
name: "Cloud Service"
description: "Cloud-based development platforms"
extension:
name: "Editor Extension"
description: "Plugins for existing editors"
# Naming conventions and rules
conventions:
code_format: "lowercase-kebab-case"
name_format: "Title Case"
max_code_length: 20
allowed_characters: "a-z0-9-"