Compare commits
5 Commits
b15708a3b7
...
9277ae2b2e
| Author | SHA1 | Date |
|---|---|---|
|
|
9277ae2b2e | |
|
|
7f7690dbfd | |
|
|
a92f5d626b | |
|
|
1d5a3caec5 | |
|
|
97d32405d0 |
51
CHANGELOG.md
51
CHANGELOG.md
|
|
@ -1,5 +1,56 @@
|
|||
# Changelog
|
||||
|
||||
## v6.3.0 - 2026-04-09
|
||||
|
||||
### 💥 Breaking Changes
|
||||
|
||||
* Remove custom content installation feature; use marketplace-based plugin installation instead (#2227)
|
||||
* Remove bmad-init skill; all agents and skills now load config directly from `{project-root}/_bmad/bmm/config.yaml` (#2159)
|
||||
* Remove spec-wip.md singleton; quick-dev now writes directly to `spec-{slug}.md` with status field, enabling parallel sessions (#2214)
|
||||
* Consolidate three agent personas into Developer agent (Amelia): remove Barry quick-flow-solo-dev (#2177), Quinn QA agent (#2179), and Bob Scrum Master agent (#2186)
|
||||
|
||||
### 🎁 Features
|
||||
|
||||
* Universal source support for custom module installs with 5-strategy PluginResolver cascade supporting any Git host (GitHub, GitLab, Bitbucket, self-hosted) and local file paths (#2233)
|
||||
* Community module browser with three-tier selection: official, community (category drill-down from marketplace index), and custom URL with unverified source warning (#2229)
|
||||
* Switch module source of truth from bundled config to remote marketplace registry with network-failure fallback (#2228)
|
||||
* Add bmad-prfaq skill implementing Amazon's Working Backwards methodology as alternative Phase 1 analysis path with 5-stage coached workflow and subagent architecture (#2157)
|
||||
* Add bmad-checkpoint-preview skill for guided, concern-ordered human review of commits, branches, or PRs (#2145)
|
||||
* Epic context compilation for quick-dev step-01: sub-agent compiles planning docs into cached `epic-{N}-context.md` for story implementation (#2218)
|
||||
* Previous story continuity in quick-dev: load completed spec from same epic as implementation context (#2201)
|
||||
* Planning artifact awareness in quick-dev: selectively load PRD, architecture, UX, and epics docs for context-informed specs (#2185)
|
||||
* One-shot route now generates lightweight spec trace file for consistent artifact tracking (#2121)
|
||||
* Improve checkpoint-preview UX with clickable spec paths, external edit detection, and missing-file halt (#2217)
|
||||
* Add Junie (JetBrains AI) platform support (#2142)
|
||||
* Restore KiloCoder support with native-skills installation (#2151)
|
||||
* Add bmad-help support for llms.txt general questions (#2230)
|
||||
|
||||
### ♻️ Refactoring
|
||||
|
||||
* Consolidate party-mode into single SKILL.md with real subagent spawning via Agent tool, replacing multi-file workflow architecture (#2160)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* Fix version display bug where marketplace.json walk-up reported wrong version (#2233)
|
||||
* Fix checkpoint-preview step-05 advancing without user confirmation by adding explicit HALT (#2184)
|
||||
* Address adversarial triage findings: clarify review_mode transitions, label walkthrough branches, fix terse commit handling (#2180)
|
||||
* Preserve local custom module sources during quick update (#2172)
|
||||
* Support skills/ folder as fallback module source location for bmb compatibility (#2149)
|
||||
|
||||
### 🔧 Maintenance
|
||||
|
||||
* Overhaul installer branding with responsive BMAD METHOD logo, blue color scheme, unified version sourcing from marketplace.json, and surgical manifest-based skill cleanup (#2223)
|
||||
* Stop copying skill prompts to _bmad by default (#2182)
|
||||
* Add Python 3.10+ and uv as documented prerequisites (#2221)
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
* Complete Czech (cs-CZ) documentation translation (#2134)
|
||||
* Complete Vietnamese (vi-VN) documentation translation (#2110, #2192)
|
||||
* Rewrite get-answers-about-bmad as 1-2-3 escalation flow, remove deprecated references (#2213)
|
||||
* Add checkpoint-preview explainer page and workflow diagram (#2183)
|
||||
* Update docs theme to match bmadcode.com with responsive logo and blue color scheme (#2176)
|
||||
|
||||
## v6.2.2 - 2026-03-25
|
||||
|
||||
### ♻️ Refactoring
|
||||
|
|
|
|||
15
README_VN.md
15
README_VN.md
|
|
@ -3,6 +3,8 @@
|
|||
[](https://www.npmjs.com/package/bmad-method)
|
||||
[](LICENSE)
|
||||
[](https://nodejs.org)
|
||||
[](https://www.python.org)
|
||||
[](https://docs.astral.sh/uv/)
|
||||
[](https://discord.gg/gk8jAdXWmj)
|
||||
|
||||
[English](README.md) | [简体中文](README_CN.md) | Tiếng Việt
|
||||
|
|
@ -36,7 +38,7 @@ Các công cụ AI truyền thống thường làm thay phần suy nghĩ của b
|
|||
|
||||
## Bắt đầu nhanh
|
||||
|
||||
**Điều kiện tiên quyết**: [Node.js](https://nodejs.org) v20+
|
||||
**Điều kiện tiên quyết**: [Node.js](https://nodejs.org) v20+ · [Python](https://www.python.org) 3.10+ · [uv](https://docs.astral.sh/uv/)
|
||||
|
||||
```bash
|
||||
npx bmad-method install
|
||||
|
|
@ -80,18 +82,15 @@ BMad Method có thể được mở rộng bằng các mô-đun chính thức ch
|
|||
## Cộng đồng
|
||||
|
||||
- [Discord](https://discord.gg/gk8jAdXWmj) - Nhận trợ giúp, chia sẻ ý tưởng, cộng tác
|
||||
- [Đăng ký trên YouTube](https://www.youtube.com/@BMadCode) - video hướng dẫn, lớp chuyên sâu và podcast (ra mắt tháng 2 năm 2025)
|
||||
- [YouTube](https://youtube.com/@BMadCode) - Video hướng dẫn, master class và nhiều nội dung khác
|
||||
- [X / Twitter](https://x.com/BMadCode)
|
||||
- [Website](https://bmadcode.com)
|
||||
- [GitHub Issues](https://github.com/bmad-code-org/BMAD-METHOD/issues) - Báo lỗi và yêu cầu tính năng
|
||||
- [Discussions](https://github.com/bmad-code-org/BMAD-METHOD/discussions) - Trao đổi cộng đồng
|
||||
|
||||
## Hỗ trợ BMad
|
||||
|
||||
BMad miễn phí cho tất cả mọi người - và sẽ luôn như vậy. Nếu bạn muốn hỗ trợ quá trình phát triển:
|
||||
|
||||
- ⭐ Hãy nhấn sao cho dự án ở góc trên bên phải của trang này
|
||||
- ☕ [Buy Me a Coffee](https://buymeacoffee.com/bmad) - Tiếp thêm năng lượng cho quá trình phát triển
|
||||
- 🏢 Tài trợ doanh nghiệp - Nhắn riêng trên Discord
|
||||
- 🎤 Diễn thuyết và truyền thông - Sẵn sàng cho hội nghị, podcast, phỏng vấn (BM trên Discord)
|
||||
BMad miễn phí cho tất cả mọi người và sẽ luôn như vậy. Hãy nhấn sao cho repo này, [mời tôi một ly cà phê](https://buymeacoffee.com/bmad), hoặc gửi email tới <contact@bmadcode.com> nếu bạn muốn tài trợ doanh nghiệp.
|
||||
|
||||
## Đóng góp
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
title: "How to Customize BMad"
|
||||
title: 'How to Customize BMad'
|
||||
description: Customize agents, workflows, and modules while preserving update compatibility
|
||||
sidebar:
|
||||
order: 7
|
||||
order: 8
|
||||
---
|
||||
|
||||
Use the `.customize.yaml` files to tailor agent behavior, personas, and menus while preserving your changes across updates.
|
||||
|
|
@ -15,9 +15,10 @@ Use the `.customize.yaml` files to tailor agent behavior, personas, and menus wh
|
|||
- You want agents to perform specific actions every time they start up
|
||||
|
||||
:::note[Prerequisites]
|
||||
|
||||
- BMad installed in your project (see [How to Install BMad](./install-bmad.md))
|
||||
- A text editor for YAML files
|
||||
:::
|
||||
:::
|
||||
|
||||
:::caution[Keep Your Customizations Safe]
|
||||
Always use the `.customize.yaml` files described here rather than editing agent files directly. The installer overwrites agent files during updates, but preserves your `.customize.yaml` changes.
|
||||
|
|
@ -136,10 +137,10 @@ npx bmad-method install
|
|||
|
||||
The installer detects the existing installation and offers these options:
|
||||
|
||||
| Option | What It Does |
|
||||
| ---------------------------- | ------------------------------------------------------------------- |
|
||||
| Option | What It Does |
|
||||
| ---------------------------- | -------------------------------------------------------------------- |
|
||||
| **Quick Update** | Updates all modules to the latest version and applies customizations |
|
||||
| **Modify BMad Installation** | Full installation flow for adding or removing modules |
|
||||
| **Modify BMad Installation** | Full installation flow for adding or removing modules |
|
||||
|
||||
For customization-only changes, **Quick Update** is the fastest option.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
title: "Established Projects"
|
||||
title: 'Established Projects'
|
||||
description: How to use BMad Method on existing codebases
|
||||
sidebar:
|
||||
order: 6
|
||||
order: 7
|
||||
---
|
||||
|
||||
Use BMad Method effectively when working on existing projects and legacy codebases.
|
||||
|
|
@ -10,10 +10,11 @@ Use BMad Method effectively when working on existing projects and legacy codebas
|
|||
This guide covers the essential workflow for onboarding to existing projects with BMad Method.
|
||||
|
||||
:::note[Prerequisites]
|
||||
|
||||
- BMad Method installed (`npx bmad-method install`)
|
||||
- An existing codebase you want to work on
|
||||
- Access to an AI-powered IDE (Claude Code or Cursor)
|
||||
:::
|
||||
:::
|
||||
|
||||
## Step 1: Clean Up Completed Planning Artifacts
|
||||
|
||||
|
|
@ -36,6 +37,7 @@ bmad-generate-project-context
|
|||
```
|
||||
|
||||
This scans your codebase to identify:
|
||||
|
||||
- Technology stack and versions
|
||||
- Code organization patterns
|
||||
- Naming conventions
|
||||
|
|
@ -79,10 +81,10 @@ BMad-Help also **automatically runs at the end of every workflow**, providing cl
|
|||
|
||||
You have two primary options depending on the scope of changes:
|
||||
|
||||
| Scope | Recommended Approach |
|
||||
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Scope | Recommended Approach |
|
||||
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Small updates or additions** | Run `bmad-quick-dev` to clarify intent, plan, implement, and review in a single workflow. The full four-phase BMad Method is likely overkill. |
|
||||
| **Major changes or additions** | Start with the BMad Method, applying as much or as little rigor as needed. |
|
||||
| **Major changes or additions** | Start with the BMad Method, applying as much or as little rigor as needed. |
|
||||
|
||||
### During PRD Creation
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
title: "How to Get Answers About BMad"
|
||||
title: 'How to Get Answers About BMad'
|
||||
description: Use an LLM to quickly answer your own BMad questions
|
||||
sidebar:
|
||||
order: 4
|
||||
order: 5
|
||||
---
|
||||
|
||||
Use BMad's built-in help, source docs, or the community to get answers — from quickest to most thorough.
|
||||
|
|
@ -46,35 +46,35 @@ If your AI can't read local files (ChatGPT, Claude.ai, etc.), fetch [llms-full.t
|
|||
|
||||
If neither BMad-Help nor the source answered your question, you now have a much better question to ask.
|
||||
|
||||
| Channel | Use For |
|
||||
| ------------------------- | ------------------------------------------- |
|
||||
| `help-requests` forum | Questions |
|
||||
| `#suggestions-feedback` | Ideas and feature requests |
|
||||
| Channel | Use For |
|
||||
| ----------------------- | -------------------------- |
|
||||
| `help-requests` forum | Questions |
|
||||
| `#suggestions-feedback` | Ideas and feature requests |
|
||||
|
||||
**Discord:** [discord.gg/gk8jAdXWmj](https://discord.gg/gk8jAdXWmj)
|
||||
|
||||
**GitHub Issues:** [github.com/bmad-code-org/BMAD-METHOD/issues](https://github.com/bmad-code-org/BMAD-METHOD/issues)
|
||||
*You!*
|
||||
*Stuck*
|
||||
*in the queue—*
|
||||
*waiting*
|
||||
*for who?*
|
||||
_You!_
|
||||
_Stuck_
|
||||
_in the queue—_
|
||||
_waiting_
|
||||
_for who?_
|
||||
|
||||
*The source*
|
||||
*is there,*
|
||||
*plain to see!*
|
||||
_The source_
|
||||
_is there,_
|
||||
_plain to see!_
|
||||
|
||||
*Point*
|
||||
*your machine.*
|
||||
*Set it free.*
|
||||
_Point_
|
||||
_your machine._
|
||||
_Set it free._
|
||||
|
||||
*It reads.*
|
||||
*It speaks.*
|
||||
*Ask away—*
|
||||
_It reads._
|
||||
_It speaks._
|
||||
_Ask away—_
|
||||
|
||||
*Why wait*
|
||||
*for tomorrow*
|
||||
*when you have*
|
||||
*today?*
|
||||
_Why wait_
|
||||
_for tomorrow_
|
||||
_when you have_
|
||||
_today?_
|
||||
|
||||
*—Claude*
|
||||
_—Claude_
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
title: "How to Install BMad"
|
||||
title: 'How to Install BMad'
|
||||
description: Step-by-step guide to installing BMad in your project
|
||||
sidebar:
|
||||
order: 1
|
||||
|
|
@ -16,10 +16,11 @@ If you want to use a non interactive installer and provide all install options o
|
|||
- Update the existing BMad Installation
|
||||
|
||||
:::note[Prerequisites]
|
||||
|
||||
- **Node.js** 20+ (required for the installer)
|
||||
- **Git** (recommended)
|
||||
- **AI tool** (Claude Code, Cursor, or similar)
|
||||
:::
|
||||
:::
|
||||
|
||||
## Steps
|
||||
|
||||
|
|
@ -31,6 +32,7 @@ npx bmad-method install
|
|||
|
||||
:::tip[Want the newest prerelease build?]
|
||||
Use the `next` dist-tag:
|
||||
|
||||
```bash
|
||||
npx bmad-method@next install
|
||||
```
|
||||
|
|
@ -40,9 +42,11 @@ This gets you newer changes earlier, with a higher chance of churn than the defa
|
|||
|
||||
:::tip[Bleeding edge]
|
||||
To install the latest from the main branch (may be unstable):
|
||||
|
||||
```bash
|
||||
npx github:bmad-code-org/BMAD-METHOD install
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### 2. Choose Installation Location
|
||||
|
|
@ -99,11 +103,13 @@ your-project/
|
|||
Run `bmad-help` to verify everything works and see what to do next.
|
||||
|
||||
**BMad-Help is your intelligent guide** that will:
|
||||
|
||||
- Confirm your installation is working
|
||||
- Show what's available based on your installed modules
|
||||
- Recommend your first step
|
||||
|
||||
You can also ask it questions:
|
||||
|
||||
```
|
||||
bmad-help I just installed, what should I do first?
|
||||
bmad-help What are my options for a SaaS project?
|
||||
|
|
|
|||
|
|
@ -0,0 +1,180 @@
|
|||
---
|
||||
title: 'Install Custom and Community Modules'
|
||||
description: Install third-party modules from the community registry, Git repositories, or local paths
|
||||
sidebar:
|
||||
order: 3
|
||||
---
|
||||
|
||||
Use the BMad installer to add modules from the community registry, third-party Git repositories, or local file paths.
|
||||
|
||||
## When to Use This
|
||||
|
||||
- Installing a community-contributed module from the BMad registry
|
||||
- Installing a module from a third-party Git repository (GitHub, GitLab, Bitbucket, self-hosted)
|
||||
- Testing a module you are developing locally with BMad Builder
|
||||
- Installing modules from a private or self-hosted Git server
|
||||
|
||||
:::note[Prerequisites]
|
||||
Requires [Node.js](https://nodejs.org) v20+ and `npx` (included with npm). Custom and community modules can be selected during a fresh install or added to an existing installation.
|
||||
:::
|
||||
|
||||
## Community Modules
|
||||
|
||||
Community modules are curated in the [BMad plugins marketplace](https://github.com/bmad-code-org/bmad-plugins-marketplace). They are organized by category and are pinned to an approved commit for safety.
|
||||
|
||||
### 1. Run the Installer
|
||||
|
||||
```bash
|
||||
npx bmad-method install
|
||||
```
|
||||
|
||||
### 2. Browse the Community Catalog
|
||||
|
||||
After selecting official modules, the installer asks:
|
||||
|
||||
```
|
||||
Would you like to browse community modules?
|
||||
```
|
||||
|
||||
Select **Yes** to enter the catalog browser. You can:
|
||||
|
||||
- Browse by category
|
||||
- View featured modules
|
||||
- View all available modules
|
||||
- Search by keyword
|
||||
|
||||
### 3. Select Modules
|
||||
|
||||
Pick modules from any category. The installer shows descriptions, versions, and trust tiers. Already-installed modules are pre-checked for update.
|
||||
|
||||
### 4. Continue with Installation
|
||||
|
||||
After selecting community modules, the installer proceeds to custom sources, then tool/IDE configuration and the rest of the install flow.
|
||||
|
||||
## Custom Sources (Git URLs and Local Paths)
|
||||
|
||||
Custom modules can come from any Git repository or a local directory on your machine. The installer resolves the source, analyzes the module structure, and installs it alongside your other modules.
|
||||
|
||||
### Interactive Installation
|
||||
|
||||
During installation, after the community module step, the installer asks:
|
||||
|
||||
```
|
||||
Would you like to install from a custom source (Git URL or local path)?
|
||||
```
|
||||
|
||||
Select **Yes**, then provide a source:
|
||||
|
||||
| Input Type | Example |
|
||||
| --------------------- | ------------------------------------------------- |
|
||||
| HTTPS URL (any host) | `https://github.com/org/repo` |
|
||||
| HTTPS URL with subdir | `https://github.com/org/repo/tree/main/my-module` |
|
||||
| SSH URL | `git@github.com:org/repo.git` |
|
||||
| Local path | `/Users/me/projects/my-module` |
|
||||
| Local path with tilde | `~/projects/my-module` |
|
||||
|
||||
The installer clones the repository (for URLs) or reads directly from disk (for local paths), then presents the discovered modules for selection.
|
||||
|
||||
### Non-Interactive Installation
|
||||
|
||||
Use the `--custom-source` flag to install custom modules from the command line:
|
||||
|
||||
```bash
|
||||
npx bmad-method install \
|
||||
--directory . \
|
||||
--custom-source /path/to/my-module \
|
||||
--tools claude-code \
|
||||
--yes
|
||||
```
|
||||
|
||||
When `--custom-source` is provided without `--modules`, only core and the custom modules are installed. To include official modules as well, add `--modules`:
|
||||
|
||||
```bash
|
||||
npx bmad-method install \
|
||||
--directory . \
|
||||
--modules bmm \
|
||||
--custom-source https://gitlab.com/myorg/my-module \
|
||||
--tools claude-code \
|
||||
--yes
|
||||
```
|
||||
|
||||
Multiple sources can be comma-separated:
|
||||
|
||||
```bash
|
||||
--custom-source /path/one,https://github.com/org/repo,/path/two
|
||||
```
|
||||
|
||||
## How Module Discovery Works
|
||||
|
||||
The installer uses two modes to find installable modules in a source:
|
||||
|
||||
| Mode | Trigger | Behavior |
|
||||
| --------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
||||
| Discovery | Source contains `.claude-plugin/marketplace.json` | Lists all plugins from the manifest; you pick which to install |
|
||||
| Direct | No marketplace.json found | Scans the directory for skills (subdirectories with `SKILL.md`), resolves as a single module |
|
||||
|
||||
Discovery mode is typical for published modules. Direct mode is convenient when pointing at a skills directory during local development.
|
||||
|
||||
:::note[About `.claude-plugin/`]
|
||||
The `.claude-plugin/marketplace.json` path is a standard convention adopted across multiple AI tool installers for plugin discoverability. It does not require Claude, does not use Claude APIs, and has no effect on which AI tool you use. Any module with this file can be discovered by any installer that follows the convention.
|
||||
:::
|
||||
|
||||
## Local Development Workflow
|
||||
|
||||
If you are building a module with [BMad Builder](https://github.com/bmad-code-org/bmad-builder), you can install it directly from your working directory:
|
||||
|
||||
```bash
|
||||
npx bmad-method install \
|
||||
--directory ~/my-project \
|
||||
--custom-source ~/my-module-repo/skills \
|
||||
--tools claude-code \
|
||||
--yes
|
||||
```
|
||||
|
||||
Local sources are referenced by path, not copied to a cache. When you update your module source and reinstall, the installer picks up the latest changes.
|
||||
|
||||
:::caution[Source Removal]
|
||||
If you delete the local source directory after installation, the installed module files in `_bmad/` are preserved. The module will be skipped during updates until the source path is restored.
|
||||
:::
|
||||
|
||||
## What You Get
|
||||
|
||||
After installation, custom modules appear in `_bmad/` alongside official modules:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── _bmad/
|
||||
│ ├── core/ # Built-in core module
|
||||
│ ├── bmm/ # Official module (if selected)
|
||||
│ ├── my-module/ # Your custom module
|
||||
│ │ ├── my-skill/
|
||||
│ │ │ └── SKILL.md
|
||||
│ │ └── module-help.csv
|
||||
│ └── _config/
|
||||
│ └── manifest.yaml # Tracks all modules, versions, and sources
|
||||
└── ...
|
||||
```
|
||||
|
||||
The manifest records the source of each custom module (`repoUrl` for Git sources, `localPath` for local sources) so that quick updates can locate the source again.
|
||||
|
||||
## Updating Custom Modules
|
||||
|
||||
Custom modules participate in the normal update flow:
|
||||
|
||||
- **Quick update** (`--action quick-update`): Refreshes all modules from their original sources. Git-based modules are re-fetched; local modules are re-read from their source path.
|
||||
- **Full update**: Re-runs module selection so you can add or remove custom modules.
|
||||
|
||||
## Creating Your Own Modules
|
||||
|
||||
Use [BMad Builder](https://github.com/bmad-code-org/bmad-builder) to create modules that others can install:
|
||||
|
||||
1. Run `bmad-module-builder` to scaffold your module structure
|
||||
2. Add skills, agents, and workflows with the various bmad builder tools
|
||||
3. Publish to a Git repository or share the folder collection
|
||||
4. Others install with `--custom-source <your-repo-url>`
|
||||
|
||||
For modules to support discovery mode, include a `.claude-plugin/marketplace.json` in your repository root (this is a cross-tool convention, not Claude-specific). See the [BMad Builder documentation](https://github.com/bmad-code-org/bmad-builder) for the marketplace.json format.
|
||||
|
||||
:::tip[Testing Locally First]
|
||||
During development, install your module with a local path to iterate quickly before publishing to a Git repository.
|
||||
:::
|
||||
|
|
@ -22,39 +22,40 @@ Requires [Node.js](https://nodejs.org) v20+ and `npx` (included with npm).
|
|||
|
||||
### Installation Options
|
||||
|
||||
| Flag | Description | Example |
|
||||
|------|-------------|---------|
|
||||
| `--directory <path>` | Installation directory | `--directory ~/projects/myapp` |
|
||||
| `--modules <modules>` | Comma-separated module IDs | `--modules bmm,bmb` |
|
||||
| `--tools <tools>` | Comma-separated tool/IDE IDs (use `none` to skip) | `--tools claude-code,cursor` or `--tools none` |
|
||||
| `--action <type>` | Action for existing installations: `install` (default), `update`, or `quick-update` | `--action quick-update` |
|
||||
| Flag | Description | Example |
|
||||
| --------------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------- |
|
||||
| `--directory <path>` | Installation directory | `--directory ~/projects/myapp` |
|
||||
| `--modules <modules>` | Comma-separated module IDs | `--modules bmm,bmb` |
|
||||
| `--tools <tools>` | Comma-separated tool/IDE IDs (use `none` to skip) | `--tools claude-code,cursor` or `--tools none` |
|
||||
| `--action <type>` | Action for existing installations: `install` (default), `update`, or `quick-update` | `--action quick-update` |
|
||||
| `--custom-source <sources>` | Comma-separated Git URLs or local paths for custom modules | `--custom-source /path/to/module` |
|
||||
|
||||
### Core Configuration
|
||||
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `--user-name <name>` | Name for agents to use | System username |
|
||||
| `--communication-language <lang>` | Agent communication language | English |
|
||||
| `--document-output-language <lang>` | Document output language | English |
|
||||
| `--output-folder <path>` | Output folder path (see resolution rules below) | `_bmad-output` |
|
||||
| Flag | Description | Default |
|
||||
| ----------------------------------- | ----------------------------------------------- | --------------- |
|
||||
| `--user-name <name>` | Name for agents to use | System username |
|
||||
| `--communication-language <lang>` | Agent communication language | English |
|
||||
| `--document-output-language <lang>` | Document output language | English |
|
||||
| `--output-folder <path>` | Output folder path (see resolution rules below) | `_bmad-output` |
|
||||
|
||||
#### Output Folder Path Resolution
|
||||
|
||||
The value passed to `--output-folder` (or entered interactively) is resolved according to these rules:
|
||||
|
||||
| Input type | Example | Resolved as |
|
||||
|------------|---------|-------------|
|
||||
| Relative path (default) | `_bmad-output` | `<project-root>/_bmad-output` |
|
||||
| Relative path with traversal | `../../shared-outputs` | Normalized absolute path — e.g. `/Users/me/shared-outputs` |
|
||||
| Absolute path | `/Users/me/shared-outputs` | Used as-is — project root is **not** prepended |
|
||||
| Input type | Example | Resolved as |
|
||||
| ---------------------------- | -------------------------- | ---------------------------------------------------------- |
|
||||
| Relative path (default) | `_bmad-output` | `<project-root>/_bmad-output` |
|
||||
| Relative path with traversal | `../../shared-outputs` | Normalized absolute path — e.g. `/Users/me/shared-outputs` |
|
||||
| Absolute path | `/Users/me/shared-outputs` | Used as-is — project root is **not** prepended |
|
||||
|
||||
The resolved path is what agents and workflows use at runtime when writing output files. Using an absolute path or a traversal-based relative path lets you direct all generated artifacts to a directory outside your project tree — useful for shared or monorepo setups.
|
||||
|
||||
### Other Options
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-y, --yes` | Accept all defaults and skip prompts |
|
||||
| Flag | Description |
|
||||
| ------------- | ------------------------------------------- |
|
||||
| `-y, --yes` | Accept all defaults and skip prompts |
|
||||
| `-d, --debug` | Enable debug output for manifest generation |
|
||||
|
||||
## Module IDs
|
||||
|
|
@ -76,12 +77,13 @@ Run `npx bmad-method install` interactively once to see the full current list of
|
|||
|
||||
## Installation Modes
|
||||
|
||||
| Mode | Description | Example |
|
||||
|------|-------------|---------|
|
||||
| Fully non-interactive | Provide all flags to skip all prompts | `npx bmad-method install --directory . --modules bmm --tools claude-code --yes` |
|
||||
| Semi-interactive | Provide some flags; BMad prompts for the rest | `npx bmad-method install --directory . --modules bmm` |
|
||||
| Defaults only | Accept all defaults with `-y` | `npx bmad-method install --yes` |
|
||||
| Without tools | Skip tool/IDE configuration | `npx bmad-method install --modules bmm --tools none` |
|
||||
| Mode | Description | Example |
|
||||
| --------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| Fully non-interactive | Provide all flags to skip all prompts | `npx bmad-method install --directory . --modules bmm --tools claude-code --yes` |
|
||||
| Semi-interactive | Provide some flags; BMad prompts for the rest | `npx bmad-method install --directory . --modules bmm` |
|
||||
| Defaults only | Accept all defaults with `-y` | `npx bmad-method install --yes` |
|
||||
| Custom source only | Install core + custom module(s) | `npx bmad-method install --directory . --custom-source /path/to/module --tools claude-code --yes` |
|
||||
| Without tools | Skip tool/IDE configuration | `npx bmad-method install --modules bmm --tools none` |
|
||||
|
||||
## Examples
|
||||
|
||||
|
|
@ -119,6 +121,33 @@ npx bmad-method install \
|
|||
--action quick-update
|
||||
```
|
||||
|
||||
### Install from Custom Source
|
||||
|
||||
Install a module from a local path or any Git host:
|
||||
|
||||
```bash
|
||||
npx bmad-method install \
|
||||
--directory . \
|
||||
--custom-source /path/to/my-module \
|
||||
--tools claude-code \
|
||||
--yes
|
||||
```
|
||||
|
||||
Combine with official modules:
|
||||
|
||||
```bash
|
||||
npx bmad-method install \
|
||||
--directory . \
|
||||
--modules bmm \
|
||||
--custom-source https://gitlab.com/myorg/my-module \
|
||||
--tools claude-code \
|
||||
--yes
|
||||
```
|
||||
|
||||
:::note[Custom source behavior]
|
||||
When `--custom-source` is used without `--modules`, only core and the custom modules are installed. Add `--modules` to include official modules as well. See [Install Custom and Community Modules](./install-custom-modules.md) for details.
|
||||
:::
|
||||
|
||||
## What You Get
|
||||
|
||||
- A fully configured `_bmad/` directory in your project
|
||||
|
|
@ -135,17 +164,19 @@ BMad validates all provided flags:
|
|||
- **Action** — Must be one of: `install`, `update`, `quick-update`
|
||||
|
||||
Invalid values will either:
|
||||
|
||||
1. Show an error and exit (for critical options like directory)
|
||||
2. Show a warning and skip (for optional items)
|
||||
3. Fall back to interactive prompts (for missing required values)
|
||||
|
||||
:::tip[Best Practices]
|
||||
|
||||
- Use absolute paths for `--directory` to avoid ambiguity
|
||||
- Use an absolute path for `--output-folder` when you want artifacts written outside the project tree (e.g. a shared monorepo outputs directory)
|
||||
- Test flags locally before using in CI/CD pipelines
|
||||
- Combine with `-y` for truly unattended installations
|
||||
- Use `--debug` if you encounter issues during installation
|
||||
:::
|
||||
:::
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
---
|
||||
title: "Manage Project Context"
|
||||
title: 'Manage Project Context'
|
||||
description: Create and maintain project-context.md to guide AI agents
|
||||
sidebar:
|
||||
order: 8
|
||||
order: 9
|
||||
---
|
||||
|
||||
Use the `project-context.md` file to ensure AI agents follow your project's technical preferences and implementation rules throughout all workflows. To make sure this is always available, you can also add the line `Important project context and conventions are located in [path to project context]/project-context.md` to your tools context or always rules file (such as `AGENTS.md`)
|
||||
|
||||
:::note[Prerequisites]
|
||||
|
||||
- BMad Method installed
|
||||
- Understanding of your project's technology stack and conventions
|
||||
:::
|
||||
:::
|
||||
|
||||
## When to Use This
|
||||
|
||||
|
|
@ -60,14 +61,17 @@ sections_completed: ['technology_stack', 'critical_rules']
|
|||
## Critical Implementation Rules
|
||||
|
||||
**TypeScript:**
|
||||
|
||||
- Strict mode enabled, no `any` types
|
||||
- Use `interface` for public APIs, `type` for unions
|
||||
|
||||
**Code Organization:**
|
||||
|
||||
- Components in `/src/components/` with co-located tests
|
||||
- API calls use `apiClient` singleton — never fetch directly
|
||||
|
||||
**Testing:**
|
||||
|
||||
- Unit tests focus on business logic
|
||||
- Integration tests use MSW for API mocking
|
||||
```
|
||||
|
|
@ -115,11 +119,12 @@ A `project-context.md` file that:
|
|||
## Tips
|
||||
|
||||
:::tip[Best Practices]
|
||||
|
||||
- **Focus on the unobvious** — Document patterns agents might miss (e.g., "Use JSDoc on every public class"), not universal practices like "use meaningful variable names."
|
||||
- **Keep it lean** — This file is loaded by every implementation workflow. Long files waste context. Exclude content that only applies to narrow scope or specific stories.
|
||||
- **Update as needed** — Edit manually when patterns change, or re-generate after significant architecture changes.
|
||||
- Works for Quick Flow and full BMad Method projects alike.
|
||||
:::
|
||||
:::
|
||||
|
||||
## Next Steps
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
title: "Quick Fixes"
|
||||
title: 'Quick Fixes'
|
||||
description: How to make quick fixes and ad-hoc changes
|
||||
sidebar:
|
||||
order: 5
|
||||
order: 6
|
||||
---
|
||||
|
||||
Use **Quick Dev** for bug fixes, refactorings, or small targeted changes that don't require the full BMad Method.
|
||||
|
|
@ -15,9 +15,10 @@ Use **Quick Dev** for bug fixes, refactorings, or small targeted changes that do
|
|||
- Dependency updates
|
||||
|
||||
:::note[Prerequisites]
|
||||
|
||||
- BMad Method installed (`npx bmad-method install`)
|
||||
- An AI-powered IDE (Claude Code, Cursor, or similar)
|
||||
:::
|
||||
:::
|
||||
|
||||
## Steps
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
title: "Document Sharding Guide"
|
||||
title: 'Document Sharding Guide'
|
||||
description: Split large markdown files into smaller organized files for better context management
|
||||
sidebar:
|
||||
order: 9
|
||||
order: 10
|
||||
---
|
||||
|
||||
Use the `bmad-shard-doc` tool if you need to split large markdown files into smaller, organized files for better context management.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
title: "How to Upgrade to v6"
|
||||
title: 'How to Upgrade to v6'
|
||||
description: Migrate from BMad v4 to v6
|
||||
sidebar:
|
||||
order: 3
|
||||
order: 4
|
||||
---
|
||||
|
||||
Use the BMad installer to upgrade from v4 to v6, which includes automatic detection of legacy installations and migration assistance.
|
||||
|
|
@ -14,9 +14,10 @@ Use the BMad installer to upgrade from v4 to v6, which includes automatic detect
|
|||
- You have existing planning artifacts to preserve
|
||||
|
||||
:::note[Prerequisites]
|
||||
|
||||
- Node.js 20+
|
||||
- Existing BMad v4 installation
|
||||
:::
|
||||
:::
|
||||
|
||||
## Steps
|
||||
|
||||
|
|
|
|||
|
|
@ -1,53 +1,53 @@
|
|||
---
|
||||
title: "Giai đoạn Analysis: từ ý tưởng đến nền tảng"
|
||||
description: Brainstorming, research, product brief và PRFAQ là gì, và nên dùng từng công cụ khi nào
|
||||
title: "Giai đoạn phân tích: từ ý tưởng đến nền tảng"
|
||||
description: Động não, nghiên cứu, product brief và PRFAQ là gì, và nên dùng từng công cụ khi nào
|
||||
sidebar:
|
||||
order: 1
|
||||
---
|
||||
|
||||
Giai đoạn Analysis (Phase 1) giúp bạn suy nghĩ rõ ràng về sản phẩm trước khi cam kết bắt tay vào xây dựng. Mọi công cụ trong giai đoạn này đều là tùy chọn, nhưng nếu bỏ qua toàn bộ phần analysis thì PRD của bạn sẽ được dựng trên giả định thay vì insight.
|
||||
Giai đoạn phân tích (giai đoạn 1) giúp bạn suy nghĩ rõ ràng về sản phẩm trước khi cam kết bắt tay vào xây dựng. Mọi công cụ trong giai đoạn này đều là tùy chọn, nhưng nếu bỏ qua toàn bộ phần phân tích thì PRD của bạn sẽ được dựng trên giả định thay vì hiểu biết thực chất.
|
||||
|
||||
## Vì sao cần Analysis trước Planning?
|
||||
## Vì sao cần phân tích trước khi lập kế hoạch?
|
||||
|
||||
PRD trả lời câu hỏi "chúng ta nên xây gì và vì sao?". Nếu đầu vào của nó là những suy nghĩ mơ hồ, bạn sẽ nhận lại một PRD mơ hồ, và mọi tài liệu phía sau đều kế thừa chính sự mơ hồ đó. Kiến trúc dựng trên một PRD yếu sẽ đặt cược sai về mặt kỹ thuật. Stories sinh ra từ một kiến trúc yếu sẽ bỏ sót edge case. Chi phí sẽ dồn lên theo từng tầng.
|
||||
PRD trả lời câu hỏi "chúng ta nên xây gì và vì sao?". Nếu đầu vào của nó là những suy nghĩ mơ hồ, bạn sẽ nhận lại một PRD mơ hồ, và mọi tài liệu phía sau đều kế thừa chính sự mơ hồ đó. Kiến trúc dựng trên một PRD yếu sẽ đặt cược sai về mặt kỹ thuật. Các story sinh ra từ một kiến trúc yếu sẽ bỏ sót trường hợp biên. Chi phí sẽ dồn lên theo từng tầng.
|
||||
|
||||
Các công cụ analysis tồn tại để làm PRD của bạn sắc bén hơn. Chúng tiếp cận vấn đề từ nhiều góc độ khác nhau: khám phá sáng tạo, thực tế thị trường, độ rõ ràng về khách hàng, tính khả thi. Nhờ vậy, đến khi bạn ngồi xuống làm việc với PM agent, bạn đã biết mình đang xây cái gì và cho ai.
|
||||
Các công cụ phân tích tồn tại để làm PRD của bạn sắc bén hơn. Chúng tiếp cận vấn đề từ nhiều góc độ khác nhau: khám phá sáng tạo, thực tế thị trường, độ rõ ràng về khách hàng, tính khả thi. Nhờ vậy, đến khi bạn ngồi xuống làm việc với agent PM, bạn đã biết mình đang xây cái gì và cho ai.
|
||||
|
||||
## Các công cụ
|
||||
|
||||
### Brainstorming
|
||||
### Động não
|
||||
|
||||
**Nó là gì.** Một phiên sáng tạo có điều phối, sử dụng các kỹ thuật ideation đã được kiểm chứng. AI đóng vai trò như người huấn luyện, kéo ý tưởng ra từ bạn thông qua các bài tập có cấu trúc, chứ không nghĩ thay cho bạn.
|
||||
**Nó là gì.** Một phiên sáng tạo có điều phối, sử dụng các kỹ thuật phát ý tưởng đã được kiểm chứng. AI đóng vai trò như người huấn luyện, kéo ý tưởng ra từ bạn thông qua các bài tập có cấu trúc, chứ không nghĩ thay cho bạn.
|
||||
|
||||
**Vì sao nó có mặt ở đây.** Ý tưởng thô cần không gian để phát triển trước khi bị khóa cứng thành requirement. Brainstorming tạo ra khoảng không đó. Nó đặc biệt có giá trị khi bạn có một miền vấn đề nhưng chưa có lời giải rõ ràng, hoặc khi bạn muốn khám phá nhiều hướng trước khi commit.
|
||||
**Vì sao nó có mặt ở đây.** Ý tưởng thô cần không gian để phát triển trước khi bị khóa cứng thành yêu cầu. Động não tạo ra khoảng không đó. Nó đặc biệt có giá trị khi bạn có một miền vấn đề nhưng chưa có lời giải rõ ràng, hoặc khi bạn muốn khám phá nhiều hướng trước khi cam kết.
|
||||
|
||||
**Khi nào nên dùng.** Bạn có một hình dung mơ hồ về thứ mình muốn xây nhưng chưa kết tinh được thành khái niệm rõ ràng. Hoặc bạn đã có concept ban đầu nhưng muốn pressure-test nó với các phương án thay thế.
|
||||
**Khi nào nên dùng.** Bạn có một hình dung mơ hồ về thứ mình muốn xây nhưng chưa kết tinh được thành khái niệm rõ ràng. Hoặc bạn đã có ý tưởng ban đầu nhưng muốn kiểm chứng độ vững của nó bằng các phương án thay thế.
|
||||
|
||||
Xem [Brainstorming](./brainstorming.md) để hiểu sâu hơn về cách một phiên làm việc diễn ra.
|
||||
|
||||
### Research (Thị trường, miền nghiệp vụ, kỹ thuật)
|
||||
### Nghiên cứu (thị trường, miền nghiệp vụ, kỹ thuật)
|
||||
|
||||
**Nó là gì.** Ba workflow nghiên cứu tập trung vào các chiều khác nhau của ý tưởng. Market research xem xét đối thủ, xu hướng và cảm nhận của người dùng. Domain research xây dựng hiểu biết về miền nghiệp vụ và thuật ngữ. Technical research đánh giá tính khả thi, các lựa chọn kiến trúc và hướng triển khai.
|
||||
**Nó là gì.** Ba quy trình nghiên cứu tập trung vào các chiều khác nhau của ý tưởng. Nghiên cứu thị trường xem xét đối thủ, xu hướng và cảm nhận của người dùng. Nghiên cứu miền nghiệp vụ xây dựng hiểu biết về lĩnh vực và thuật ngữ. Nghiên cứu kỹ thuật đánh giá tính khả thi, các lựa chọn kiến trúc và hướng triển khai.
|
||||
|
||||
**Vì sao nó có mặt ở đây.** Xây dựng dựa trên giả định là con đường nhanh nhất để tạo ra thứ chẳng ai cần. Research đặt concept của bạn xuống mặt đất: đối thủ nào đã tồn tại, người dùng thực sự đang vật lộn với điều gì, điều gì khả thi về kỹ thuật, và bạn sẽ phải đối mặt với những ràng buộc đặc thù ngành nào.
|
||||
**Vì sao nó có mặt ở đây.** Xây dựng dựa trên giả định là con đường nhanh nhất để tạo ra thứ chẳng ai cần. Nghiên cứu đặt ý tưởng của bạn xuống mặt đất: đối thủ nào đã tồn tại, người dùng thực sự đang vật lộn với điều gì, điều gì khả thi về kỹ thuật, và bạn sẽ phải đối mặt với những ràng buộc đặc thù ngành nào.
|
||||
|
||||
**Khi nào nên dùng.** Bạn đang bước vào một miền mới, nghi ngờ có đối thủ nhưng chưa lập bản đồ được, hoặc concept của bạn phụ thuộc vào những năng lực kỹ thuật mà bạn chưa kiểm chứng. Có thể chạy một, hai, hoặc cả ba; mỗi workflow đều đứng độc lập.
|
||||
**Khi nào nên dùng.** Bạn đang bước vào một miền mới, nghi ngờ có đối thủ nhưng chưa lập bản đồ được, hoặc ý tưởng của bạn phụ thuộc vào những năng lực kỹ thuật mà bạn chưa kiểm chứng. Có thể chạy một, hai, hoặc cả ba; mỗi quy trình đều đứng độc lập.
|
||||
|
||||
### Product Brief
|
||||
|
||||
**Nó là gì.** Một phiên discovery có hướng dẫn, tạo ra bản tóm tắt điều hành 1-2 trang cho concept sản phẩm của bạn. AI đóng vai trò Business Analyst cộng tác, giúp bạn diễn đạt tầm nhìn, đối tượng mục tiêu, giá trị cốt lõi và phạm vi.
|
||||
|
||||
**Vì sao nó có mặt ở đây.** Product brief là con đường nhẹ nhàng hơn để đi vào planning. Nó ghi lại tầm nhìn chiến lược của bạn theo định dạng có cấu trúc và đưa thẳng vào quá trình tạo PRD. Nó hoạt động tốt nhất khi bạn đã có niềm tin tương đối chắc vào concept của mình: bạn biết khách hàng là ai, vấn đề là gì, và đại khái muốn xây gì. Brief sẽ tổ chức lại và làm sắc nét lối suy nghĩ đó.
|
||||
**Vì sao nó có mặt ở đây.** Product brief là con đường nhẹ nhàng hơn để đi vào giai đoạn lập kế hoạch. Nó ghi lại tầm nhìn chiến lược của bạn theo định dạng có cấu trúc và đưa thẳng vào quá trình tạo PRD. Nó hoạt động tốt nhất khi bạn đã có niềm tin tương đối chắc vào ý tưởng của mình: bạn biết khách hàng là ai, vấn đề là gì, và đại khái muốn xây gì. Brief sẽ tổ chức lại và làm sắc nét lối suy nghĩ đó.
|
||||
|
||||
**Khi nào nên dùng.** Concept của bạn đã tương đối rõ và bạn muốn ghi lại nó một cách hiệu quả trước khi tạo PRD. Bạn tin vào hướng đi hiện tại và không cần bị thách thức giả định một cách quá quyết liệt.
|
||||
**Khi nào nên dùng.** Ý tưởng của bạn đã tương đối rõ và bạn muốn ghi lại nó một cách hiệu quả trước khi tạo PRD. Bạn tin vào hướng đi hiện tại và không cần bị thách thức giả định một cách quá quyết liệt.
|
||||
|
||||
### PRFAQ (Working Backwards)
|
||||
|
||||
**Nó là gì.** Phương pháp Working Backwards của Amazon được chuyển thành một thử thách tương tác. Bạn viết thông cáo báo chí công bố sản phẩm hoàn thiện trước khi tồn tại dù chỉ một dòng code, rồi trả lời những câu hỏi khó nhất mà khách hàng và stakeholder sẽ đặt ra. AI đóng vai trò product coach dai dẳng nhưng mang tính xây dựng.
|
||||
|
||||
**Vì sao nó có mặt ở đây.** PRFAQ là con đường nghiêm ngặt hơn để đi vào planning. Nó buộc bạn đạt đến sự rõ ràng theo hướng customer-first bằng cách bắt bạn bảo vệ từng phát biểu. Nếu bạn không viết nổi một thông cáo báo chí đủ thuyết phục, sản phẩm đó chưa sẵn sàng. Nếu phần FAQ lộ ra những khoảng trống, đó chính là những khoảng trống mà bạn sẽ phát hiện muộn hơn rất nhiều, và với chi phí lớn hơn nhiều, trong lúc triển khai. Bài kiểm tra này bóc tách lối suy nghĩ yếu ngay từ sớm, khi chi phí sửa còn rẻ nhất.
|
||||
**Vì sao nó có mặt ở đây.** PRFAQ là con đường nghiêm ngặt hơn để đi vào giai đoạn lập kế hoạch. Nó buộc bạn đạt đến sự rõ ràng theo hướng lấy khách hàng làm trung tâm bằng cách bắt bạn bảo vệ từng phát biểu. Nếu bạn không viết nổi một thông cáo báo chí đủ thuyết phục, sản phẩm đó chưa sẵn sàng. Nếu phần FAQ lộ ra những khoảng trống, đó chính là những khoảng trống mà bạn sẽ phát hiện muộn hơn rất nhiều, và với chi phí lớn hơn nhiều, trong lúc triển khai. Bài kiểm tra này bóc tách lối suy nghĩ yếu ngay từ sớm, khi chi phí sửa còn rẻ nhất.
|
||||
|
||||
**Khi nào nên dùng.** Bạn muốn stress-test concept trước khi commit tài nguyên. Bạn chưa chắc người dùng có thực sự quan tâm hay không. Bạn muốn xác nhận rằng mình có thể diễn đạt một value proposition rõ ràng và có thể bảo vệ được. Hoặc đơn giản là bạn muốn dùng sự kỷ luật của Working Backwards để làm suy nghĩ của mình sắc bén hơn.
|
||||
**Khi nào nên dùng.** Bạn muốn kiểm tra độ vững của ý tưởng trước khi cam kết tài nguyên. Bạn chưa chắc người dùng có thực sự quan tâm hay không. Bạn muốn xác nhận rằng mình có thể diễn đạt một giá trị cốt lõi rõ ràng và có thể bảo vệ được. Hoặc đơn giản là bạn muốn dùng sự kỷ luật của Working Backwards để làm suy nghĩ của mình sắc bén hơn.
|
||||
|
||||
## Tôi nên dùng cái nào?
|
||||
|
||||
|
|
@ -65,6 +65,6 @@ Product Brief và PRFAQ đều tạo ra đầu vào cho PRD. Hãy chọn một t
|
|||
Hãy chạy `bmad-help` và mô tả tình huống của bạn. Nó sẽ gợi ý điểm bắt đầu phù hợp dựa trên những gì bạn đã làm và điều bạn đang muốn đạt được.
|
||||
:::
|
||||
|
||||
## Sau Analysis thì chuyện gì xảy ra?
|
||||
## Sau giai đoạn phân tích thì chuyện gì xảy ra?
|
||||
|
||||
Output từ Analysis đi thẳng vào Phase 2 (Planning). Workflow tạo PRD chấp nhận product brief, tài liệu PRFAQ, kết quả research và báo cáo brainstorming làm đầu vào. Nó sẽ tổng hợp bất cứ thứ gì bạn đã tạo thành các requirement có cấu trúc. Bạn làm analysis càng kỹ, PRD của bạn càng sắc.
|
||||
Đầu ra từ giai đoạn phân tích đi thẳng vào giai đoạn 2, lập kế hoạch. Quy trình tạo PRD chấp nhận product brief, tài liệu PRFAQ, kết quả nghiên cứu và báo cáo động não làm đầu vào. Nó sẽ tổng hợp bất cứ thứ gì bạn đã tạo thành các yêu cầu có cấu trúc. Bạn làm phân tích càng kỹ, PRD của bạn càng sắc.
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
title: "Party Mode"
|
||||
title: "Chế độ Party"
|
||||
description: Cộng tác đa agent - đưa tất cả agent AI vào cùng một cuộc trò chuyện
|
||||
sidebar:
|
||||
order: 7
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
title: "Project Context"
|
||||
title: "Bối cảnh dự án"
|
||||
description: Cách project-context.md định hướng các agent AI theo quy tắc và ưu tiên của dự án
|
||||
sidebar:
|
||||
order: 7
|
||||
|
|
|
|||
|
|
@ -1,73 +1,73 @@
|
|||
---
|
||||
title: "Quick Dev"
|
||||
description: Giảm ma sát human-in-the-loop mà vẫn giữ các checkpoint bảo vệ chất lượng output
|
||||
title: "Phát triển nhanh"
|
||||
description: Giảm ma sát có người trong vòng lặp mà vẫn giữ các điểm kiểm tra bảo vệ chất lượng đầu ra
|
||||
sidebar:
|
||||
order: 2
|
||||
---
|
||||
|
||||
Đưa ý định vào, nhận thay đổi mã nguồn ra, với số lần cần con người nhảy vào giữa quy trình ít nhất có thể - nhưng không đánh đổi chất lượng.
|
||||
|
||||
Nó cho phép model tự vận hành lâu hơn giữa các checkpoint, rồi chỉ đưa con người quay lại khi tác vụ không thể tiếp tục an toàn nếu thiếu phán đoán của con người, hoặc khi đã đến lúc review kết quả cuối.
|
||||
Nó cho phép mô hình tự vận hành lâu hơn giữa các điểm kiểm tra, rồi chỉ đưa con người quay lại khi tác vụ không thể tiếp tục an toàn nếu thiếu phán đoán của con người, hoặc khi đã đến lúc rà soát kết quả cuối.
|
||||
|
||||

|
||||
|
||||
## Vì sao nó tồn tại
|
||||
|
||||
Các lượt human-in-the-loop vừa cần thiết vừa tốn kém.
|
||||
Các lượt có người trong vòng lặp vừa cần thiết vừa tốn kém.
|
||||
|
||||
LLM hiện tại vẫn thất bại theo những cách dễ đoán: hiểu sai ý định, tự điền vào khoảng trống bằng những phán đoán tự tin, lệch sang công việc không liên quan, và tạo ra các bản review nhiễu. Đồng thời, việc cần con người nhảy vào liên tục làm giảm tốc độ phát triển. Sự chú ý của con người là nút thắt.
|
||||
|
||||
`bmad-quick-dev` cân bằng lại đánh đổi đó. Nó tin model có thể chạy tự chủ lâu hơn, nhưng chỉ sau khi workflow đã tạo được một ranh giới đủ mạnh để làm điều đó an toàn.
|
||||
`bmad-quick-dev` cân bằng lại đánh đổi đó. Nó tin mô hình có thể chạy tự chủ lâu hơn, nhưng chỉ sau khi quy trình đã tạo được một ranh giới đủ mạnh để làm điều đó an toàn.
|
||||
|
||||
## Thiết kế cốt lõi
|
||||
|
||||
### 1. Nén ý định trước
|
||||
|
||||
Workflow bắt đầu bằng việc để con người và model nén yêu cầu thành một mục tiêu thống nhất. Đầu vào có thể bắt đầu như một ý định thô, nhưng trước khi workflow tự vận hành thì nó phải đủ nhỏ, đủ rõ ràng, và đủ ít mâu thuẫn để có thể thực thi.
|
||||
Quy trình bắt đầu bằng việc để con người và mô hình nén yêu cầu thành một mục tiêu thống nhất. Đầu vào có thể bắt đầu như một ý định thô, nhưng trước khi quy trình tự vận hành thì nó phải đủ nhỏ, đủ rõ ràng, và đủ ít mâu thuẫn để có thể thực thi.
|
||||
|
||||
Ý định có thể đến từ nhiều dạng: vài cụm từ, liên kết bug tracker, output từ plan mode, đoạn văn bản copy từ phiên chat, hoặc thậm chí một số story trong `epics.md` của chính BMAD. Ở trường hợp cuối, workflow không hiểu được ngữ nghĩa theo dõi story của BMAD, nhưng vẫn có thể lấy chính story đó và tiếp tục.
|
||||
Ý định có thể đến từ nhiều dạng: vài cụm từ, liên kết trình theo dõi lỗi, đầu ra từ chế độ lập kế hoạch, đoạn văn bản sao chép từ phiên chat, hoặc thậm chí một số story trong `epics.md` của chính BMAD. Ở trường hợp cuối, quy trình không hiểu được ngữ nghĩa theo dõi story của BMAD, nhưng vẫn có thể lấy chính story đó và tiếp tục.
|
||||
|
||||
Workflow này không loại bỏ quyền kiểm soát của con người. Nó chuyển nó về một số thời điểm có giá trị cao:
|
||||
Quy trình này không loại bỏ quyền kiểm soát của con người. Nó chuyển nó về một số thời điểm có giá trị cao:
|
||||
|
||||
- **Làm rõ ý định** - biến một yêu cầu lộn xộn thành một mục tiêu thống nhất, không mâu thuẫn ngầm
|
||||
- **Phê duyệt spec** - xác nhận rằng cách hiểu đã đóng băng là đúng thứ cần xây
|
||||
- **Review sản phẩm cuối** - checkpoint chính, nơi con người quyết định kết quả cuối có chấp nhận được hay không
|
||||
- **Phê duyệt đặc tả** - xác nhận rằng cách hiểu đã được chốt là đúng thứ cần xây
|
||||
- **Rà soát sản phẩm cuối** - điểm kiểm tra chính, nơi con người quyết định kết quả cuối có chấp nhận được hay không
|
||||
|
||||
### 2. Định tuyến theo con đường an toàn nhỏ nhất
|
||||
|
||||
Khi mục tiêu đã rõ, workflow sẽ quyết định đây có phải thay đổi one-shot thật sự hay cần đi theo đường đầy đủ hơn. Những thay đổi nhỏ, blast radius gần như bằng 0 có thể đi thẳng vào triển khai. Còn lại sẽ đi qua lập kế hoạch để model có được một ranh giới mạnh hơn trước khi tự chạy lâu hơn.
|
||||
Khi mục tiêu đã rõ, quy trình sẽ quyết định đây có phải thay đổi thực hiện một lần là xong hay cần đi theo đường đầy đủ hơn. Những thay đổi nhỏ, phạm vi ảnh hưởng gần như bằng 0 có thể đi thẳng vào triển khai. Còn lại sẽ đi qua lập kế hoạch để mô hình có được một ranh giới mạnh hơn trước khi tự chạy lâu hơn.
|
||||
|
||||
### 3. Chạy lâu hơn với ít giám sát hơn
|
||||
|
||||
Sau quyết định định tuyến đó, model có thể tự gánh thêm công việc. Trên con đường đầy đủ, spec đã được phê duyệt trở thành ranh giới mà model sẽ thực thi với ít giám sát hơn, và đó chính là mục tiêu của thiết kế này.
|
||||
Sau quyết định định tuyến đó, mô hình có thể tự gánh thêm công việc. Trên con đường đầy đủ, đặc tả đã được phê duyệt trở thành ranh giới mà mô hình sẽ thực thi với ít giám sát hơn, và đó chính là mục tiêu của thiết kế này.
|
||||
|
||||
### 4. Chẩn đoán lỗi ở đúng tầng
|
||||
|
||||
Nếu triển khai sai vì ý định sai, vậy sửa code không phải cách fix đúng. Nếu code sai vì spec yếu, thì vá diff cũng không phải cách fix đúng. Workflow được thiết kế để chẩn đoán lỗi đã đi vào hệ thống từ tầng nào, quay lại đúng tầng đó, rồi sinh lại từ đấy.
|
||||
Nếu triển khai sai vì ý định sai, vậy sửa code không phải cách sửa đúng. Nếu code sai vì đặc tả yếu, thì vá diff cũng không phải cách sửa đúng. Quy trình được thiết kế để chẩn đoán lỗi đã đi vào hệ thống từ tầng nào, quay lại đúng tầng đó, rồi sinh lại từ đấy.
|
||||
|
||||
Các phát hiện từ review được dùng để xác định vấn đề đến từ ý định, quá trình tạo spec, hay triển khai cục bộ. Chỉ những lỗi thật sự cục bộ mới được sửa tại chỗ.
|
||||
Các phát hiện từ bước rà soát được dùng để xác định vấn đề đến từ ý định, quá trình tạo đặc tả, hay triển khai cục bộ. Chỉ những lỗi thật sự cục bộ mới được sửa tại chỗ.
|
||||
|
||||
### 5. Chỉ đưa con người quay lại khi cần
|
||||
|
||||
Bước interview ý định có human-in-the-loop, nhưng nó không giống một checkpoint lặp đi lặp lại. Workflow cố gắng giảm thiểu những checkpoint lặp lại đó. Sau bước định hình ý định ban đầu, con người chủ yếu quay lại khi workflow không thể tiếp tục an toàn nếu thiếu phán đoán, và ở cuối quy trình để review kết quả.
|
||||
Bước phỏng vấn ý định có người trong vòng lặp, nhưng nó không giống một điểm kiểm tra lặp đi lặp lại. Quy trình cố gắng giảm thiểu những điểm kiểm tra lặp lại đó. Sau bước định hình ý định ban đầu, con người chủ yếu quay lại khi quy trình không thể tiếp tục an toàn nếu thiếu phán đoán, và ở cuối quy trình để rà soát kết quả.
|
||||
|
||||
- **Xử lý khoảng trống của ý định** - quay lại khi review cho thấy workflow không thể suy ra an toàn điều được hàm ý
|
||||
|
||||
Mọi thứ còn lại đều là ứng viên cho việc thực thi tự chủ lâu hơn. Đánh đổi này là có chủ đích. Các pattern cũ tốn nhiều sự chú ý của con người cho việc giám sát liên tục. Quick Dev đặt nhiều niềm tin hơn vào model, nhưng để dành sự chú ý của con người cho những thời điểm mà lý trí con người có đòn bẩy lớn nhất.
|
||||
Mọi thứ còn lại đều là ứng viên cho việc thực thi tự chủ lâu hơn. Đánh đổi này là có chủ đích. Các mẫu cũ tốn nhiều sự chú ý của con người cho việc giám sát liên tục. Quick Dev đặt nhiều niềm tin hơn vào mô hình, nhưng để dành sự chú ý của con người cho những thời điểm mà lý trí con người có đòn bẩy lớn nhất.
|
||||
|
||||
## Vì sao hệ thống review quan trọng
|
||||
|
||||
Giai đoạn review không chỉ để tìm bug. Nó còn để định tuyến cách sửa mà không phá hỏng động lượng.
|
||||
Giai đoạn rà soát không chỉ để tìm lỗi. Nó còn để định tuyến cách sửa mà không phá hỏng động lượng.
|
||||
|
||||
Workflow này hoạt động tốt nhất trên nền tảng có thể spawn subagent, hoặc ít nhất gọi được một LLM khác qua dòng lệnh và đợi kết quả. Nếu nền tảng của bạn không hỗ trợ sẵn, bạn có thể thêm skill để làm việc đó. Các subagent không mang context là một trụ cột trong thiết kế review.
|
||||
Quy trình này hoạt động tốt nhất trên nền tảng có thể tạo subagent, hoặc ít nhất gọi được một LLM khác qua dòng lệnh và đợi kết quả. Nếu nền tảng của bạn không hỗ trợ sẵn, bạn có thể thêm skill để làm việc đó. Các subagent không mang ngữ cảnh là một trụ cột trong thiết kế rà soát.
|
||||
|
||||
Review agentic thường sai theo hai cách:
|
||||
Rà soát kiểu agent thường sai theo hai cách:
|
||||
|
||||
- Tạo quá nhiều phát hiện, buộc con người lọc quá nhiều nhiễu.
|
||||
- Làm lệch thay đổi hiện tại bằng cách kéo vào các vấn đề không liên quan, biến mỗi lần chạy thành một dự án dọn dẹp ad-hoc.
|
||||
- Làm lệch thay đổi hiện tại bằng cách kéo vào các vấn đề không liên quan, biến mỗi lần chạy thành một dự án dọn dẹp chắp vá.
|
||||
|
||||
Quick Dev xử lý cả hai bằng cách coi review là triage.
|
||||
Quick Dev xử lý cả hai bằng cách coi rà soát là bước phân loại.
|
||||
|
||||
Có những phát hiện thuộc về thay đổi hiện tại. Có những phát hiện không thuộc về nó. Nếu một phát hiện chỉ là ngẫu nhiên xuất hiện, không gắn nhân quả với thay đổi đang làm, workflow có thể trì hoãn nó thay vì ép con người xử lý ngay. Điều đó giữ cho mỗi lần chạy tập trung và ngăn các ngả rẽ ngẫu nhiên ăn hết ngân sách chú ý.
|
||||
Có những phát hiện thuộc về thay đổi hiện tại. Có những phát hiện không thuộc về nó. Nếu một phát hiện chỉ là ngẫu nhiên xuất hiện, không gắn nhân quả với thay đổi đang làm, quy trình có thể trì hoãn nó thay vì ép con người xử lý ngay. Điều đó giữ cho mỗi lần chạy tập trung và ngăn các ngả rẽ ngẫu nhiên ăn hết ngân sách chú ý.
|
||||
|
||||
Quá trình triage này đôi khi sẽ không hoàn hảo. Điều đó chấp nhận được. Thường tốt hơn khi đánh giá sai một số phát hiện còn hơn là nhận về hàng ngàn bình luận review giá trị thấp. Hệ thống tối ưu cho chất lượng tín hiệu, không phải độ phủ tuyệt đối.
|
||||
|
|
|
|||
|
|
@ -5,79 +5,27 @@ sidebar:
|
|||
order: 4
|
||||
---
|
||||
|
||||
## Bắt đầu tại đây: BMad-Help
|
||||
Hãy dùng trợ giúp tích hợp sẵn của BMad, tài liệu nguồn, hoặc cộng đồng để tìm câu trả lời, theo thứ tự từ nhanh nhất đến đầy đủ nhất.
|
||||
|
||||
**Cách nhanh nhất để tìm câu trả lời về BMad là dùng skill `bmad-help`.** Đây là công cụ hướng dẫn thông minh có thể trả lời hơn 80% các câu hỏi và có sẵn ngay trong IDE khi bạn làm việc.
|
||||
## 1. Hỏi BMad-Help
|
||||
|
||||
BMad-Help không chỉ là công cụ tra cứu, nó còn:
|
||||
- **Kiểm tra dự án của bạn** để xem những gì đã hoàn thành
|
||||
- **Hiểu ngôn ngữ tự nhiên** - đặt câu hỏi bằng ngôn ngữ bình thường
|
||||
- **Thay đổi theo module đã cài** - hiển thị các lựa chọn liên quan
|
||||
- **Tự động chạy sau workflow** - nói rõ bạn cần làm gì tiếp theo
|
||||
- **Đề xuất tác vụ đầu tiên cần thiết** - không cần đoán nên bắt đầu từ đâu
|
||||
|
||||
### Cách dùng BMad-Help
|
||||
|
||||
Gọi nó trực tiếp trong phiên AI của bạn:
|
||||
Cách nhanh nhất để có câu trả lời. Skill `bmad-help` có sẵn ngay trong phiên AI của bạn và xử lý được hơn 80% câu hỏi. Nó sẽ kiểm tra dự án, nhìn xem bạn đã hoàn thành đến đâu và cho bạn biết nên làm gì tiếp theo.
|
||||
|
||||
```text
|
||||
bmad-help
|
||||
bmad-help Tôi có ý tưởng SaaS và đã biết tất cả tính năng. Tôi nên bắt đầu từ đâu?
|
||||
bmad-help Tôi có những lựa chọn nào cho thiết kế UX?
|
||||
bmad-help Tôi đang bị mắc ở workflow PRD
|
||||
```
|
||||
|
||||
:::tip
|
||||
Bạn cũng có thể dùng `/bmad-help` hoặc `$bmad-help` tùy nền tảng, nhưng chỉ `bmad-help` là cách nên hoạt động mọi nơi.
|
||||
:::
|
||||
|
||||
Kết hợp với câu hỏi ngôn ngữ tự nhiên:
|
||||
## 2. Đi sâu hơn với mã nguồn
|
||||
|
||||
```text
|
||||
bmad-help Tôi có ý tưởng SaaS và đã biết tất cả tính năng. Tôi nên bắt đầu từ đâu?
|
||||
bmad-help Tôi có những lựa chọn nào cho thiết kế UX?
|
||||
bmad-help Tôi đang bị mắc ở workflow PRD
|
||||
bmad-help Cho tôi xem tôi đã làm được gì đến giờ
|
||||
```
|
||||
BMad-Help dựa trên cấu hình bạn đã cài đặt. Nếu bạn cần tìm hiểu nội bộ, lịch sử, hay kiến trúc của BMad, hoặc đang nghiên cứu BMad trước khi cài, hãy để AI đọc trực tiếp mã nguồn.
|
||||
|
||||
BMad-Help sẽ trả lời:
|
||||
- Điều gì được khuyến nghị cho tình huống của bạn
|
||||
- Tác vụ đầu tiên cần thiết là gì
|
||||
- Phần còn lại của quy trình trông thế nào
|
||||
|
||||
## Khi nào nên dùng tài liệu này
|
||||
|
||||
Hãy xem phần này khi:
|
||||
- Bạn muốn hiểu kiến trúc hoặc nội bộ của BMad
|
||||
- Bạn cần câu trả lời nằm ngoài phạm vi BMad-Help cung cấp
|
||||
- Bạn đang nghiên cứu BMad trước khi cài đặt
|
||||
- Bạn muốn tự khám phá source code trực tiếp
|
||||
|
||||
## Các bước thực hiện
|
||||
|
||||
### 1. Chọn nguồn thông tin
|
||||
|
||||
| Nguồn | Phù hợp nhất cho | Ví dụ |
|
||||
| --- | --- | --- |
|
||||
| **Thư mục `_bmad`** | Cách BMad vận hành: agent, workflow, prompt | "PM agent làm gì?" |
|
||||
| **Toàn bộ repo GitHub** | Lịch sử, installer, kiến trúc | "v6 thay đổi gì?" |
|
||||
| **`llms-full.txt`** | Tổng quan nhanh từ tài liệu | "Giải thích bốn giai đoạn của BMad" |
|
||||
|
||||
Thư mục `_bmad` được tạo khi bạn cài đặt BMad. Nếu chưa có, hãy clone repo thay thế.
|
||||
|
||||
### 2. Cho AI của bạn truy cập nguồn thông tin
|
||||
|
||||
**Nếu AI của bạn đọc được tệp (Claude Code, Cursor, ...):**
|
||||
|
||||
- **Đã cài BMad:** Trỏ đến thư mục `_bmad` và hỏi trực tiếp
|
||||
- **Cần bối cảnh sâu hơn:** Clone [repo đầy đủ](https://github.com/bmad-code-org/BMAD-METHOD)
|
||||
|
||||
**Nếu bạn dùng ChatGPT hoặc Claude.ai:**
|
||||
|
||||
Nạp `llms-full.txt` vào phiên làm việc:
|
||||
|
||||
```text
|
||||
https://bmad-code-org.github.io/BMAD-METHOD/llms-full.txt
|
||||
```
|
||||
|
||||
### 3. Đặt câu hỏi
|
||||
Hãy clone hoặc mở [repo BMAD-METHOD](https://github.com/bmad-code-org/BMAD-METHOD) rồi hỏi AI của bạn về nó. Bất kỳ công cụ nào có hỗ trợ agent như Claude Code, Cursor, Windsurf... đều có thể đọc mã nguồn và trả lời trực tiếp.
|
||||
|
||||
:::note[Ví dụ]
|
||||
**Q:** "Hãy chỉ tôi cách nhanh nhất để xây dựng một thứ gì đó bằng BMad"
|
||||
|
|
@ -85,29 +33,27 @@ https://bmad-code-org.github.io/BMAD-METHOD/llms-full.txt
|
|||
**A:** Dùng Quick Flow: Chạy `bmad-quick-dev` - nó sẽ làm rõ ý định, lập kế hoạch, triển khai, review và trình bày kết quả trong một workflow duy nhất, bỏ qua các giai đoạn lập kế hoạch đầy đủ.
|
||||
:::
|
||||
|
||||
## Bạn nhận được gì
|
||||
**Mẹo để có câu trả lời tốt hơn:**
|
||||
|
||||
Các câu trả lời trực tiếp về BMad: agent hoạt động ra sao, workflow làm gì, tại sao cấu trúc lại được tổ chức như vậy, mà không cần chờ người khác trả lời.
|
||||
- **Hãy hỏi thật cụ thể** - "Bước 3 trong workflow PRD làm gì?" sẽ tốt hơn "PRD hoạt động ra sao?"
|
||||
- **Kiểm tra lại những câu trả lời nghe lạ** - LLM đôi khi vẫn sai. Hãy kiểm tra file nguồn hoặc hỏi trên Discord.
|
||||
|
||||
## Mẹo
|
||||
### Không dùng agent? Dùng trang docs
|
||||
|
||||
- **Xác minh những câu trả lời gây bất ngờ** - LLM vẫn có lúc nhầm. Hãy kiểm tra tệp nguồn hoặc hỏi trên Discord.
|
||||
- **Đặt câu hỏi cụ thể** - "Bước 3 trong workflow PRD làm gì?" tốt hơn "PRD hoạt động ra sao?"
|
||||
Nếu AI của bạn không đọc được file cục bộ như ChatGPT hoặc Claude.ai, hãy nạp [llms-full.txt](https://bmad-code-org.github.io/BMAD-METHOD/llms-full.txt) vào phiên làm việc. Đây là bản chụp tài liệu BMad trong một file duy nhất.
|
||||
|
||||
## Vẫn bị mắc?
|
||||
## 3. Hỏi người thật
|
||||
|
||||
Đã thử cách tiếp cận bằng LLM mà vẫn cần trợ giúp? Lúc này bạn đã có một câu hỏi tốt hơn để đem đi hỏi.
|
||||
Nếu cả BMad-Help lẫn mã nguồn vẫn chưa trả lời được câu hỏi của bạn, lúc này bạn đã có một câu hỏi rõ hơn nhiều để đem đi hỏi cộng đồng.
|
||||
|
||||
| Kênh | Dùng cho |
|
||||
| --- | --- |
|
||||
| `#bmad-method-help` | Câu hỏi nhanh (trò chuyện thời gian thực) |
|
||||
| `help-requests` forum | Câu hỏi chi tiết (có thể tìm lại, tồn tại lâu dài) |
|
||||
| `help-requests` forum | Câu hỏi |
|
||||
| `#suggestions-feedback` | Ý tưởng và đề xuất tính năng |
|
||||
| `#report-bugs-and-issues` | Báo cáo lỗi |
|
||||
|
||||
**Discord:** [discord.gg/gk8jAdXWmj](https://discord.gg/gk8jAdXWmj)
|
||||
|
||||
**GitHub Issues:** [github.com/bmad-code-org/BMAD-METHOD/issues](https://github.com/bmad-code-org/BMAD-METHOD/issues) (dành cho các lỗi rõ ràng)
|
||||
**GitHub Issues:** [github.com/bmad-code-org/BMAD-METHOD/issues](https://github.com/bmad-code-org/BMAD-METHOD/issues)
|
||||
|
||||
*Chính bạn,*
|
||||
*đang mắc kẹt*
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
title: "Quản lý Project Context"
|
||||
title: "Quản lý bối cảnh dự án"
|
||||
description: Tạo và duy trì project-context.md để định hướng cho các agent AI
|
||||
sidebar:
|
||||
order: 8
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
title: "Quick Fixes"
|
||||
title: "Sửa nhanh"
|
||||
description: Cách thực hiện các sửa nhanh và thay đổi ad-hoc
|
||||
sidebar:
|
||||
order: 5
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
title: Agents
|
||||
title: Các agent
|
||||
description: Các agent mặc định của BMM cùng skill ID, trigger menu và workflow chính
|
||||
sidebar:
|
||||
order: 2
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
title: Skills
|
||||
title: Các skill
|
||||
description: Tài liệu tham chiếu cho skill của BMad — skill là gì, hoạt động ra sao và tìm ở đâu.
|
||||
sidebar:
|
||||
order: 3
|
||||
|
|
|
|||
|
|
@ -1,31 +1,31 @@
|
|||
---
|
||||
title: Core Tools
|
||||
description: Tài liệu tham chiếu cho mọi task và workflow tích hợp sẵn có trong mọi bản cài BMad mà không cần module bổ sung.
|
||||
title: Công cụ cốt lõi
|
||||
description: Tài liệu tham chiếu cho mọi tác vụ và quy trình tích hợp sẵn có trong mọi bản cài BMad mà không cần module bổ sung.
|
||||
sidebar:
|
||||
order: 2
|
||||
---
|
||||
|
||||
Mọi bản cài BMad đều bao gồm một tập core skills có thể dùng cùng với bất cứ việc gì bạn đang làm — các task và workflow độc lập hoạt động xuyên suốt mọi dự án, mọi module và mọi phase. Chúng luôn có sẵn bất kể bạn cài những module tùy chọn nào.
|
||||
Mọi bản cài BMad đều bao gồm một tập skill cốt lõi có thể dùng cùng với bất cứ việc gì bạn đang làm, các tác vụ và quy trình độc lập hoạt động xuyên suốt mọi dự án, mọi module và mọi giai đoạn. Chúng luôn có sẵn bất kể bạn cài những module tùy chọn nào.
|
||||
|
||||
:::tip[Lối đi nhanh]
|
||||
Chạy bất kỳ core tool nào bằng cách gõ tên skill của nó, ví dụ `bmad-help`, trong IDE của bạn. Không cần mở phiên agent trước.
|
||||
Chạy bất kỳ công cụ cốt lõi nào bằng cách gõ tên skill của nó, ví dụ `bmad-help`, trong IDE của bạn. Không cần mở phiên agent trước.
|
||||
:::
|
||||
|
||||
## Tổng Quan
|
||||
|
||||
| Công cụ | Loại | Mục đích |
|
||||
| --- | --- | --- |
|
||||
| [`bmad-help`](#bmad-help) | Task | Nhận hướng dẫn có ngữ cảnh về việc nên làm gì tiếp theo |
|
||||
| [`bmad-brainstorming`](#bmad-brainstorming) | Workflow | Tổ chức các phiên brainstorming có tương tác |
|
||||
| [`bmad-party-mode`](#bmad-party-mode) | Workflow | Điều phối thảo luận nhóm nhiều agent |
|
||||
| [`bmad-distillator`](#bmad-distillator) | Task | Nén tài liệu tối ưu cho LLM mà không mất thông tin |
|
||||
| [`bmad-advanced-elicitation`](#bmad-advanced-elicitation) | Task | Đẩy đầu ra của LLM qua các vòng tinh luyện lặp |
|
||||
| [`bmad-review-adversarial-general`](#bmad-review-adversarial-general) | Task | Review hoài nghi để tìm chỗ thiếu và chỗ sai |
|
||||
| [`bmad-review-edge-case-hunter`](#bmad-review-edge-case-hunter) | Task | Phân tích toàn bộ nhánh rẽ để tìm edge case chưa được xử lý |
|
||||
| [`bmad-editorial-review-prose`](#bmad-editorial-review-prose) | Task | Biên tập câu chữ nhằm tăng độ rõ ràng khi giao tiếp |
|
||||
| [`bmad-editorial-review-structure`](#bmad-editorial-review-structure) | Task | Biên tập cấu trúc — cắt, gộp và tổ chức lại |
|
||||
| [`bmad-shard-doc`](#bmad-shard-doc) | Task | Tách file markdown lớn thành các phần có tổ chức |
|
||||
| [`bmad-index-docs`](#bmad-index-docs) | Task | Tạo hoặc cập nhật mục lục cho toàn bộ tài liệu trong một thư mục |
|
||||
| [`bmad-help`](#bmad-help) | Tác vụ | Nhận hướng dẫn có ngữ cảnh về việc nên làm gì tiếp theo |
|
||||
| [`bmad-brainstorming`](#bmad-brainstorming) | Quy trình | Tổ chức các phiên brainstorming có tương tác |
|
||||
| [`bmad-party-mode`](#bmad-party-mode) | Quy trình | Điều phối thảo luận nhóm nhiều agent |
|
||||
| [`bmad-distillator`](#bmad-distillator) | Tác vụ | Nén tài liệu tối ưu cho LLM mà không mất thông tin |
|
||||
| [`bmad-advanced-elicitation`](#bmad-advanced-elicitation) | Tác vụ | Đẩy đầu ra của LLM qua các vòng tinh luyện lặp |
|
||||
| [`bmad-review-adversarial-general`](#bmad-review-adversarial-general) | Tác vụ | Rà soát hoài nghi để tìm chỗ thiếu và chỗ sai |
|
||||
| [`bmad-review-edge-case-hunter`](#bmad-review-edge-case-hunter) | Tác vụ | Phân tích toàn bộ nhánh rẽ để tìm trường hợp biên chưa được xử lý |
|
||||
| [`bmad-editorial-review-prose`](#bmad-editorial-review-prose) | Tác vụ | Biên tập câu chữ nhằm tăng độ rõ ràng khi giao tiếp |
|
||||
| [`bmad-editorial-review-structure`](#bmad-editorial-review-structure) | Tác vụ | Biên tập cấu trúc — cắt, gộp và tổ chức lại |
|
||||
| [`bmad-shard-doc`](#bmad-shard-doc) | Tác vụ | Tách file markdown lớn thành các phần có tổ chức |
|
||||
| [`bmad-index-docs`](#bmad-index-docs) | Tác vụ | Tạo hoặc cập nhật mục lục cho toàn bộ tài liệu trong một thư mục |
|
||||
|
||||
## bmad-help
|
||||
|
||||
|
|
@ -33,7 +33,7 @@ Chạy bất kỳ core tool nào bằng cách gõ tên skill của nó, ví dụ
|
|||
|
||||
**Dùng khi:**
|
||||
|
||||
- Bạn vừa hoàn tất một workflow và muốn biết tiếp theo là gì
|
||||
- Bạn vừa hoàn tất một quy trình và muốn biết tiếp theo là gì
|
||||
- Bạn mới làm quen với BMad và cần định hướng
|
||||
- Bạn đang mắc kẹt và muốn lời khuyên có ngữ cảnh
|
||||
- Bạn vừa cài module mới và muốn xem có gì khả dụng
|
||||
|
|
@ -51,7 +51,7 @@ Chạy bất kỳ core tool nào bằng cách gõ tên skill của nó, ví dụ
|
|||
|
||||
## bmad-brainstorming
|
||||
|
||||
**Tạo ra nhiều ý tưởng đa dạng bằng các kỹ thuật sáng tạo có tương tác.** Đây là một phiên brainstorming có điều phối, nạp các phương pháp phát ý tưởng đã được kiểm chứng từ thư viện kỹ thuật và dẫn bạn đến 100+ ý tưởng trước khi bắt đầu sắp xếp.
|
||||
**Tạo ra nhiều ý tưởng đa dạng bằng các kỹ thuật sáng tạo có tương tác.** Đây là một phiên động não có điều phối, nạp các phương pháp phát ý tưởng đã được kiểm chứng từ thư viện kỹ thuật và dẫn bạn đến 100+ ý tưởng trước khi bắt đầu sắp xếp.
|
||||
|
||||
**Dùng khi:**
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
---
|
||||
title: "Workflow Map"
|
||||
description: Tài liệu trực quan về các phase workflow và output của BMad Method
|
||||
title: "Sơ đồ workflow"
|
||||
description: Tài liệu trực quan về các giai đoạn, quy trình và đầu ra của BMad Method
|
||||
sidebar:
|
||||
order: 1
|
||||
---
|
||||
|
||||
BMad Method (BMM) là một module trong hệ sinh thái BMad, tập trung vào các thực hành tốt nhất của context engineering và lập kế hoạch. AI agent hoạt động hiệu quả nhất khi có ngữ cảnh rõ ràng và có cấu trúc. Hệ thống BMM xây dựng ngữ cảnh đó theo tiến trình qua 4 phase riêng biệt. Mỗi phase, cùng với nhiều workflow tùy chọn bên trong phase đó, tạo ra các tài liệu làm đầu vào cho phase kế tiếp, nhờ vậy agent luôn biết phải xây gì và vì sao.
|
||||
BMad Method (BMM) là một module trong hệ sinh thái BMad, tập trung vào các thực hành tốt nhất của kỹ nghệ ngữ cảnh và lập kế hoạch. AI agent hoạt động hiệu quả nhất khi có ngữ cảnh rõ ràng và có cấu trúc. Hệ thống BMM xây dựng ngữ cảnh đó theo tiến trình qua 4 giai đoạn riêng biệt. Mỗi giai đoạn, cùng với nhiều quy trình tùy chọn bên trong nó, tạo ra các tài liệu làm đầu vào cho giai đoạn kế tiếp, nhờ vậy agent luôn biết phải xây gì và vì sao.
|
||||
|
||||
Lý do và các khái niệm nền tảng ở đây đến từ các phương pháp agile đã được áp dụng rất thành công trong toàn ngành như một khung tư duy.
|
||||
Lý do và các khái niệm nền tảng ở đây đến từ các phương pháp Agile đã được áp dụng rất thành công trong toàn ngành như một khung tư duy.
|
||||
|
||||
Nếu có lúc nào bạn không chắc nên làm gì, skill `bmad-help` sẽ giúp bạn giữ đúng hướng hoặc biết bước tiếp theo. Bạn vẫn có thể dùng trang này để tham chiếu, nhưng `bmad-help` mang tính tương tác đầy đủ và nhanh hơn nhiều nếu bạn đã cài BMad Method. Ngoài ra, nếu bạn đang dùng thêm các module mở rộng BMad Method hoặc các module bổ sung khác, `bmad-help` cũng sẽ phát triển theo để biết mọi thứ đang có sẵn và đưa ra lời khuyên tốt nhất tại thời điểm đó.
|
||||
Nếu có lúc nào bạn không chắc nên làm gì, skill `bmad-help` sẽ giúp bạn giữ đúng hướng hoặc biết bước tiếp theo. Bạn vẫn có thể dùng trang này để tham chiếu, nhưng `bmad-help` mang tính tương tác đầy đủ và nhanh hơn nhiều nếu bạn đã cài BMad Method. Ngoài ra, nếu bạn đang dùng thêm các module mở rộng BMad Method hoặc các module bổ sung khác, `bmad-help` cũng sẽ mở rộng theo để biết mọi thứ đang có sẵn và đưa ra lời khuyên tốt nhất tại thời điểm đó.
|
||||
|
||||
Lưu ý quan trọng cuối cùng: mọi workflow dưới đây đều có thể chạy trực tiếp bằng công cụ bạn chọn thông qua skill, hoặc bằng cách nạp agent trước rồi chọn mục tương ứng trong menu agent.
|
||||
Lưu ý quan trọng cuối cùng: mọi quy trình dưới đây đều có thể chạy trực tiếp bằng công cụ bạn chọn thông qua skill, hoặc bằng cách nạp agent trước rồi chọn mục tương ứng trong menu agent.
|
||||
|
||||
<iframe src="/workflow-map-diagram.html" title="Sơ đồ Workflow Map của BMad Method" width="100%" height="100%" style="border-radius: 8px; border: 1px solid #334155; min-height: 900px;"></iframe>
|
||||
|
||||
|
|
@ -19,43 +19,43 @@ Lưu ý quan trọng cuối cùng: mọi workflow dưới đây đều có thể
|
|||
<a href="/workflow-map-diagram.html" target="_blank" rel="noopener noreferrer">Mở sơ đồ trong tab mới ↗</a>
|
||||
</p>
|
||||
|
||||
## Phase 1: Analysis (Tùy chọn)
|
||||
## Giai đoạn 1: Phân tích (tùy chọn)
|
||||
|
||||
Khám phá không gian vấn đề và xác nhận ý tưởng trước khi cam kết đi vào lập kế hoạch. [**Tìm hiểu từng công cụ làm gì và nên dùng khi nào**](../explanation/analysis-phase.md).
|
||||
|
||||
| Workflow | Mục đích | Tạo ra |
|
||||
| Quy trình | Mục đích | Tạo ra |
|
||||
| ------------------------------- | -------------------------------------------------------------------------- | ------------------------- |
|
||||
| `bmad-brainstorming` | Brainstorm ý tưởng dự án với sự điều phối của brainstorming coach | `brainstorming-report.md` |
|
||||
| `bmad-brainstorming` | Động não ý tưởng dự án với sự điều phối của người dẫn dắt brainstorming | `brainstorming-report.md` |
|
||||
| `bmad-domain-research`, `bmad-market-research`, `bmad-technical-research` | Xác thực giả định về thị trường, kỹ thuật hoặc miền nghiệp vụ | Kết quả nghiên cứu |
|
||||
| `bmad-product-brief` | Ghi lại tầm nhìn chiến lược — phù hợp nhất khi concept của bạn đã rõ | `product-brief.md` |
|
||||
| `bmad-prfaq` | Working Backwards — stress-test và rèn sắc concept sản phẩm của bạn | `prfaq-{project}.md` |
|
||||
|
||||
## Phase 2: Planning
|
||||
## Giai đoạn 2: Lập kế hoạch
|
||||
|
||||
Xác định cần xây gì và xây cho ai.
|
||||
|
||||
| Workflow | Mục đích | Tạo ra |
|
||||
| Quy trình | Mục đích | Tạo ra |
|
||||
| --------------------------- | ---------------------------------------- | ------------ |
|
||||
| `bmad-create-prd` | Xác định yêu cầu (FR/NFR) | `PRD.md` |
|
||||
| `bmad-create-ux-design` | Thiết kế trải nghiệm người dùng khi UX là yếu tố quan trọng | `ux-spec.md` |
|
||||
|
||||
## Phase 3: Solutioning
|
||||
## Giai đoạn 3: Định hình giải pháp
|
||||
|
||||
Quyết định cách xây và chia nhỏ công việc thành stories.
|
||||
Quyết định cách xây và chia nhỏ công việc thành các story.
|
||||
|
||||
| Workflow | Mục đích | Tạo ra |
|
||||
| Quy trình | Mục đích | Tạo ra |
|
||||
| ----------------------------------------- | ------------------------------------------ | --------------------------- |
|
||||
| `bmad-create-architecture` | Làm rõ các quyết định kỹ thuật | `architecture.md` kèm ADR |
|
||||
| `bmad-create-epics-and-stories` | Phân rã yêu cầu thành các phần việc có thể triển khai | Các file epic chứa stories |
|
||||
| `bmad-create-epics-and-stories` | Phân rã yêu cầu thành các phần việc có thể triển khai | Các file epic chứa các story |
|
||||
| `bmad-check-implementation-readiness` | Cổng kiểm tra trước khi triển khai | Quyết định PASS/CONCERNS/FAIL |
|
||||
|
||||
## Phase 4: Implementation
|
||||
## Giai đoạn 4: Triển khai
|
||||
|
||||
Xây dựng từng story một. Tự động hóa toàn bộ phase 4 sẽ sớm ra mắt.
|
||||
Xây dựng từng story một. Tự động hóa toàn bộ giai đoạn 4 sẽ sớm ra mắt.
|
||||
|
||||
| Workflow | Mục đích | Tạo ra |
|
||||
| Quy trình | Mục đích | Tạo ra |
|
||||
| -------------------------- | ------------------------------------------------------------------------ | -------------------------------- |
|
||||
| `bmad-sprint-planning` | Khởi tạo theo dõi, thường chạy một lần mỗi dự án để sắp thứ tự chu trình dev | `sprint-status.yaml` |
|
||||
| `bmad-sprint-planning` | Khởi tạo theo dõi, thường chạy một lần mỗi dự án để sắp thứ tự chu trình phát triển | `sprint-status.yaml` |
|
||||
| `bmad-create-story` | Chuẩn bị story tiếp theo cho implementation | `story-[slug].md` |
|
||||
| `bmad-dev-story` | Triển khai story | Code chạy được + tests |
|
||||
| `bmad-code-review` | Kiểm tra chất lượng phần triển khai | Được duyệt hoặc yêu cầu thay đổi |
|
||||
|
|
@ -63,22 +63,22 @@ Xây dựng từng story một. Tự động hóa toàn bộ phase 4 sẽ sớm
|
|||
| `bmad-sprint-status` | Theo dõi tiến độ sprint và trạng thái story | Cập nhật trạng thái sprint |
|
||||
| `bmad-retrospective` | Review sau khi hoàn tất epic | Bài học rút ra |
|
||||
|
||||
## Quick Flow (Nhánh Song Song)
|
||||
## Luồng nhanh (nhánh song song)
|
||||
|
||||
Bỏ qua phase 1-3 đối với những việc nhỏ, rõ và đã hiểu đầy đủ.
|
||||
Bỏ qua giai đoạn 1-3 đối với những việc nhỏ, rõ và đã hiểu đầy đủ.
|
||||
|
||||
| Workflow | Mục đích | Tạo ra |
|
||||
| Quy trình | Mục đích | Tạo ra |
|
||||
| ------------------ | --------------------------------------------------------------------------- | ---------------------- |
|
||||
| `bmad-quick-dev` | Luồng nhanh hợp nhất — làm rõ yêu cầu, lập kế hoạch, triển khai, review và trình bày | `spec-*.md` + mã nguồn |
|
||||
|
||||
## Quản Lý Context
|
||||
## Quản lý ngữ cảnh
|
||||
|
||||
Mỗi tài liệu sẽ trở thành context cho phase tiếp theo. PRD cho architect biết những ràng buộc nào quan trọng. Architecture chỉ cho dev agent những pattern cần tuân theo. File story cung cấp context tập trung và đầy đủ cho việc triển khai. Nếu không có cấu trúc này, agent sẽ đưa ra quyết định thiếu nhất quán.
|
||||
Mỗi tài liệu sẽ trở thành ngữ cảnh cho giai đoạn tiếp theo. PRD cho architect biết những ràng buộc nào quan trọng. Tài liệu kiến trúc chỉ cho dev agent những mẫu cần tuân theo. File story cung cấp ngữ cảnh tập trung và đầy đủ cho việc triển khai. Nếu không có cấu trúc này, agent sẽ đưa ra quyết định thiếu nhất quán.
|
||||
|
||||
### Project Context
|
||||
### Bối cảnh dự án
|
||||
|
||||
:::tip[Khuyến nghị]
|
||||
Hãy tạo `project-context.md` để bảo đảm AI agent tuân theo quy tắc và sở thích của dự án. File này hoạt động như một bản hiến pháp cho dự án của bạn, nó dẫn dắt các quyết định triển khai xuyên suốt mọi workflow. File tùy chọn này có thể được tạo ở cuối bước Architecture Creation, hoặc cũng có thể được sinh trong dự án hiện hữu để ghi lại những điều quan trọng cần giữ đồng bộ với quy ước đang có.
|
||||
Hãy tạo `project-context.md` để bảo đảm AI agent tuân theo quy tắc và sở thích của dự án. File này hoạt động như một bản hiến pháp cho dự án của bạn, nó dẫn dắt các quyết định triển khai xuyên suốt mọi quy trình. File tùy chọn này có thể được tạo ở cuối bước tạo kiến trúc, hoặc cũng có thể được sinh trong dự án hiện hữu để ghi lại những điều quan trọng cần giữ đồng bộ với quy ước đang có.
|
||||
:::
|
||||
|
||||
**Cách tạo:**
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "bmad-method",
|
||||
"version": "6.2.2",
|
||||
"version": "6.3.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "bmad-method",
|
||||
"version": "6.2.2",
|
||||
"version": "6.3.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@clack/core": "^1.0.0",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "bmad-method",
|
||||
"version": "6.2.2",
|
||||
"version": "6.3.0",
|
||||
"description": "Breakthrough Method of Agile AI-driven Development",
|
||||
"keywords": [
|
||||
"agile",
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ module.exports = {
|
|||
['--communication-language <lang>', 'Language for agent communication (default: English)'],
|
||||
['--document-output-language <lang>', 'Language for document output (default: English)'],
|
||||
['--output-folder <path>', 'Output folder path relative to project root (default: _bmad-output)'],
|
||||
['--custom-source <sources>', 'Comma-separated Git URLs or local paths to install custom modules from'],
|
||||
['-y, --yes', 'Accept all defaults and skip prompts where possible'],
|
||||
],
|
||||
action: async (options) => {
|
||||
|
|
|
|||
|
|
@ -569,6 +569,7 @@ class Installer {
|
|||
*/
|
||||
async _installOfficialModules(config, paths, officialModuleIds, addResult, isQuickUpdate, officialModules, ctx) {
|
||||
const { message, installedModuleNames } = ctx;
|
||||
const { CustomModuleManager } = require('../modules/custom-module-manager');
|
||||
|
||||
for (const moduleName of officialModuleIds) {
|
||||
if (installedModuleNames.has(moduleName)) continue;
|
||||
|
|
@ -591,11 +592,15 @@ class Installer {
|
|||
},
|
||||
);
|
||||
|
||||
// Get display name from source module.yaml; version from marketplace.json
|
||||
// Get display name from source module.yaml; version from resolution cache or marketplace.json
|
||||
const sourcePath = await officialModules.findModuleSource(moduleName, { silent: true });
|
||||
const moduleInfo = sourcePath ? await officialModules.getModuleInfo(sourcePath, moduleName, '') : null;
|
||||
const displayName = moduleInfo?.name || moduleName;
|
||||
const version = sourcePath ? await this._getMarketplaceVersion(sourcePath) : '';
|
||||
|
||||
// Prefer version from resolution cache (accurate for custom/local modules),
|
||||
// fall back to marketplace.json walk-up for official modules
|
||||
const cachedResolution = CustomModuleManager._resolutionCache.get(moduleName);
|
||||
const version = cachedResolution?.version || (sourcePath ? await this._getMarketplaceVersion(sourcePath) : '');
|
||||
addResult(displayName, 'ok', '', { moduleCode: moduleName, newVersion: version });
|
||||
}
|
||||
}
|
||||
|
|
@ -1189,7 +1194,7 @@ class Installer {
|
|||
const customMgr = new CustomModuleManager();
|
||||
for (const moduleId of installedModules) {
|
||||
if (!availableModules.some((m) => m.id === moduleId)) {
|
||||
const customSource = await customMgr.findModuleSourceByCode(moduleId);
|
||||
const customSource = await customMgr.findModuleSourceByCode(moduleId, { bmadDir });
|
||||
if (customSource) {
|
||||
availableModules.push({
|
||||
id: moduleId,
|
||||
|
|
|
|||
|
|
@ -412,7 +412,7 @@ class ManifestGenerator {
|
|||
// Get existing install date if available
|
||||
const existing = existingModulesMap.get(moduleName);
|
||||
|
||||
updatedModules.push({
|
||||
const moduleEntry = {
|
||||
name: moduleName,
|
||||
version: versionInfo.version,
|
||||
installDate: existing?.installDate || new Date().toISOString(),
|
||||
|
|
@ -420,7 +420,9 @@ class ManifestGenerator {
|
|||
source: versionInfo.source,
|
||||
npmPackage: versionInfo.npmPackage,
|
||||
repoUrl: versionInfo.repoUrl,
|
||||
});
|
||||
};
|
||||
if (versionInfo.localPath) moduleEntry.localPath = versionInfo.localPath;
|
||||
updatedModules.push(moduleEntry);
|
||||
}
|
||||
|
||||
const manifest = {
|
||||
|
|
|
|||
|
|
@ -181,10 +181,10 @@ class Manifest {
|
|||
|
||||
// Handle adding a new module with version info
|
||||
if (updates.addModule) {
|
||||
const { name, version, source, npmPackage, repoUrl } = updates.addModule;
|
||||
const { name, version, source, npmPackage, repoUrl, localPath } = updates.addModule;
|
||||
const existing = manifest.modules.find((m) => m.name === name);
|
||||
if (!existing) {
|
||||
manifest.modules.push({
|
||||
const entry = {
|
||||
name,
|
||||
version: version || null,
|
||||
installDate: new Date().toISOString(),
|
||||
|
|
@ -192,7 +192,9 @@ class Manifest {
|
|||
source: source || 'external',
|
||||
npmPackage: npmPackage || null,
|
||||
repoUrl: repoUrl || null,
|
||||
});
|
||||
};
|
||||
if (localPath) entry.localPath = localPath;
|
||||
manifest.modules.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -280,7 +282,7 @@ class Manifest {
|
|||
|
||||
if (existingIndex === -1) {
|
||||
// Module doesn't exist, add it
|
||||
manifest.modules.push({
|
||||
const entry = {
|
||||
name: moduleName,
|
||||
version: options.version || null,
|
||||
installDate: new Date().toISOString(),
|
||||
|
|
@ -288,7 +290,9 @@ class Manifest {
|
|||
source: options.source || 'unknown',
|
||||
npmPackage: options.npmPackage || null,
|
||||
repoUrl: options.repoUrl || null,
|
||||
});
|
||||
};
|
||||
if (options.localPath) entry.localPath = options.localPath;
|
||||
manifest.modules.push(entry);
|
||||
} else {
|
||||
// Module exists, update its version info
|
||||
const existing = manifest.modules[existingIndex];
|
||||
|
|
@ -298,6 +302,7 @@ class Manifest {
|
|||
source: options.source || existing.source,
|
||||
npmPackage: options.npmPackage === undefined ? existing.npmPackage : options.npmPackage,
|
||||
repoUrl: options.repoUrl === undefined ? existing.repoUrl : options.repoUrl,
|
||||
localPath: options.localPath === undefined ? existing.localPath : options.localPath,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
|
@ -832,17 +837,19 @@ class Manifest {
|
|||
};
|
||||
}
|
||||
|
||||
// Check if this is a custom module (from user-provided URL)
|
||||
// Check if this is a custom module (from user-provided URL or local path)
|
||||
const { CustomModuleManager } = require('../modules/custom-module-manager');
|
||||
const customMgr = new CustomModuleManager();
|
||||
const customSource = await customMgr.findModuleSourceByCode(moduleName);
|
||||
if (customSource) {
|
||||
const customVersion = await this._readMarketplaceVersion(moduleName, moduleSourcePath);
|
||||
const resolved = customMgr.getResolution(moduleName);
|
||||
const customSource = await customMgr.findModuleSourceByCode(moduleName, { bmadDir });
|
||||
if (customSource || resolved) {
|
||||
const customVersion = resolved?.version || (await this._readMarketplaceVersion(moduleName, moduleSourcePath));
|
||||
return {
|
||||
version: customVersion,
|
||||
source: 'custom',
|
||||
npmPackage: null,
|
||||
repoUrl: null,
|
||||
repoUrl: resolved?.repoUrl || null,
|
||||
localPath: resolved?.localPath || null,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,22 +3,161 @@ const os = require('node:os');
|
|||
const path = require('node:path');
|
||||
const { execSync } = require('node:child_process');
|
||||
const prompts = require('../prompts');
|
||||
const { RegistryClient } = require('./registry-client');
|
||||
|
||||
/**
|
||||
* Manages custom modules installed from user-provided GitHub URLs.
|
||||
* Validates URLs, fetches .claude-plugin/marketplace.json, clones repos.
|
||||
* Manages custom modules installed from user-provided sources.
|
||||
* Supports any Git host (GitHub, GitLab, Bitbucket, self-hosted) and local file paths.
|
||||
* Validates input, clones repos, reads .claude-plugin/marketplace.json, resolves plugins.
|
||||
*/
|
||||
class CustomModuleManager {
|
||||
constructor() {
|
||||
this._client = new RegistryClient();
|
||||
}
|
||||
/** @type {Map<string, Object>} Shared across all instances: module code -> ResolvedModule */
|
||||
static _resolutionCache = new Map();
|
||||
|
||||
// ─── URL Validation ───────────────────────────────────────────────────────
|
||||
// ─── Source Parsing ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Parse a user-provided source input into a structured descriptor.
|
||||
* Accepts local file paths, HTTPS Git URLs, and SSH Git URLs.
|
||||
* For HTTPS URLs with deep paths (e.g., /tree/main/subdir), extracts the subdir.
|
||||
*
|
||||
* @param {string} input - URL or local file path
|
||||
* @returns {Object} Parsed source descriptor:
|
||||
* { type: 'url'|'local', cloneUrl, subdir, localPath, cacheKey, displayName, isValid, error }
|
||||
*/
|
||||
parseSource(input) {
|
||||
if (!input || typeof input !== 'string') {
|
||||
return {
|
||||
type: null,
|
||||
cloneUrl: null,
|
||||
subdir: null,
|
||||
localPath: null,
|
||||
cacheKey: null,
|
||||
displayName: null,
|
||||
isValid: false,
|
||||
error: 'Source is required',
|
||||
};
|
||||
}
|
||||
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) {
|
||||
return {
|
||||
type: null,
|
||||
cloneUrl: null,
|
||||
subdir: null,
|
||||
localPath: null,
|
||||
cacheKey: null,
|
||||
displayName: null,
|
||||
isValid: false,
|
||||
error: 'Source is required',
|
||||
};
|
||||
}
|
||||
|
||||
// Local path detection: starts with /, ./, ../, or ~
|
||||
if (trimmed.startsWith('/') || trimmed.startsWith('./') || trimmed.startsWith('../') || trimmed.startsWith('~')) {
|
||||
return this._parseLocalPath(trimmed);
|
||||
}
|
||||
|
||||
// SSH URL: git@host:owner/repo.git
|
||||
const sshMatch = trimmed.match(/^git@([^:]+):([^/]+)\/([^/.]+?)(?:\.git)?$/);
|
||||
if (sshMatch) {
|
||||
const [, host, owner, repo] = sshMatch;
|
||||
return {
|
||||
type: 'url',
|
||||
cloneUrl: trimmed,
|
||||
subdir: null,
|
||||
localPath: null,
|
||||
cacheKey: `${host}/${owner}/${repo}`,
|
||||
displayName: `${owner}/${repo}`,
|
||||
isValid: true,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
// HTTPS URL: https://host/owner/repo[/tree/branch/subdir][.git]
|
||||
const httpsMatch = trimmed.match(/^https?:\/\/([^/]+)\/([^/]+)\/([^/.]+?)(?:\.git)?(\/.*)?$/);
|
||||
if (httpsMatch) {
|
||||
const [, host, owner, repo, remainder] = httpsMatch;
|
||||
const cloneUrl = `https://${host}/${owner}/${repo}`;
|
||||
let subdir = null;
|
||||
|
||||
if (remainder) {
|
||||
// Extract subdir from deep path patterns used by various Git hosts
|
||||
const deepPathPatterns = [
|
||||
/^\/(?:-\/)?tree\/[^/]+\/(.+)$/, // GitHub /tree/branch/path, GitLab /-/tree/branch/path
|
||||
/^\/(?:-\/)?blob\/[^/]+\/(.+)$/, // /blob/branch/path (treat same as tree)
|
||||
/^\/src\/[^/]+\/(.+)$/, // Gitea/Forgejo /src/branch/path
|
||||
];
|
||||
|
||||
for (const pattern of deepPathPatterns) {
|
||||
const match = remainder.match(pattern);
|
||||
if (match) {
|
||||
subdir = match[1].replace(/\/$/, ''); // strip trailing slash
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'url',
|
||||
cloneUrl,
|
||||
subdir,
|
||||
localPath: null,
|
||||
cacheKey: `${host}/${owner}/${repo}`,
|
||||
displayName: `${owner}/${repo}`,
|
||||
isValid: true,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: null,
|
||||
cloneUrl: null,
|
||||
subdir: null,
|
||||
localPath: null,
|
||||
cacheKey: null,
|
||||
displayName: null,
|
||||
isValid: false,
|
||||
error: 'Not a valid Git URL or local path',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a local filesystem path.
|
||||
* @param {string} rawPath - Path string (may contain ~ for home)
|
||||
* @returns {Object} Parsed source descriptor
|
||||
*/
|
||||
_parseLocalPath(rawPath) {
|
||||
const expanded = rawPath.startsWith('~') ? path.join(os.homedir(), rawPath.slice(1)) : rawPath;
|
||||
const resolved = path.resolve(expanded);
|
||||
|
||||
if (!fs.pathExistsSync(resolved)) {
|
||||
return {
|
||||
type: 'local',
|
||||
cloneUrl: null,
|
||||
subdir: null,
|
||||
localPath: resolved,
|
||||
cacheKey: null,
|
||||
displayName: path.basename(resolved),
|
||||
isValid: false,
|
||||
error: `Path does not exist: ${resolved}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'local',
|
||||
cloneUrl: null,
|
||||
subdir: null,
|
||||
localPath: resolved,
|
||||
cacheKey: null,
|
||||
displayName: path.basename(resolved),
|
||||
isValid: true,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use parseSource() instead. Kept for backward compatibility.
|
||||
* Parse and validate a GitHub repository URL.
|
||||
* Supports HTTPS and SSH formats.
|
||||
* @param {string} url - GitHub URL to validate
|
||||
* @returns {Object} { owner, repo, isValid, error }
|
||||
*/
|
||||
|
|
@ -26,16 +165,15 @@ class CustomModuleManager {
|
|||
if (!url || typeof url !== 'string') {
|
||||
return { owner: null, repo: null, isValid: false, error: 'URL is required' };
|
||||
}
|
||||
|
||||
const trimmed = url.trim();
|
||||
|
||||
// HTTPS format: https://github.com/owner/repo[.git]
|
||||
// HTTPS format: https://github.com/owner/repo[.git] (strict, no trailing path)
|
||||
const httpsMatch = trimmed.match(/^https?:\/\/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?$/);
|
||||
if (httpsMatch) {
|
||||
return { owner: httpsMatch[1], repo: httpsMatch[2], isValid: true, error: null };
|
||||
}
|
||||
|
||||
// SSH format: git@github.com:owner/repo.git
|
||||
// SSH format: git@github.com:owner/repo[.git]
|
||||
const sshMatch = trimmed.match(/^git@github\.com:([^/]+)\/([^/.]+?)(?:\.git)?$/);
|
||||
if (sshMatch) {
|
||||
return { owner: sshMatch[1], repo: sshMatch[2], isValid: true, error: null };
|
||||
|
|
@ -44,46 +182,75 @@ class CustomModuleManager {
|
|||
return { owner: null, repo: null, isValid: false, error: 'Not a valid GitHub URL (expected https://github.com/owner/repo)' };
|
||||
}
|
||||
|
||||
// ─── Discovery ────────────────────────────────────────────────────────────
|
||||
// ─── Marketplace JSON ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Fetch .claude-plugin/marketplace.json from a GitHub repository.
|
||||
* @param {string} repoUrl - GitHub repository URL
|
||||
* @returns {Object} Parsed marketplace.json content
|
||||
* Read .claude-plugin/marketplace.json from a local directory.
|
||||
* @param {string} dirPath - Directory to read from
|
||||
* @returns {Object|null} Parsed marketplace.json or null if not found
|
||||
*/
|
||||
async fetchMarketplaceJson(repoUrl) {
|
||||
const { owner, repo, isValid, error } = this.validateGitHubUrl(repoUrl);
|
||||
if (!isValid) throw new Error(error);
|
||||
|
||||
const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/HEAD/.claude-plugin/marketplace.json`;
|
||||
|
||||
async readMarketplaceJsonFromDisk(dirPath) {
|
||||
const marketplacePath = path.join(dirPath, '.claude-plugin', 'marketplace.json');
|
||||
if (!(await fs.pathExists(marketplacePath))) return null;
|
||||
try {
|
||||
return await this._client.fetchJson(rawUrl);
|
||||
} catch (error_) {
|
||||
if (error_.message.includes('404')) {
|
||||
throw new Error(`No .claude-plugin/marketplace.json found in ${owner}/${repo}. This repository may not be a BMad module.`);
|
||||
}
|
||||
if (error_.message.includes('403')) {
|
||||
throw new Error(`Repository ${owner}/${repo} is not accessible. Make sure it is public.`);
|
||||
}
|
||||
throw new Error(`Failed to fetch marketplace.json from ${owner}/${repo}: ${error_.message}`);
|
||||
return JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Discovery ────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Discover modules from a GitHub repository's marketplace.json.
|
||||
* @param {string} repoUrl - GitHub repository URL
|
||||
* Discover modules from pre-read marketplace.json data.
|
||||
* @param {Object} marketplaceData - Parsed marketplace.json content
|
||||
* @param {string|null} sourceUrl - Source URL for tracking (null for local paths)
|
||||
* @returns {Array<Object>} Normalized plugin list
|
||||
*/
|
||||
async discoverModules(repoUrl) {
|
||||
const data = await this.fetchMarketplaceJson(repoUrl);
|
||||
const plugins = data?.plugins;
|
||||
async discoverModules(marketplaceData, sourceUrl) {
|
||||
const plugins = marketplaceData?.plugins;
|
||||
|
||||
if (!Array.isArray(plugins) || plugins.length === 0) {
|
||||
throw new Error('marketplace.json contains no plugins');
|
||||
}
|
||||
|
||||
return plugins.map((plugin) => this._normalizeCustomModule(plugin, repoUrl, data));
|
||||
return plugins.map((plugin) => this._normalizeCustomModule(plugin, sourceUrl, marketplaceData));
|
||||
}
|
||||
|
||||
// ─── Source Resolution ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* High-level coordinator: parse input, clone if URL, determine discovery vs direct mode.
|
||||
* @param {string} input - URL or local path
|
||||
* @param {Object} [options] - Options passed to cloneRepo
|
||||
* @returns {Object} { parsed, rootDir, repoPath, sourceUrl, marketplace, mode: 'discovery'|'direct' }
|
||||
*/
|
||||
async resolveSource(input, options = {}) {
|
||||
const parsed = this.parseSource(input);
|
||||
if (!parsed.isValid) throw new Error(parsed.error);
|
||||
|
||||
let rootDir;
|
||||
let repoPath;
|
||||
let sourceUrl;
|
||||
|
||||
if (parsed.type === 'local') {
|
||||
rootDir = parsed.localPath;
|
||||
repoPath = null;
|
||||
sourceUrl = null;
|
||||
} else {
|
||||
repoPath = await this.cloneRepo(input, options);
|
||||
sourceUrl = parsed.cloneUrl;
|
||||
rootDir = parsed.subdir ? path.join(repoPath, parsed.subdir) : repoPath;
|
||||
|
||||
if (parsed.subdir && !(await fs.pathExists(rootDir))) {
|
||||
throw new Error(`Subdirectory '${parsed.subdir}' not found in cloned repository`);
|
||||
}
|
||||
}
|
||||
|
||||
const marketplace = await this.readMarketplaceJsonFromDisk(rootDir);
|
||||
const mode = marketplace ? 'discovery' : 'direct';
|
||||
|
||||
return { parsed, rootDir, repoPath, sourceUrl, marketplace, mode };
|
||||
}
|
||||
|
||||
// ─── Clone ────────────────────────────────────────────────────────────────
|
||||
|
|
@ -98,20 +265,24 @@ class CustomModuleManager {
|
|||
|
||||
/**
|
||||
* Clone a custom module repository to cache.
|
||||
* @param {string} repoUrl - GitHub repository URL
|
||||
* Supports any Git host (GitHub, GitLab, Bitbucket, self-hosted, etc.).
|
||||
* @param {string} sourceInput - Git URL (HTTPS or SSH)
|
||||
* @param {Object} [options] - Clone options
|
||||
* @param {boolean} [options.silent] - Suppress spinner output
|
||||
* @param {boolean} [options.skipInstall] - Skip npm install (for browsing before user confirms)
|
||||
* @returns {string} Path to the cloned repository
|
||||
*/
|
||||
async cloneRepo(repoUrl, options = {}) {
|
||||
const { owner, repo, isValid, error } = this.validateGitHubUrl(repoUrl);
|
||||
if (!isValid) throw new Error(error);
|
||||
async cloneRepo(sourceInput, options = {}) {
|
||||
const parsed = this.parseSource(sourceInput);
|
||||
if (!parsed.isValid) throw new Error(parsed.error);
|
||||
if (parsed.type === 'local') throw new Error('cloneRepo does not accept local paths');
|
||||
|
||||
const cacheDir = this.getCacheDir();
|
||||
const repoCacheDir = path.join(cacheDir, owner, repo);
|
||||
const repoCacheDir = path.join(cacheDir, ...parsed.cacheKey.split('/'));
|
||||
const silent = options.silent || false;
|
||||
const displayName = parsed.displayName;
|
||||
|
||||
await fs.ensureDir(path.join(cacheDir, owner));
|
||||
await fs.ensureDir(path.dirname(repoCacheDir));
|
||||
|
||||
const createSpinner = async () => {
|
||||
if (silent) {
|
||||
|
|
@ -123,7 +294,7 @@ class CustomModuleManager {
|
|||
if (await fs.pathExists(repoCacheDir)) {
|
||||
// Update existing clone
|
||||
const fetchSpinner = await createSpinner();
|
||||
fetchSpinner.start(`Updating ${owner}/${repo}...`);
|
||||
fetchSpinner.start(`Updating ${displayName}...`);
|
||||
try {
|
||||
execSync('git fetch origin --depth 1', {
|
||||
cwd: repoCacheDir,
|
||||
|
|
@ -134,42 +305,51 @@ class CustomModuleManager {
|
|||
cwd: repoCacheDir,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
fetchSpinner.stop(`Updated ${owner}/${repo}`);
|
||||
fetchSpinner.stop(`Updated ${displayName}`);
|
||||
} catch {
|
||||
fetchSpinner.error(`Update failed, re-downloading ${owner}/${repo}`);
|
||||
fetchSpinner.error(`Update failed, re-downloading ${displayName}`);
|
||||
await fs.remove(repoCacheDir);
|
||||
}
|
||||
}
|
||||
|
||||
if (!(await fs.pathExists(repoCacheDir))) {
|
||||
const fetchSpinner = await createSpinner();
|
||||
fetchSpinner.start(`Cloning ${owner}/${repo}...`);
|
||||
fetchSpinner.start(`Cloning ${displayName}...`);
|
||||
try {
|
||||
execSync(`git clone --depth 1 "${repoUrl}" "${repoCacheDir}"`, {
|
||||
execSync(`git clone --depth 1 "${parsed.cloneUrl}" "${repoCacheDir}"`, {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
||||
});
|
||||
fetchSpinner.stop(`Cloned ${owner}/${repo}`);
|
||||
fetchSpinner.stop(`Cloned ${displayName}`);
|
||||
} catch (error_) {
|
||||
fetchSpinner.error(`Failed to clone ${owner}/${repo}`);
|
||||
throw new Error(`Failed to clone ${repoUrl}: ${error_.message}`);
|
||||
fetchSpinner.error(`Failed to clone ${displayName}`);
|
||||
throw new Error(`Failed to clone ${parsed.cloneUrl}: ${error_.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Install dependencies if package.json exists
|
||||
// Write source metadata for later URL reconstruction
|
||||
const metadataPath = path.join(repoCacheDir, '.bmad-source.json');
|
||||
await fs.writeJson(metadataPath, {
|
||||
cloneUrl: parsed.cloneUrl,
|
||||
cacheKey: parsed.cacheKey,
|
||||
displayName: parsed.displayName,
|
||||
clonedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Install dependencies if package.json exists (skip during browsing/analysis)
|
||||
const packageJsonPath = path.join(repoCacheDir, 'package.json');
|
||||
if (await fs.pathExists(packageJsonPath)) {
|
||||
if (!options.skipInstall && (await fs.pathExists(packageJsonPath))) {
|
||||
const installSpinner = await createSpinner();
|
||||
installSpinner.start(`Installing dependencies for ${owner}/${repo}...`);
|
||||
installSpinner.start(`Installing dependencies for ${displayName}...`);
|
||||
try {
|
||||
execSync('npm install --omit=dev --no-audit --no-fund --no-progress --legacy-peer-deps', {
|
||||
cwd: repoCacheDir,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: 120_000,
|
||||
});
|
||||
installSpinner.stop(`Installed dependencies for ${owner}/${repo}`);
|
||||
installSpinner.stop(`Installed dependencies for ${displayName}`);
|
||||
} catch (error_) {
|
||||
installSpinner.error(`Failed to install dependencies for ${owner}/${repo}`);
|
||||
installSpinner.error(`Failed to install dependencies for ${displayName}`);
|
||||
if (!silent) await prompts.log.warn(` ${error_.message}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -177,23 +357,65 @@ class CustomModuleManager {
|
|||
return repoCacheDir;
|
||||
}
|
||||
|
||||
// ─── Plugin Resolution ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Resolve a plugin to determine installation strategy and module registration files.
|
||||
* Results are cached in _resolutionCache keyed by module code.
|
||||
* @param {string} repoPath - Absolute path to the cloned repository or local directory
|
||||
* @param {Object} plugin - Raw plugin object from marketplace.json
|
||||
* @param {string} [sourceUrl] - Original URL for manifest tracking (null for local)
|
||||
* @param {string} [localPath] - Local source path for manifest tracking (null for URLs)
|
||||
* @returns {Promise<Array<Object>>} Array of ResolvedModule objects
|
||||
*/
|
||||
async resolvePlugin(repoPath, plugin, sourceUrl, localPath) {
|
||||
const { PluginResolver } = require('./plugin-resolver');
|
||||
const resolver = new PluginResolver();
|
||||
const resolved = await resolver.resolve(repoPath, plugin);
|
||||
|
||||
// Stamp source info onto each resolved module for manifest tracking
|
||||
for (const mod of resolved) {
|
||||
if (sourceUrl) mod.repoUrl = sourceUrl;
|
||||
if (localPath) mod.localPath = localPath;
|
||||
CustomModuleManager._resolutionCache.set(mod.code, mod);
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a cached resolution result by module code.
|
||||
* @param {string} moduleCode - Module code to look up
|
||||
* @returns {Object|null} ResolvedModule or null if not cached
|
||||
*/
|
||||
getResolution(moduleCode) {
|
||||
return CustomModuleManager._resolutionCache.get(moduleCode) || null;
|
||||
}
|
||||
|
||||
// ─── Source Finding ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Find the module source path within a cloned custom repo.
|
||||
* @param {string} repoUrl - GitHub repository URL (for cache location)
|
||||
* Find the module source path within a cached or local source directory.
|
||||
* @param {string} sourceInput - Git URL or local path (used to locate cached clone)
|
||||
* @param {string} [pluginSource] - Plugin source path from marketplace.json
|
||||
* @returns {string|null} Path to directory containing module.yaml
|
||||
*/
|
||||
async findModuleSource(repoUrl, pluginSource) {
|
||||
const { owner, repo } = this.validateGitHubUrl(repoUrl);
|
||||
const repoCacheDir = path.join(this.getCacheDir(), owner, repo);
|
||||
async findModuleSource(sourceInput, pluginSource) {
|
||||
const parsed = this.parseSource(sourceInput);
|
||||
if (!parsed.isValid) return null;
|
||||
|
||||
if (!(await fs.pathExists(repoCacheDir))) return null;
|
||||
let baseDir;
|
||||
if (parsed.type === 'local') {
|
||||
baseDir = parsed.localPath;
|
||||
} else {
|
||||
baseDir = path.join(this.getCacheDir(), ...parsed.cacheKey.split('/'));
|
||||
}
|
||||
|
||||
if (!(await fs.pathExists(baseDir))) return null;
|
||||
|
||||
// Try plugin source path first (e.g., "./src/pro-skills")
|
||||
if (pluginSource) {
|
||||
const sourcePath = path.join(repoCacheDir, pluginSource);
|
||||
const sourcePath = path.join(baseDir, pluginSource);
|
||||
const moduleYaml = path.join(sourcePath, 'module.yaml');
|
||||
if (await fs.pathExists(moduleYaml)) {
|
||||
return sourcePath;
|
||||
|
|
@ -202,11 +424,11 @@ class CustomModuleManager {
|
|||
|
||||
// Fallback: search skills/ and src/ directories
|
||||
for (const dir of ['skills', 'src']) {
|
||||
const rootCandidate = path.join(repoCacheDir, dir, 'module.yaml');
|
||||
const rootCandidate = path.join(baseDir, dir, 'module.yaml');
|
||||
if (await fs.pathExists(rootCandidate)) {
|
||||
return path.dirname(rootCandidate);
|
||||
}
|
||||
const dirPath = path.join(repoCacheDir, dir);
|
||||
const dirPath = path.join(baseDir, dir);
|
||||
if (await fs.pathExists(dirPath)) {
|
||||
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
|
|
@ -220,10 +442,10 @@ class CustomModuleManager {
|
|||
}
|
||||
}
|
||||
|
||||
// Check repo root
|
||||
const rootCandidate = path.join(repoCacheDir, 'module.yaml');
|
||||
// Check base directory root
|
||||
const rootCandidate = path.join(baseDir, 'module.yaml');
|
||||
if (await fs.pathExists(rootCandidate)) {
|
||||
return repoCacheDir;
|
||||
return baseDir;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
@ -231,51 +453,163 @@ class CustomModuleManager {
|
|||
|
||||
/**
|
||||
* Find module source by module code, searching the custom cache.
|
||||
* Handles both new 3-level cache structure (host/owner/repo) and
|
||||
* legacy 2-level structure (owner/repo).
|
||||
* @param {string} moduleCode - Module code to search for
|
||||
* @param {Object} [options] - Options
|
||||
* @returns {string|null} Path to the module source or null
|
||||
*/
|
||||
async findModuleSourceByCode(moduleCode, options = {}) {
|
||||
// Check resolution cache first (populated by resolvePlugin)
|
||||
const resolved = CustomModuleManager._resolutionCache.get(moduleCode);
|
||||
if (resolved) {
|
||||
// For strategies 1-2: the common parent or setup skill's parent has the module files
|
||||
if (resolved.moduleYamlPath) {
|
||||
return path.dirname(resolved.moduleYamlPath);
|
||||
}
|
||||
// For strategy 5 (synthesized): return the first skill's parent as a reference path
|
||||
if (resolved.skillPaths && resolved.skillPaths.length > 0) {
|
||||
return path.dirname(resolved.skillPaths[0]);
|
||||
}
|
||||
}
|
||||
|
||||
const cacheDir = this.getCacheDir();
|
||||
if (!(await fs.pathExists(cacheDir))) return null;
|
||||
|
||||
// Search through all custom repo caches
|
||||
// Search through all cached repo roots
|
||||
try {
|
||||
const owners = await fs.readdir(cacheDir, { withFileTypes: true });
|
||||
for (const ownerEntry of owners) {
|
||||
if (!ownerEntry.isDirectory()) continue;
|
||||
const ownerPath = path.join(cacheDir, ownerEntry.name);
|
||||
const repos = await fs.readdir(ownerPath, { withFileTypes: true });
|
||||
for (const repoEntry of repos) {
|
||||
if (!repoEntry.isDirectory()) continue;
|
||||
const repoPath = path.join(ownerPath, repoEntry.name);
|
||||
const { PluginResolver } = require('./plugin-resolver');
|
||||
const resolver = new PluginResolver();
|
||||
const repoRoots = await this._findCacheRepoRoots(cacheDir);
|
||||
|
||||
// Check marketplace.json for matching module code
|
||||
const marketplacePath = path.join(repoPath, '.claude-plugin', 'marketplace.json');
|
||||
if (await fs.pathExists(marketplacePath)) {
|
||||
try {
|
||||
const data = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
|
||||
for (const plugin of data.plugins || []) {
|
||||
if (plugin.name === moduleCode) {
|
||||
// Found the module - find its source
|
||||
const sourcePath = plugin.source ? path.join(repoPath, plugin.source) : repoPath;
|
||||
const moduleYaml = path.join(sourcePath, 'module.yaml');
|
||||
if (await fs.pathExists(moduleYaml)) {
|
||||
return sourcePath;
|
||||
for (const { repoPath, metadata } of repoRoots) {
|
||||
// Check marketplace.json for matching module code
|
||||
const marketplacePath = path.join(repoPath, '.claude-plugin', 'marketplace.json');
|
||||
if (!(await fs.pathExists(marketplacePath))) continue;
|
||||
|
||||
try {
|
||||
const data = JSON.parse(await fs.readFile(marketplacePath, 'utf8'));
|
||||
for (const plugin of data.plugins || []) {
|
||||
// Direct name match (legacy behavior)
|
||||
if (plugin.name === moduleCode) {
|
||||
const sourcePath = plugin.source ? path.join(repoPath, plugin.source) : repoPath;
|
||||
const moduleYaml = path.join(sourcePath, 'module.yaml');
|
||||
if (await fs.pathExists(moduleYaml)) {
|
||||
return sourcePath;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve plugin to check if any module.yaml code matches
|
||||
if (plugin.skills && plugin.skills.length > 0) {
|
||||
try {
|
||||
const resolvedMods = await resolver.resolve(repoPath, plugin);
|
||||
for (const mod of resolvedMods) {
|
||||
if (mod.code === moduleCode) {
|
||||
// Use metadata for URL reconstruction instead of deriving from path
|
||||
mod.repoUrl = metadata?.cloneUrl || null;
|
||||
CustomModuleManager._resolutionCache.set(mod.code, mod);
|
||||
if (mod.moduleYamlPath) {
|
||||
return path.dirname(mod.moduleYamlPath);
|
||||
}
|
||||
if (mod.skillPaths && mod.skillPaths.length > 0) {
|
||||
return path.dirname(mod.skillPaths[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip unresolvable plugins
|
||||
}
|
||||
} catch {
|
||||
// Skip malformed marketplace.json
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip malformed marketplace.json
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Cache doesn't exist or is inaccessible
|
||||
}
|
||||
|
||||
return null;
|
||||
// Fallback: check manifest for localPath (local-source modules not in cache)
|
||||
return this._findLocalSourceFromManifest(moduleCode, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the installation manifest for a localPath entry for this module.
|
||||
* Used as fallback when the module was installed from a local source (no cache entry).
|
||||
* Returns the path only if it still exists on disk; never removes installed files.
|
||||
* @param {string} moduleCode - Module code to search for
|
||||
* @param {Object} [options] - Options (must include bmadDir or will search common locations)
|
||||
* @returns {string|null} Path to the local module source or null
|
||||
*/
|
||||
async _findLocalSourceFromManifest(moduleCode, options = {}) {
|
||||
try {
|
||||
const { Manifest } = require('../core/manifest');
|
||||
const manifestObj = new Manifest();
|
||||
|
||||
// Try to find bmadDir from options or common locations
|
||||
const bmadDir = options.bmadDir;
|
||||
if (!bmadDir) return null;
|
||||
|
||||
const manifestData = await manifestObj.read(bmadDir);
|
||||
if (!manifestData?.modulesDetailed) return null;
|
||||
|
||||
const moduleEntry = manifestData.modulesDetailed.find((m) => m.name === moduleCode);
|
||||
if (!moduleEntry?.localPath) return null;
|
||||
|
||||
// Only return the path if it still exists (source not removed)
|
||||
if (await fs.pathExists(moduleEntry.localPath)) {
|
||||
return moduleEntry.localPath;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively find repo root directories within the cache.
|
||||
* A repo root is identified by containing .bmad-source.json (new) or .claude-plugin/ (legacy).
|
||||
* Handles both 3-level (host/owner/repo) and legacy 2-level (owner/repo) cache layouts.
|
||||
* @param {string} dir - Directory to search
|
||||
* @param {number} [depth=0] - Current recursion depth
|
||||
* @param {number} [maxDepth=4] - Maximum recursion depth
|
||||
* @returns {Promise<Array<{repoPath: string, metadata: Object|null}>>}
|
||||
*/
|
||||
async _findCacheRepoRoots(dir, depth = 0, maxDepth = 4) {
|
||||
const results = [];
|
||||
|
||||
// Check if this directory is a repo root
|
||||
const metadataPath = path.join(dir, '.bmad-source.json');
|
||||
const claudePluginDir = path.join(dir, '.claude-plugin');
|
||||
|
||||
if (await fs.pathExists(metadataPath)) {
|
||||
try {
|
||||
const metadata = JSON.parse(await fs.readFile(metadataPath, 'utf8'));
|
||||
results.push({ repoPath: dir, metadata });
|
||||
} catch {
|
||||
results.push({ repoPath: dir, metadata: null });
|
||||
}
|
||||
return results; // Don't recurse into repo contents
|
||||
}
|
||||
if (await fs.pathExists(claudePluginDir)) {
|
||||
results.push({ repoPath: dir, metadata: null });
|
||||
return results;
|
||||
}
|
||||
|
||||
// Recurse into subdirectories
|
||||
if (depth >= maxDepth) return results;
|
||||
try {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
|
||||
const subResults = await this._findCacheRepoRoots(path.join(dir, entry.name), depth + 1, maxDepth);
|
||||
results.push(...subResults);
|
||||
}
|
||||
} catch {
|
||||
// Directory not readable
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// ─── Normalization ────────────────────────────────────────────────────────
|
||||
|
|
@ -283,11 +617,11 @@ class CustomModuleManager {
|
|||
/**
|
||||
* Normalize a plugin from marketplace.json to a consistent shape.
|
||||
* @param {Object} plugin - Plugin object from marketplace.json
|
||||
* @param {string} repoUrl - Source repository URL
|
||||
* @param {string|null} sourceUrl - Source URL (null for local paths)
|
||||
* @param {Object} data - Full marketplace.json data
|
||||
* @returns {Object} Normalized module info
|
||||
*/
|
||||
_normalizeCustomModule(plugin, repoUrl, data) {
|
||||
_normalizeCustomModule(plugin, sourceUrl, data) {
|
||||
return {
|
||||
code: plugin.name,
|
||||
name: plugin.name,
|
||||
|
|
@ -295,8 +629,10 @@ class CustomModuleManager {
|
|||
description: plugin.description || '',
|
||||
version: plugin.version || null,
|
||||
author: plugin.author || data.owner || '',
|
||||
url: repoUrl,
|
||||
url: sourceUrl || null,
|
||||
source: plugin.source || null,
|
||||
skills: plugin.skills || [],
|
||||
rawPlugin: plugin,
|
||||
type: 'custom',
|
||||
trustTier: 'unverified',
|
||||
builtIn: false,
|
||||
|
|
|
|||
|
|
@ -135,6 +135,22 @@ class OfficialModules {
|
|||
const moduleConfigPath = path.join(modulePath, 'module.yaml');
|
||||
|
||||
if (!(await fs.pathExists(moduleConfigPath))) {
|
||||
// Check resolution cache for strategy 5 modules (no module.yaml on disk)
|
||||
const { CustomModuleManager } = require('./custom-module-manager');
|
||||
const customMgr = new CustomModuleManager();
|
||||
const resolved = customMgr.getResolution(defaultName);
|
||||
if (resolved && resolved.synthesizedModuleYaml) {
|
||||
return {
|
||||
id: resolved.code,
|
||||
path: modulePath,
|
||||
name: resolved.name,
|
||||
description: resolved.description,
|
||||
version: resolved.version || '1.0.0',
|
||||
source: sourceDescription,
|
||||
dependencies: [],
|
||||
defaultSelected: false,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -232,6 +248,14 @@ class OfficialModules {
|
|||
* @param {Object} options.logger - Logger instance for output
|
||||
*/
|
||||
async install(moduleName, bmadDir, fileTrackingCallback = null, options = {}) {
|
||||
// Check if this module has a plugin resolution (custom marketplace install)
|
||||
const { CustomModuleManager } = require('./custom-module-manager');
|
||||
const customMgr = new CustomModuleManager();
|
||||
const resolved = customMgr.getResolution(moduleName);
|
||||
if (resolved) {
|
||||
return this.installFromResolution(resolved, bmadDir, fileTrackingCallback, options);
|
||||
}
|
||||
|
||||
const sourcePath = await this.findModuleSource(moduleName, { silent: options.silent });
|
||||
const targetPath = path.join(bmadDir, moduleName);
|
||||
|
||||
|
|
@ -265,6 +289,62 @@ class OfficialModules {
|
|||
return { success: true, module: moduleName, path: targetPath, versionInfo };
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a module from a PluginResolver resolution result.
|
||||
* Copies specific skill directories and places module-help.csv at the target root.
|
||||
* @param {Object} resolved - ResolvedModule from PluginResolver
|
||||
* @param {string} bmadDir - Target bmad directory
|
||||
* @param {Function} fileTrackingCallback - Optional callback to track installed files
|
||||
* @param {Object} options - Installation options
|
||||
*/
|
||||
async installFromResolution(resolved, bmadDir, fileTrackingCallback = null, options = {}) {
|
||||
const targetPath = path.join(bmadDir, resolved.code);
|
||||
|
||||
if (await fs.pathExists(targetPath)) {
|
||||
await fs.remove(targetPath);
|
||||
}
|
||||
|
||||
await fs.ensureDir(targetPath);
|
||||
|
||||
// Copy each skill directory, flattened by leaf name
|
||||
for (const skillPath of resolved.skillPaths) {
|
||||
const skillDirName = path.basename(skillPath);
|
||||
const skillTarget = path.join(targetPath, skillDirName);
|
||||
await this.copyModuleWithFiltering(skillPath, skillTarget, fileTrackingCallback, options.moduleConfig);
|
||||
}
|
||||
|
||||
// Place module-help.csv at the module root
|
||||
if (resolved.moduleHelpCsvPath) {
|
||||
// Strategies 1-4: copy the existing file
|
||||
const helpTarget = path.join(targetPath, 'module-help.csv');
|
||||
await fs.copy(resolved.moduleHelpCsvPath, helpTarget, { overwrite: true });
|
||||
if (fileTrackingCallback) fileTrackingCallback(helpTarget);
|
||||
} else if (resolved.synthesizedHelpCsv) {
|
||||
// Strategy 5: write synthesized content
|
||||
const helpTarget = path.join(targetPath, 'module-help.csv');
|
||||
await fs.writeFile(helpTarget, resolved.synthesizedHelpCsv, 'utf8');
|
||||
if (fileTrackingCallback) fileTrackingCallback(helpTarget);
|
||||
}
|
||||
|
||||
// Create directories declared in module.yaml (strategies 1-4 may have these)
|
||||
if (!options.skipModuleInstaller) {
|
||||
await this.createModuleDirectories(resolved.code, bmadDir, options);
|
||||
}
|
||||
|
||||
// Update manifest
|
||||
const { Manifest } = require('../core/manifest');
|
||||
const manifestObj = new Manifest();
|
||||
|
||||
await manifestObj.addModule(bmadDir, resolved.code, {
|
||||
version: resolved.version || null,
|
||||
source: 'custom',
|
||||
npmPackage: null,
|
||||
repoUrl: resolved.repoUrl || null,
|
||||
});
|
||||
|
||||
return { success: true, module: resolved.code, path: targetPath, versionInfo: { version: resolved.version || '' } };
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing module
|
||||
* @param {string} moduleName - Name of the module to update
|
||||
|
|
|
|||
|
|
@ -0,0 +1,398 @@
|
|||
const fs = require('fs-extra');
|
||||
const path = require('node:path');
|
||||
const yaml = require('yaml');
|
||||
|
||||
/**
|
||||
* Resolves how to install a plugin from marketplace.json by analyzing
|
||||
* where module.yaml and module-help.csv live relative to the listed skills.
|
||||
*
|
||||
* Five strategies, tried in order:
|
||||
* 1. Root module files at the common parent of all skills
|
||||
* 2. A -setup skill with assets/module.yaml + assets/module-help.csv
|
||||
* 3. Single standalone skill with both files in its assets/
|
||||
* 4. Multiple standalone skills, each with both files in assets/
|
||||
* 5. Fallback: synthesize from marketplace.json + SKILL.md frontmatter
|
||||
*/
|
||||
class PluginResolver {
|
||||
/**
|
||||
* Resolve a plugin to one or more installable module definitions.
|
||||
* @param {string} repoPath - Absolute path to the cloned repository root
|
||||
* @param {Object} plugin - Plugin object from marketplace.json
|
||||
* @param {string} plugin.name - Plugin identifier
|
||||
* @param {string} [plugin.source] - Relative path from repo root
|
||||
* @param {string} [plugin.version] - Semantic version
|
||||
* @param {string} [plugin.description] - Plugin description
|
||||
* @param {string[]} [plugin.skills] - Relative paths to skill directories
|
||||
* @returns {Promise<ResolvedModule[]>} Array of resolved module definitions
|
||||
*/
|
||||
async resolve(repoPath, plugin) {
|
||||
const skillRelPaths = plugin.skills || [];
|
||||
|
||||
// No skills array: legacy behavior - caller should use existing findModuleSource
|
||||
if (skillRelPaths.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Resolve skill paths to absolute, constrain to repo root, filter non-existent
|
||||
const repoRoot = path.resolve(repoPath);
|
||||
const skillPaths = [];
|
||||
for (const rel of skillRelPaths) {
|
||||
const normalized = rel.replace(/^\.\//, '');
|
||||
const abs = path.resolve(repoPath, normalized);
|
||||
// Guard against path traversal (.. segments, absolute paths in marketplace.json)
|
||||
if (!abs.startsWith(repoRoot + path.sep) && abs !== repoRoot) {
|
||||
continue;
|
||||
}
|
||||
if (await fs.pathExists(abs)) {
|
||||
skillPaths.push(abs);
|
||||
}
|
||||
}
|
||||
|
||||
if (skillPaths.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Try each strategy in order
|
||||
const result =
|
||||
(await this._tryRootModuleFiles(repoPath, plugin, skillPaths)) ||
|
||||
(await this._trySetupSkill(repoPath, plugin, skillPaths)) ||
|
||||
(await this._trySingleStandalone(repoPath, plugin, skillPaths)) ||
|
||||
(await this._tryMultipleStandalone(repoPath, plugin, skillPaths)) ||
|
||||
(await this._synthesizeFallback(repoPath, plugin, skillPaths));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ─── Strategy 1: Root Module Files ──────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Check if module.yaml + module-help.csv exist at the common parent of all skills.
|
||||
*/
|
||||
async _tryRootModuleFiles(repoPath, plugin, skillPaths) {
|
||||
const commonParent = this._computeCommonParent(skillPaths);
|
||||
const moduleYamlPath = path.join(commonParent, 'module.yaml');
|
||||
const moduleHelpPath = path.join(commonParent, 'module-help.csv');
|
||||
|
||||
if (!(await fs.pathExists(moduleYamlPath)) || !(await fs.pathExists(moduleHelpPath))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const moduleData = await this._readModuleYaml(moduleYamlPath);
|
||||
if (!moduleData) return null;
|
||||
|
||||
return [
|
||||
{
|
||||
code: moduleData.code || plugin.name,
|
||||
name: moduleData.name || plugin.name,
|
||||
version: plugin.version || moduleData.module_version || null,
|
||||
description: moduleData.description || plugin.description || '',
|
||||
strategy: 1,
|
||||
pluginName: plugin.name,
|
||||
moduleYamlPath,
|
||||
moduleHelpCsvPath: moduleHelpPath,
|
||||
skillPaths,
|
||||
synthesizedModuleYaml: null,
|
||||
synthesizedHelpCsv: null,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ─── Strategy 2: Setup Skill ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Search for a skill ending in -setup with assets/module.yaml + assets/module-help.csv.
|
||||
*/
|
||||
async _trySetupSkill(repoPath, plugin, skillPaths) {
|
||||
for (const skillPath of skillPaths) {
|
||||
const dirName = path.basename(skillPath);
|
||||
if (!dirName.endsWith('-setup')) continue;
|
||||
|
||||
const moduleYamlPath = path.join(skillPath, 'assets', 'module.yaml');
|
||||
const moduleHelpPath = path.join(skillPath, 'assets', 'module-help.csv');
|
||||
|
||||
if (!(await fs.pathExists(moduleYamlPath)) || !(await fs.pathExists(moduleHelpPath))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const moduleData = await this._readModuleYaml(moduleYamlPath);
|
||||
if (!moduleData) continue;
|
||||
|
||||
return [
|
||||
{
|
||||
code: moduleData.code || plugin.name,
|
||||
name: moduleData.name || plugin.name,
|
||||
version: plugin.version || moduleData.module_version || null,
|
||||
description: moduleData.description || plugin.description || '',
|
||||
strategy: 2,
|
||||
pluginName: plugin.name,
|
||||
moduleYamlPath,
|
||||
moduleHelpCsvPath: moduleHelpPath,
|
||||
skillPaths,
|
||||
synthesizedModuleYaml: null,
|
||||
synthesizedHelpCsv: null,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── Strategy 3: Single Standalone Skill ────────────────────────────────────
|
||||
|
||||
/**
|
||||
* One skill listed, with assets/module.yaml + assets/module-help.csv.
|
||||
*/
|
||||
async _trySingleStandalone(repoPath, plugin, skillPaths) {
|
||||
if (skillPaths.length !== 1) return null;
|
||||
|
||||
const skillPath = skillPaths[0];
|
||||
const moduleYamlPath = path.join(skillPath, 'assets', 'module.yaml');
|
||||
const moduleHelpPath = path.join(skillPath, 'assets', 'module-help.csv');
|
||||
|
||||
if (!(await fs.pathExists(moduleYamlPath)) || !(await fs.pathExists(moduleHelpPath))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const moduleData = await this._readModuleYaml(moduleYamlPath);
|
||||
if (!moduleData) return null;
|
||||
|
||||
return [
|
||||
{
|
||||
code: moduleData.code || plugin.name,
|
||||
name: moduleData.name || plugin.name,
|
||||
version: plugin.version || moduleData.module_version || null,
|
||||
description: moduleData.description || plugin.description || '',
|
||||
strategy: 3,
|
||||
pluginName: plugin.name,
|
||||
moduleYamlPath,
|
||||
moduleHelpCsvPath: moduleHelpPath,
|
||||
skillPaths,
|
||||
synthesizedModuleYaml: null,
|
||||
synthesizedHelpCsv: null,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ─── Strategy 4: Multiple Standalone Skills ─────────────────────────────────
|
||||
|
||||
/**
|
||||
* Multiple skills, each with assets/module.yaml + assets/module-help.csv.
|
||||
* Each becomes its own installable module.
|
||||
*/
|
||||
async _tryMultipleStandalone(repoPath, plugin, skillPaths) {
|
||||
if (skillPaths.length < 2) return null;
|
||||
|
||||
const resolved = [];
|
||||
|
||||
for (const skillPath of skillPaths) {
|
||||
const moduleYamlPath = path.join(skillPath, 'assets', 'module.yaml');
|
||||
const moduleHelpPath = path.join(skillPath, 'assets', 'module-help.csv');
|
||||
|
||||
if (!(await fs.pathExists(moduleYamlPath)) || !(await fs.pathExists(moduleHelpPath))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const moduleData = await this._readModuleYaml(moduleYamlPath);
|
||||
if (!moduleData) continue;
|
||||
|
||||
resolved.push({
|
||||
code: moduleData.code || path.basename(skillPath),
|
||||
name: moduleData.name || path.basename(skillPath),
|
||||
version: plugin.version || moduleData.module_version || null,
|
||||
description: moduleData.description || '',
|
||||
strategy: 4,
|
||||
pluginName: plugin.name,
|
||||
moduleYamlPath,
|
||||
moduleHelpCsvPath: moduleHelpPath,
|
||||
skillPaths: [skillPath],
|
||||
synthesizedModuleYaml: null,
|
||||
synthesizedHelpCsv: null,
|
||||
});
|
||||
}
|
||||
|
||||
// Only use strategy 4 if ALL skills have module files
|
||||
if (resolved.length === skillPaths.length) {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
// Partial match: fall through to strategy 5
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── Strategy 5: Fallback (Synthesized) ─────────────────────────────────────
|
||||
|
||||
/**
|
||||
* No module files found anywhere. Synthesize from marketplace.json metadata
|
||||
* and SKILL.md frontmatter.
|
||||
*/
|
||||
async _synthesizeFallback(repoPath, plugin, skillPaths) {
|
||||
const skillInfos = [];
|
||||
|
||||
for (const skillPath of skillPaths) {
|
||||
const frontmatter = await this._parseSkillFrontmatter(skillPath);
|
||||
skillInfos.push({
|
||||
dirName: path.basename(skillPath),
|
||||
name: frontmatter.name || path.basename(skillPath),
|
||||
description: frontmatter.description || '',
|
||||
});
|
||||
}
|
||||
|
||||
const moduleName = this._formatDisplayName(plugin.name);
|
||||
const code = plugin.name;
|
||||
|
||||
const synthesizedYaml = {
|
||||
code,
|
||||
name: moduleName,
|
||||
description: plugin.description || '',
|
||||
module_version: plugin.version || '1.0.0',
|
||||
default_selected: false,
|
||||
};
|
||||
|
||||
const synthesizedCsv = this._buildSynthesizedHelpCsv(moduleName, skillInfos);
|
||||
|
||||
return [
|
||||
{
|
||||
code,
|
||||
name: moduleName,
|
||||
version: plugin.version || null,
|
||||
description: plugin.description || '',
|
||||
strategy: 5,
|
||||
pluginName: plugin.name,
|
||||
moduleYamlPath: null,
|
||||
moduleHelpCsvPath: null,
|
||||
skillPaths,
|
||||
synthesizedModuleYaml: synthesizedYaml,
|
||||
synthesizedHelpCsv: synthesizedCsv,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Compute the deepest common ancestor directory of an array of absolute paths.
|
||||
* @param {string[]} absPaths - Absolute directory paths
|
||||
* @returns {string} Common parent directory
|
||||
*/
|
||||
_computeCommonParent(absPaths) {
|
||||
if (absPaths.length === 0) return '/';
|
||||
if (absPaths.length === 1) return path.dirname(absPaths[0]);
|
||||
|
||||
const segments = absPaths.map((p) => p.split(path.sep));
|
||||
const minLen = Math.min(...segments.map((s) => s.length));
|
||||
const common = [];
|
||||
|
||||
for (let i = 0; i < minLen; i++) {
|
||||
const segment = segments[0][i];
|
||||
if (segments.every((s) => s[i] === segment)) {
|
||||
common.push(segment);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return common.join(path.sep) || '/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and parse a module.yaml file.
|
||||
* @param {string} yamlPath - Absolute path to module.yaml
|
||||
* @returns {Object|null} Parsed content or null on failure
|
||||
*/
|
||||
async _readModuleYaml(yamlPath) {
|
||||
try {
|
||||
const content = await fs.readFile(yamlPath, 'utf8');
|
||||
return yaml.parse(content);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract name and description from a SKILL.md YAML frontmatter block.
|
||||
* @param {string} skillDirPath - Absolute path to the skill directory
|
||||
* @returns {Object} { name, description } or empty strings
|
||||
*/
|
||||
async _parseSkillFrontmatter(skillDirPath) {
|
||||
const skillMdPath = path.join(skillDirPath, 'SKILL.md');
|
||||
try {
|
||||
const content = await fs.readFile(skillMdPath, 'utf8');
|
||||
const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
||||
if (!match) return { name: '', description: '' };
|
||||
|
||||
const parsed = yaml.parse(match[1]);
|
||||
return {
|
||||
name: parsed.name || '',
|
||||
description: parsed.description || '',
|
||||
};
|
||||
} catch {
|
||||
return { name: '', description: '' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a synthesized module-help.csv from plugin metadata and skill frontmatter.
|
||||
* Uses the standard 13-column format.
|
||||
* @param {string} moduleName - Display name for the module column
|
||||
* @param {Array<{dirName: string, name: string, description: string}>} skillInfos
|
||||
* @returns {string} CSV content
|
||||
*/
|
||||
_buildSynthesizedHelpCsv(moduleName, skillInfos) {
|
||||
const header = 'module,skill,display-name,menu-code,description,action,args,phase,after,before,required,output-location,outputs';
|
||||
const rows = [header];
|
||||
|
||||
for (const info of skillInfos) {
|
||||
const displayName = this._formatDisplayName(info.name || info.dirName);
|
||||
const menuCode = this._generateMenuCode(info.name || info.dirName);
|
||||
const description = this._escapeCSVField(info.description);
|
||||
|
||||
rows.push(`${moduleName},${info.dirName},${displayName},${menuCode},${description},activate,,anytime,,,false,,`);
|
||||
}
|
||||
|
||||
return rows.join('\n') + '\n';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a kebab-case or snake_case name into a display name.
|
||||
* Strips common prefixes like "bmad-" or "bmad-agent-".
|
||||
* @param {string} name - Raw name
|
||||
* @returns {string} Formatted display name
|
||||
*/
|
||||
_formatDisplayName(name) {
|
||||
let cleaned = name.replace(/^bmad-agent-/, '').replace(/^bmad-/, '');
|
||||
return cleaned
|
||||
.split(/[-_]/)
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a short menu code from a skill name.
|
||||
* Takes first letter of each significant word, uppercased, max 3 chars.
|
||||
* @param {string} name - Skill name (kebab-case)
|
||||
* @returns {string} Menu code (e.g., "CC" for "code-coach")
|
||||
*/
|
||||
_generateMenuCode(name) {
|
||||
const cleaned = name.replace(/^bmad-agent-/, '').replace(/^bmad-/, '');
|
||||
const words = cleaned.split(/[-_]/).filter((w) => w.length > 0);
|
||||
return words
|
||||
.map((w) => w.charAt(0).toUpperCase())
|
||||
.join('')
|
||||
.slice(0, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape a value for CSV output (wrap in quotes if it contains commas, quotes, or newlines).
|
||||
* @param {string} value
|
||||
* @returns {string}
|
||||
*/
|
||||
_escapeCSVField(value) {
|
||||
if (!value) return '';
|
||||
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
|
||||
return `"${value.replaceAll('"', '""')}"`;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { PluginResolver };
|
||||
|
|
@ -158,6 +158,9 @@ class UI {
|
|||
.map((m) => m.trim())
|
||||
.filter(Boolean);
|
||||
await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`);
|
||||
} else if (options.customSource) {
|
||||
// Custom source without --modules: start with empty list (core added below)
|
||||
selectedModules = [];
|
||||
} else if (options.yes) {
|
||||
selectedModules = await this.getDefaultModules(installedModuleIds);
|
||||
await prompts.log.info(
|
||||
|
|
@ -167,6 +170,14 @@ class UI {
|
|||
selectedModules = await this.selectAllModules(installedModuleIds);
|
||||
}
|
||||
|
||||
// Resolve custom sources from --custom-source flag
|
||||
if (options.customSource) {
|
||||
const customCodes = await this._resolveCustomSourcesCli(options.customSource);
|
||||
for (const code of customCodes) {
|
||||
if (!selectedModules.includes(code)) selectedModules.push(code);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure core is in the modules list
|
||||
if (!selectedModules.includes('core')) {
|
||||
selectedModules.unshift('core');
|
||||
|
|
@ -202,6 +213,9 @@ class UI {
|
|||
.map((m) => m.trim())
|
||||
.filter(Boolean);
|
||||
await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`);
|
||||
} else if (options.customSource) {
|
||||
// Custom source without --modules: start with empty list (core added below)
|
||||
selectedModules = [];
|
||||
} else if (options.yes) {
|
||||
// Use default modules when --yes flag is set
|
||||
selectedModules = await this.getDefaultModules(installedModuleIds);
|
||||
|
|
@ -210,6 +224,14 @@ class UI {
|
|||
selectedModules = await this.selectAllModules(installedModuleIds);
|
||||
}
|
||||
|
||||
// Resolve custom sources from --custom-source flag
|
||||
if (options.customSource) {
|
||||
const customCodes = await this._resolveCustomSourcesCli(options.customSource);
|
||||
for (const code of customCodes) {
|
||||
if (!selectedModules.includes(code)) selectedModules.push(code);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure core is in the modules list
|
||||
if (!selectedModules.includes('core')) {
|
||||
selectedModules.unshift('core');
|
||||
|
|
@ -818,13 +840,13 @@ class UI {
|
|||
}
|
||||
|
||||
/**
|
||||
* Prompt user to install modules from custom GitHub URLs.
|
||||
* Prompt user to install modules from custom sources (Git URLs or local paths).
|
||||
* @param {Set} installedModuleIds - Currently installed module IDs
|
||||
* @returns {Array} Selected custom module code strings
|
||||
*/
|
||||
async _addCustomUrlModules(installedModuleIds = new Set()) {
|
||||
const addCustom = await prompts.confirm({
|
||||
message: 'Would you like to install from a custom GitHub URL?',
|
||||
message: 'Would you like to install from a custom source (Git URL or local path)?',
|
||||
default: false,
|
||||
});
|
||||
if (!addCustom) return [];
|
||||
|
|
@ -835,61 +857,158 @@ class UI {
|
|||
|
||||
let addMore = true;
|
||||
while (addMore) {
|
||||
const url = await prompts.text({
|
||||
message: 'GitHub repository URL:',
|
||||
placeholder: 'https://github.com/owner/repo',
|
||||
const sourceInput = await prompts.text({
|
||||
message: 'Git URL or local path:',
|
||||
placeholder: 'https://github.com/owner/repo or /path/to/module',
|
||||
validate: (input) => {
|
||||
if (!input || input.trim() === '') return 'URL is required';
|
||||
const result = customMgr.validateGitHubUrl(input.trim());
|
||||
if (!input || input.trim() === '') return 'Source is required';
|
||||
const result = customMgr.parseSource(input.trim());
|
||||
return result.isValid ? undefined : result.error;
|
||||
},
|
||||
});
|
||||
|
||||
const s = await prompts.spinner();
|
||||
s.start('Fetching module info...');
|
||||
s.start('Resolving source...');
|
||||
|
||||
let sourceResult;
|
||||
try {
|
||||
const plugins = await customMgr.discoverModules(url.trim());
|
||||
s.stop('Module info loaded');
|
||||
sourceResult = await customMgr.resolveSource(sourceInput.trim(), { skipInstall: true, silent: true });
|
||||
s.stop(sourceResult.parsed.type === 'local' ? 'Local source resolved' : 'Repository cloned');
|
||||
} catch (error) {
|
||||
s.error('Failed to resolve source');
|
||||
await prompts.log.error(` ${error.message}`);
|
||||
addMore = await prompts.confirm({ message: 'Try another source?', default: false });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sourceResult.parsed.type === 'local') {
|
||||
await prompts.log.info('LOCAL MODULE: Pointing directly at local source (changes take effect on reinstall).');
|
||||
} else {
|
||||
await prompts.log.warn(
|
||||
'UNVERIFIED MODULE: This module has not been reviewed by the BMad team.\n' + ' Only install modules from sources you trust.',
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve plugins based on discovery mode vs direct mode
|
||||
s.start('Analyzing plugin structure...');
|
||||
const allResolved = [];
|
||||
const localPath = sourceResult.parsed.type === 'local' ? sourceResult.rootDir : null;
|
||||
|
||||
if (sourceResult.mode === 'discovery') {
|
||||
// Discovery mode: marketplace.json found, list available plugins
|
||||
let plugins;
|
||||
try {
|
||||
plugins = await customMgr.discoverModules(sourceResult.marketplace, sourceResult.sourceUrl);
|
||||
} catch (discoverError) {
|
||||
s.error('Failed to discover modules');
|
||||
await prompts.log.error(` ${discoverError.message}`);
|
||||
addMore = await prompts.confirm({ message: 'Try another source?', default: false });
|
||||
continue;
|
||||
}
|
||||
|
||||
const effectiveRepoPath = sourceResult.repoPath || sourceResult.rootDir;
|
||||
for (const plugin of plugins) {
|
||||
const versionStr = plugin.version ? ` v${plugin.version}` : '';
|
||||
await prompts.log.info(` ${plugin.name}${versionStr}\n ${plugin.description}\n Author: ${plugin.author}`);
|
||||
}
|
||||
|
||||
const confirmInstall = await prompts.confirm({
|
||||
message: `Install ${plugins.length} plugin${plugins.length === 1 ? '' : 's'} from ${url.trim()}?`,
|
||||
default: false,
|
||||
});
|
||||
|
||||
if (confirmInstall) {
|
||||
// Pre-clone the repo so it's cached for the install pipeline
|
||||
s.start('Cloning repository...');
|
||||
try {
|
||||
await customMgr.cloneRepo(url.trim());
|
||||
s.stop('Repository cloned');
|
||||
} catch (cloneError) {
|
||||
s.error('Failed to clone repository');
|
||||
await prompts.log.error(` ${cloneError.message}`);
|
||||
addMore = await prompts.confirm({ message: 'Try another URL?', default: false });
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const plugin of plugins) {
|
||||
selectedModules.push(plugin.code);
|
||||
const resolved = await customMgr.resolvePlugin(effectiveRepoPath, plugin.rawPlugin, sourceResult.sourceUrl, localPath);
|
||||
if (resolved.length > 0) {
|
||||
allResolved.push(...resolved);
|
||||
} else {
|
||||
// No skills array or empty - use plugin metadata as-is (legacy)
|
||||
allResolved.push({
|
||||
code: plugin.code,
|
||||
name: plugin.displayName || plugin.name,
|
||||
version: plugin.version,
|
||||
description: plugin.description,
|
||||
strategy: 0,
|
||||
pluginName: plugin.name,
|
||||
skillPaths: [],
|
||||
});
|
||||
}
|
||||
} catch (resolveError) {
|
||||
await prompts.log.warn(` Could not resolve ${plugin.name}: ${resolveError.message}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
s.error('Failed to load module info');
|
||||
await prompts.log.error(` ${error.message}`);
|
||||
} else {
|
||||
// Direct mode: no marketplace.json, scan directory for skills and resolve
|
||||
const directPlugin = {
|
||||
name: sourceResult.parsed.displayName || path.basename(sourceResult.rootDir),
|
||||
source: '.',
|
||||
skills: [],
|
||||
};
|
||||
|
||||
// Scan for SKILL.md directories to populate skills array
|
||||
try {
|
||||
const entries = await fs.readdir(sourceResult.rootDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const skillMd = path.join(sourceResult.rootDir, entry.name, 'SKILL.md');
|
||||
if (await fs.pathExists(skillMd)) {
|
||||
directPlugin.skills.push(entry.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (scanError) {
|
||||
s.error('Failed to scan directory');
|
||||
await prompts.log.error(` ${scanError.message}`);
|
||||
addMore = await prompts.confirm({ message: 'Try another source?', default: false });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (directPlugin.skills.length > 0) {
|
||||
try {
|
||||
const resolved = await customMgr.resolvePlugin(sourceResult.rootDir, directPlugin, sourceResult.sourceUrl, localPath);
|
||||
allResolved.push(...resolved);
|
||||
} catch (resolveError) {
|
||||
await prompts.log.warn(` Could not resolve: ${resolveError.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
s.stop(`Found ${allResolved.length} installable module${allResolved.length === 1 ? '' : 's'}`);
|
||||
|
||||
if (allResolved.length === 0) {
|
||||
await prompts.log.warn('No installable modules found in this source.');
|
||||
addMore = await prompts.confirm({ message: 'Try another source?', default: false });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build multiselect choices
|
||||
// Already-installed modules are pre-checked (update). New modules are unchecked (opt-in).
|
||||
// Unchecking an installed module means "skip update" - removal is handled elsewhere.
|
||||
const choices = allResolved.map((mod) => {
|
||||
const versionStr = mod.version ? ` v${mod.version}` : '';
|
||||
const skillCount = mod.skillPaths ? mod.skillPaths.length : 0;
|
||||
const skillStr = skillCount > 0 ? ` (${skillCount} skill${skillCount === 1 ? '' : 's'})` : '';
|
||||
const alreadyInstalled = installedModuleIds.has(mod.code);
|
||||
const hint = alreadyInstalled ? 'update' : undefined;
|
||||
|
||||
return {
|
||||
name: `${mod.name}${versionStr}${skillStr}`,
|
||||
value: mod.code,
|
||||
hint,
|
||||
checked: alreadyInstalled,
|
||||
};
|
||||
});
|
||||
|
||||
// Show descriptions before the multiselect
|
||||
for (const mod of allResolved) {
|
||||
const versionStr = mod.version ? ` v${mod.version}` : '';
|
||||
await prompts.log.info(` ${mod.name}${versionStr}\n ${mod.description}`);
|
||||
}
|
||||
|
||||
const selected = await prompts.multiselect({
|
||||
message: 'Select modules to install:',
|
||||
choices,
|
||||
required: false,
|
||||
});
|
||||
|
||||
if (selected && selected.length > 0) {
|
||||
for (const code of selected) {
|
||||
selectedModules.push(code);
|
||||
}
|
||||
}
|
||||
|
||||
addMore = await prompts.confirm({
|
||||
message: 'Add another custom module?',
|
||||
message: 'Add another custom source?',
|
||||
default: false,
|
||||
});
|
||||
}
|
||||
|
|
@ -901,6 +1020,102 @@ class UI {
|
|||
return selectedModules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve custom sources from --custom-source CLI flag (non-interactive).
|
||||
* Auto-selects all discovered modules from each source.
|
||||
* @param {string} sourcesArg - Comma-separated Git URLs or local paths
|
||||
* @returns {Array} Module codes from all resolved sources
|
||||
*/
|
||||
async _resolveCustomSourcesCli(sourcesArg) {
|
||||
const { CustomModuleManager } = require('./modules/custom-module-manager');
|
||||
const customMgr = new CustomModuleManager();
|
||||
const allCodes = [];
|
||||
|
||||
const sources = sourcesArg
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
for (const source of sources) {
|
||||
const s = await prompts.spinner();
|
||||
s.start(`Resolving ${source}...`);
|
||||
|
||||
let sourceResult;
|
||||
try {
|
||||
sourceResult = await customMgr.resolveSource(source, { skipInstall: true, silent: true });
|
||||
s.stop(sourceResult.parsed.type === 'local' ? 'Local source resolved' : 'Repository cloned');
|
||||
} catch (error) {
|
||||
s.error(`Failed to resolve ${source}`);
|
||||
await prompts.log.error(` ${error.message}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const s2 = await prompts.spinner();
|
||||
s2.start('Analyzing plugin structure...');
|
||||
const allResolved = [];
|
||||
const localPath = sourceResult.parsed.type === 'local' ? sourceResult.rootDir : null;
|
||||
|
||||
if (sourceResult.mode === 'discovery') {
|
||||
try {
|
||||
const plugins = await customMgr.discoverModules(sourceResult.marketplace, sourceResult.sourceUrl);
|
||||
const effectiveRepoPath = sourceResult.repoPath || sourceResult.rootDir;
|
||||
for (const plugin of plugins) {
|
||||
try {
|
||||
const resolved = await customMgr.resolvePlugin(effectiveRepoPath, plugin.rawPlugin, sourceResult.sourceUrl, localPath);
|
||||
if (resolved.length > 0) {
|
||||
allResolved.push(...resolved);
|
||||
}
|
||||
} catch {
|
||||
// Skip unresolvable plugins
|
||||
}
|
||||
}
|
||||
} catch (discoverError) {
|
||||
s2.error('Failed to discover modules');
|
||||
await prompts.log.error(` ${discoverError.message}`);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Direct mode: scan for SKILL.md directories
|
||||
const directPlugin = {
|
||||
name: sourceResult.parsed.displayName || path.basename(sourceResult.rootDir),
|
||||
source: '.',
|
||||
skills: [],
|
||||
};
|
||||
try {
|
||||
const entries = await fs.readdir(sourceResult.rootDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const skillMd = path.join(sourceResult.rootDir, entry.name, 'SKILL.md');
|
||||
if (await fs.pathExists(skillMd)) {
|
||||
directPlugin.skills.push(entry.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip unreadable directories
|
||||
}
|
||||
|
||||
if (directPlugin.skills.length > 0) {
|
||||
try {
|
||||
const resolved = await customMgr.resolvePlugin(sourceResult.rootDir, directPlugin, sourceResult.sourceUrl, localPath);
|
||||
allResolved.push(...resolved);
|
||||
} catch {
|
||||
// Skip unresolvable
|
||||
}
|
||||
}
|
||||
}
|
||||
s2.stop(`Found ${allResolved.length} module${allResolved.length === 1 ? '' : 's'}`);
|
||||
|
||||
for (const mod of allResolved) {
|
||||
allCodes.push(mod.code);
|
||||
const versionStr = mod.version ? ` v${mod.version}` : '';
|
||||
await prompts.log.info(` Custom module: ${mod.name}${versionStr}`);
|
||||
}
|
||||
}
|
||||
|
||||
return allCodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default modules for non-interactive mode
|
||||
* @param {Set} installedModuleIds - Already installed module IDs
|
||||
|
|
|
|||
Loading…
Reference in New Issue