Compare commits
1 Commits
ac96f19f88
...
fcdf14cc79
| Author | SHA1 | Date |
|---|---|---|
|
|
fcdf14cc79 |
|
|
@ -7,7 +7,6 @@ on:
|
||||||
- "src/**"
|
- "src/**"
|
||||||
- "tools/installer/**"
|
- "tools/installer/**"
|
||||||
- "package.json"
|
- "package.json"
|
||||||
- "removals.txt"
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
channel:
|
channel:
|
||||||
|
|
@ -136,22 +135,6 @@ 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
|
||||||
|
|
|
||||||
69
CHANGELOG.md
69
CHANGELOG.md
|
|
@ -1,74 +1,5 @@
|
||||||
# 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
|
||||||
|
|
|
||||||
|
|
@ -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/installer/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/cli/installers/lib/ide/platform-codes.yaml).
|
||||||
|
|
||||||
## Režimy instalace
|
## Režimy instalace
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "bmad-method",
|
"name": "bmad-method",
|
||||||
"version": "6.5.0",
|
"version": "6.3.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "bmad-method",
|
"name": "bmad-method",
|
||||||
"version": "6.5.0",
|
"version": "6.3.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/core": "^1.0.0",
|
"@clack/core": "^1.0.0",
|
||||||
|
|
|
||||||
|
|
@ -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.5.0",
|
"version": "6.3.0",
|
||||||
"description": "Breakthrough Method of Agile AI-driven Development",
|
"description": "Breakthrough Method of Agile AI-driven Development",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"agile",
|
"agile",
|
||||||
|
|
|
||||||
37
removals.txt
37
removals.txt
|
|
@ -15,40 +15,3 @@ 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
|
|
||||||
|
|
|
||||||
|
|
@ -139,10 +139,19 @@ 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 === '.agents/skills', 'Windsurf target_dir uses native skills path');
|
assert(windsurfInstaller?.target_dir === '.windsurf/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();
|
||||||
|
|
@ -153,9 +162,11 @@ 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, '.agents', 'skills', 'bmad-master', 'SKILL.md');
|
const skillFile = path.join(tempProjectDir, '.windsurf', '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) {
|
||||||
|
|
@ -176,8 +187,17 @@ 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();
|
||||||
|
|
@ -191,6 +211,8 @@ 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) {
|
||||||
|
|
@ -211,8 +233,17 @@ 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();
|
||||||
|
|
@ -226,6 +257,8 @@ 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) {
|
||||||
|
|
@ -244,7 +277,12 @@ 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 === '.agents/skills', 'Auggie target_dir uses native skills path');
|
assert(auggieInstaller?.target_dir === '.augment/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,
|
||||||
|
|
@ -253,6 +291,10 @@ 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();
|
||||||
|
|
@ -263,9 +305,11 @@ 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, '.agents', 'skills', 'bmad-master', 'SKILL.md');
|
const skillFile = path.join(tempProjectDir, '.augment', '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) {
|
||||||
|
|
@ -284,10 +328,30 @@ 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 === '.agents/skills', 'OpenCode target_dir uses native skills path');
|
assert(opencodeInstaller?.target_dir === '.opencode/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();
|
||||||
|
|
@ -298,9 +362,16 @@ 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, '.agents', 'skills', 'bmad-master', 'SKILL.md');
|
const skillFile = path.join(tempProjectDir, '.opencode', '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) {
|
||||||
|
|
@ -321,8 +392,16 @@ 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();
|
||||||
|
|
@ -341,6 +420,8 @@ 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) {
|
||||||
|
|
@ -363,8 +444,16 @@ 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();
|
||||||
|
|
@ -383,6 +472,8 @@ 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) {
|
||||||
|
|
@ -403,12 +494,20 @@ 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 === '.agents/skills', 'Cursor target_dir uses native skills path');
|
assert(cursorInstaller?.target_dir === '.cursor/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();
|
||||||
|
|
@ -419,7 +518,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, '.agents', 'skills', 'bmad-master', 'SKILL.md');
|
const skillFile13c = path.join(tempProjectDir13c, '.cursor', '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
|
||||||
|
|
@ -427,6 +526,8 @@ 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) {
|
||||||
|
|
@ -445,10 +546,19 @@ 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 === '.agents/skills', 'Roo target_dir uses native skills path');
|
assert(rooInstaller?.target_dir === '.roo/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();
|
||||||
|
|
@ -459,7 +569,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, '.agents', 'skills', 'bmad-master', 'SKILL.md');
|
const skillFile13 = path.join(tempProjectDir13, '.roo', '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)
|
||||||
|
|
@ -470,6 +580,8 @@ 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,
|
||||||
|
|
@ -503,13 +615,31 @@ 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 === '.agents/skills', 'GitHub Copilot target_dir uses native skills path');
|
assert(copilotInstaller?.target_dir === '.github/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',
|
||||||
|
|
@ -524,7 +654,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, '.agents', 'skills', 'bmad-master', 'SKILL.md');
|
const skillFile17 = path.join(tempProjectDir17, '.github', '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
|
||||||
|
|
@ -532,6 +662,10 @@ 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(
|
||||||
|
|
@ -563,8 +697,17 @@ 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();
|
||||||
|
|
@ -583,6 +726,8 @@ 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,
|
||||||
|
|
@ -612,8 +757,17 @@ 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();
|
||||||
|
|
@ -631,6 +785,8 @@ 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'],
|
||||||
|
|
@ -657,10 +813,19 @@ 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 === '.agents/skills', 'Crush target_dir uses native skills path');
|
assert(crushInstaller?.target_dir === '.crush/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();
|
||||||
|
|
@ -671,13 +836,15 @@ 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, '.agents', 'skills', 'bmad-master', 'SKILL.md');
|
const skillFile20 = path.join(tempProjectDir20, '.crush', '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'],
|
||||||
|
|
@ -706,8 +873,16 @@ 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();
|
||||||
|
|
@ -725,6 +900,8 @@ 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'],
|
||||||
|
|
@ -753,7 +930,12 @@ async function runTests() {
|
||||||
|
|
||||||
assert(!kiloConfig22?.suspended, 'KiloCoder is not suspended');
|
assert(!kiloConfig22?.suspended, 'KiloCoder is not suspended');
|
||||||
|
|
||||||
assert(kiloConfig22?.installer?.target_dir === '.agents/skills', 'KiloCoder target_dir uses native skills path');
|
assert(kiloConfig22?.installer?.target_dir === '.kilocode/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();
|
||||||
|
|
@ -768,6 +950,11 @@ 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'],
|
||||||
|
|
@ -775,13 +962,15 @@ 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, '.agents', 'skills', 'bmad-master', 'SKILL.md');
|
const skillFile22 = path.join(tempProjectDir22, '.kilocode', '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'],
|
||||||
|
|
@ -808,10 +997,18 @@ 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 === '.agents/skills', 'Gemini target_dir uses native skills path');
|
assert(geminiInstaller?.target_dir === '.gemini/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();
|
||||||
|
|
@ -822,13 +1019,15 @@ 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, '.agents', 'skills', 'bmad-master', 'SKILL.md');
|
const skillFile23 = path.join(tempProjectDir23, '.gemini', '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'],
|
||||||
|
|
@ -856,9 +1055,16 @@ 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();
|
||||||
|
|
@ -877,6 +1083,8 @@ 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) {
|
||||||
|
|
@ -896,9 +1104,16 @@ 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();
|
||||||
|
|
@ -917,6 +1132,8 @@ 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) {
|
||||||
|
|
@ -935,10 +1152,17 @@ 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 === '.agents/skills', 'Rovo Dev target_dir uses native skills path');
|
assert(rovoInstaller?.target_dir === '.rovodev/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');
|
||||||
|
|
@ -949,7 +1173,6 @@ 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();
|
||||||
|
|
@ -961,7 +1184,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, '.agents', 'skills', 'bmad-master', 'SKILL.md');
|
const skillFile26 = path.join(tempProjectDir26, '.rovodev', '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
|
||||||
|
|
@ -969,6 +1192,8 @@ 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(
|
||||||
|
|
@ -1070,7 +1295,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 === '.agents/skills', 'Pi target_dir uses native skills path');
|
assert(piInstaller?.target_dir === '.pi/skills', 'Pi target_dir uses native skills path');
|
||||||
|
|
||||||
tempProjectDir28 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-pi-test-'));
|
tempProjectDir28 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-pi-test-'));
|
||||||
installedBmadDir28 = await createTestBmadFixture();
|
installedBmadDir28 = await createTestBmadFixture();
|
||||||
|
|
@ -1100,7 +1325,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, '.agents', 'skills', 'bmad-master', 'SKILL.md');
|
const skillFile28 = path.join(tempProjectDir28, '.pi', '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
|
||||||
|
|
@ -1382,7 +1607,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 → .agent/skills', 'Installer detail reports skill count and target dir');
|
assert(result.detail === '1 skills', 'Installer detail reports skill count');
|
||||||
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(
|
||||||
|
|
@ -2622,157 +2847,6 @@ 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
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -222,6 +222,7 @@ 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
|
||||||
|
|
@ -235,6 +236,7 @@ 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
|
||||||
|
|
@ -247,6 +249,7 @@ 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
|
||||||
|
|
@ -259,6 +262,7 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,13 @@ checkForUpdate().catch(() => {
|
||||||
|
|
||||||
async function checkForUpdate() {
|
async function checkForUpdate() {
|
||||||
try {
|
try {
|
||||||
// Prereleases (e.g. 6.5.1-next.0) live on the `next` dist-tag; stable
|
// For beta versions, check the beta tag; otherwise check latest
|
||||||
// releases live on `latest`. semver.prerelease() returns null for stable,
|
const isBeta =
|
||||||
// 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('beta') ||
|
||||||
|
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',
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ 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() {
|
||||||
|
|
@ -42,16 +41,6 @@ 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);
|
||||||
|
|
@ -194,16 +183,15 @@ class Installer {
|
||||||
|
|
||||||
if (toRemove.length === 0) return;
|
if (toRemove.length === 0) return;
|
||||||
|
|
||||||
// Pass the newly-selected list as remainingIdes so cleanupByList skips
|
await this.ideManager.ensureInitialized();
|
||||||
// target_dir wipes for IDEs whose directory is still owned by a peer
|
for (const ide of toRemove) {
|
||||||
// (e.g. removing 'cursor' while 'gemini' remains — both share .agents/skills).
|
try {
|
||||||
const results = await this.ideManager.cleanupByList(paths.projectRoot, toRemove, {
|
const handler = this.ideManager.handlers.get(ide);
|
||||||
remainingIdes: [...newlySelected],
|
if (handler) {
|
||||||
});
|
await handler.cleanup(paths.projectRoot);
|
||||||
|
}
|
||||||
for (const result of results || []) {
|
} catch (error) {
|
||||||
if (result && result.success === false) {
|
await prompts.log.warn(`Warning: Failed to remove ${ide}: ${error.message}`);
|
||||||
await prompts.log.warn(`Warning: Failed to remove ${result.ide}: ${result.error || 'unknown error'}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -354,14 +342,13 @@ class Installer {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const setupResults = await this.ideManager.setupBatch(validIdes, paths.projectRoot, paths.bmadDir, {
|
for (const ide of validIdes) {
|
||||||
selectedModules: allModules || [],
|
const setupResult = await this.ideManager.setup(ide, paths.projectRoot, paths.bmadDir, {
|
||||||
verbose: config.verbose,
|
selectedModules: allModules || [],
|
||||||
previousSkillIds,
|
verbose: config.verbose,
|
||||||
});
|
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 {
|
||||||
|
|
|
||||||
|
|
@ -1,151 +0,0 @@
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
@ -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 { getInstalledCanonicalIds, isBmadOwnedEntry } = require('./shared/install
|
||||||
* 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
|
||||||
* - IDE-specific marker removal (copilot-instructions, kilo modes, rovodev prompts)
|
* - Legacy directory cleanup and IDE-specific marker removal
|
||||||
*/
|
*/
|
||||||
class ConfigDrivenIdeSetup {
|
class ConfigDrivenIdeSetup {
|
||||||
constructor(platformCode, platformConfig) {
|
constructor(platformCode, platformConfig) {
|
||||||
|
|
@ -44,20 +44,16 @@ class ConfigDrivenIdeSetup {
|
||||||
async detect(projectDir) {
|
async detect(projectDir) {
|
||||||
if (!this.configDir) return false;
|
if (!this.configDir) return false;
|
||||||
|
|
||||||
const root = projectDir || process.cwd();
|
const dir = path.join(projectDir || process.cwd(), this.configDir);
|
||||||
const dir = path.join(root, this.configDir);
|
if (await fs.pathExists(dir)) {
|
||||||
if (!(await fs.pathExists(dir))) return false;
|
try {
|
||||||
|
const entries = await fs.readdir(dir);
|
||||||
let entries;
|
return entries.some((e) => typeof e === 'string' && e.startsWith('bmad'));
|
||||||
try {
|
} catch {
|
||||||
entries = await fs.readdir(dir);
|
return false;
|
||||||
} catch {
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
const bmadDir = await this._findBmadDir(root);
|
|
||||||
const canonicalIds = await getInstalledCanonicalIds(bmadDir);
|
|
||||||
return entries.some((e) => isBmadOwnedEntry(e, canonicalIds));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -96,12 +92,6 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
@ -232,6 +222,27 @@ 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);
|
||||||
|
|
@ -247,17 +258,47 @@ 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
|
||||||
|
|
@ -385,8 +426,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 fallback to manifest+prefix detection when null
|
// Surgical removal from set, or legacy prefix matching when set is null
|
||||||
const shouldRemove = removalSet ? removalSet.has(entry) : isBmadOwnedEntry(entry, null);
|
const shouldRemove = removalSet ? removalSet.has(entry) : entry.startsWith('bmad');
|
||||||
|
|
||||||
if (shouldRemove) {
|
if (shouldRemove) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -549,9 +590,10 @@ 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 ancestorBmadDir = await this._findBmadDir(current);
|
const hasBmad = entries.some(
|
||||||
const canonicalIds = await getInstalledCanonicalIds(ancestorBmadDir);
|
(e) => typeof e === 'string' && e.toLowerCase().startsWith('bmad') && !e.toLowerCase().startsWith('bmad-os-'),
|
||||||
if (entries.some((e) => isBmadOwnedEntry(e, canonicalIds))) {
|
);
|
||||||
|
if (hasBmad) {
|
||||||
return candidatePath;
|
return candidatePath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -563,6 +605,43 @@ 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 };
|
||||||
|
|
|
||||||
|
|
@ -160,18 +160,8 @@ class IdeManager {
|
||||||
let detail = '';
|
let detail = '';
|
||||||
if (handlerResult && handlerResult.results) {
|
if (handlerResult && handlerResult.results) {
|
||||||
const r = handlerResult.results;
|
const r = handlerResult.results;
|
||||||
let count = r.skillDirectories || r.skills || 0;
|
const count = r.skillDirectories || r.skills || 0;
|
||||||
// Dedup'd platform: report the count its peer wrote so the user sees
|
if (count > 0) detail = `${count} skills`;
|
||||||
// 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;
|
||||||
|
|
@ -182,57 +172,6 @@ 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
|
||||||
|
|
@ -259,8 +198,6 @@ 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 = {}) {
|
||||||
|
|
@ -274,27 +211,13 @@ 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, cleanupOptions);
|
await handler.cleanup(projectDir, options);
|
||||||
results.push({ ide: ideName, success: true, skippedTarget: !!skipTarget });
|
results.push({ ide: ideName, success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
results.push({ ide: ideName, success: false, error: error.message });
|
results.push({ ide: ideName, success: false, error: error.message });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,203 +5,128 @@
|
||||||
# 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 (project/workspace)
|
# target_dir: Directory where skill directories are installed
|
||||||
# global_target_dir: (optional) User-home directory for global install
|
# legacy_targets: (optional) Old target dirs to clean up on reinstall
|
||||||
# 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:
|
||||||
target_dir: .agents/skills
|
legacy_targets:
|
||||||
global_target_dir: ~/.agents/skills
|
- .augment/commands
|
||||||
|
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: true
|
preferred: false
|
||||||
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:
|
||||||
target_dir: .agents/skills
|
legacy_targets:
|
||||||
global_target_dir: ~/.config/agents/skills
|
- .crush/commands
|
||||||
|
target_dir: .crush/skills
|
||||||
|
|
||||||
cursor:
|
cursor:
|
||||||
name: "Cursor"
|
name: "Cursor"
|
||||||
preferred: true
|
preferred: true
|
||||||
installer:
|
installer:
|
||||||
target_dir: .agents/skills
|
legacy_targets:
|
||||||
global_target_dir: ~/.agents/skills
|
- .cursor/commands
|
||||||
|
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:
|
||||||
target_dir: .agents/skills
|
legacy_targets:
|
||||||
global_target_dir: ~/.agents/skills
|
- .gemini/commands
|
||||||
|
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:
|
||||||
target_dir: .agents/skills
|
legacy_targets:
|
||||||
global_target_dir: ~/.config/agents/skills
|
- .github/agents
|
||||||
|
- .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: .junie/skills
|
target_dir: .agents/skills
|
||||||
global_target_dir: ~/.junie/skills
|
|
||||||
|
|
||||||
kilo:
|
kilo:
|
||||||
name: "KiloCoder"
|
name: "KiloCoder"
|
||||||
preferred: false
|
preferred: false
|
||||||
installer:
|
installer:
|
||||||
target_dir: .agents/skills
|
legacy_targets:
|
||||||
global_target_dir: ~/.kilocode/skills
|
- .kilocode/workflows
|
||||||
|
target_dir: .kilocode/skills
|
||||||
|
|
||||||
kimi-code:
|
kimi-code:
|
||||||
name: "Kimi Code"
|
name: "Kimi Code"
|
||||||
preferred: false
|
preferred: false
|
||||||
installer:
|
installer:
|
||||||
target_dir: .agents/skills
|
target_dir: .kimi/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"
|
||||||
|
|
@ -209,98 +134,65 @@ 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:
|
||||||
target_dir: .agents/skills
|
legacy_targets:
|
||||||
global_target_dir: ~/.agents/skills
|
- .opencode/agents
|
||||||
|
- .opencode/commands
|
||||||
openhands:
|
- .opencode/agent
|
||||||
name: "OpenHands"
|
- .opencode/command
|
||||||
preferred: false
|
target_dir: .opencode/skills
|
||||||
installer:
|
|
||||||
target_dir: .agents/skills
|
|
||||||
global_target_dir: ~/.agents/skills
|
|
||||||
|
|
||||||
pi:
|
pi:
|
||||||
name: "Pi"
|
name: "Pi"
|
||||||
preferred: false
|
preferred: false
|
||||||
installer:
|
installer:
|
||||||
target_dir: .agents/skills
|
target_dir: .pi/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:
|
||||||
target_dir: .agents/skills
|
legacy_targets:
|
||||||
global_target_dir: ~/.agents/skills
|
- .roo/commands
|
||||||
|
target_dir: .roo/skills
|
||||||
|
|
||||||
rovo-dev:
|
rovo-dev:
|
||||||
name: "Rovo Dev"
|
name: "Rovo Dev"
|
||||||
preferred: false
|
preferred: false
|
||||||
installer:
|
installer:
|
||||||
target_dir: .agents/skills
|
legacy_targets:
|
||||||
global_target_dir: ~/.agents/skills
|
- .rovodev/workflows
|
||||||
|
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:
|
||||||
target_dir: .agents/skills
|
legacy_targets:
|
||||||
global_target_dir: ~/.agents/skills
|
- .windsurf/workflows
|
||||||
|
target_dir: .windsurf/skills
|
||||||
zencoder:
|
|
||||||
name: "Zencoder"
|
|
||||||
preferred: false
|
|
||||||
installer:
|
|
||||||
target_dir: .zencoder/skills
|
|
||||||
global_target_dir: ~/.zencoder/skills
|
|
||||||
|
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
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 };
|
|
||||||
|
|
@ -2,7 +2,6 @@ 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');
|
||||||
|
|
@ -129,24 +128,6 @@ 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) {
|
||||||
|
|
@ -351,10 +332,8 @@ 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. Skipped for prerelease launches because channelOptions.global
|
// selected. Non-interactive installs skip this and fall through to the
|
||||||
// was already seeded to 'next' upstream. Non-interactive installs skip this
|
// registry default (stable) or whatever flags were supplied.
|
||||||
// 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);
|
||||||
|
|
@ -1804,9 +1783,7 @@ class UI {
|
||||||
*
|
*
|
||||||
* Skipped when:
|
* Skipped when:
|
||||||
* - running non-interactively (--yes)
|
* - running non-interactively (--yes)
|
||||||
* - the user already passed channel flags (--channel / --pin / --next), OR
|
* - the user already passed channel flags (--channel / --pin / --next)
|
||||||
* 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.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,175 @@
|
||||||
|
# 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-"
|
||||||
Loading…
Reference in New Issue