Compare commits
3 Commits
6ff74ba662
...
7ee5fa313b
| Author | SHA1 | Date |
|---|---|---|
|
|
7ee5fa313b | |
|
|
3e89b30b3c | |
|
|
b4d73b7daf |
|
|
@ -18,7 +18,7 @@ Use `npx bmad-method install` to set up BMad in your project. One command handle
|
||||||
|
|
||||||
- **Node.js** 20+ (the installer requires it)
|
- **Node.js** 20+ (the installer requires it)
|
||||||
- **Git** (for cloning external modules)
|
- **Git** (for cloning external modules)
|
||||||
- **An AI tool** such as Claude Code or Cursor — or install without one using `--tools none`
|
- **An AI tool** such as Claude Code or Cursor (run `npx bmad-method install --list-tools` to see all supported tools)
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
|
@ -122,7 +122,8 @@ Under `--yes`, patch and minor upgrades apply automatically. Majors stay frozen
|
||||||
| `--yes`, `-y` | Skip all prompts; accept flag values + defaults |
|
| `--yes`, `-y` | Skip all prompts; accept flag values + defaults |
|
||||||
| `--directory <path>` | Install into this directory (default: current working dir) |
|
| `--directory <path>` | Install into this directory (default: current working dir) |
|
||||||
| `--modules <a,b,c>` | Exact module set. Core is auto-added. Not a delta — list everything you want kept. |
|
| `--modules <a,b,c>` | Exact module set. Core is auto-added. Not a delta — list everything you want kept. |
|
||||||
| `--tools <a,b>` or `--tools none` | IDE/tool selection. `none` skips tool config entirely. |
|
| `--tools <a,b>` | IDE/tool selection. Required for fresh `--yes` installs. Run `--list-tools` for valid IDs. |
|
||||||
|
| `--list-tools` | Print all supported tool/IDE IDs (with target directories) and exit. |
|
||||||
| `--action <type>` | `install`, `update`, or `quick-update`. Defaults based on existing install state. |
|
| `--action <type>` | `install`, `update`, or `quick-update`. Defaults based on existing install state. |
|
||||||
| `--custom-source <urls>` | Install custom modules from Git URLs or local paths |
|
| `--custom-source <urls>` | Install custom modules from Git URLs or local paths |
|
||||||
| `--channel <stable\|next>` | Apply to all externals (aliased as `--all-stable` / `--all-next`) |
|
| `--channel <stable\|next>` | Apply to all externals (aliased as `--all-stable` / `--all-next`) |
|
||||||
|
|
@ -165,17 +166,17 @@ npx bmad-method install --yes --modules bmm,bmb --all-next --tools claude-code
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx bmad-method install --yes --action update \
|
npx bmad-method install --yes --action update \
|
||||||
--modules bmm,bmb,gds \
|
--modules bmm,bmb,gds
|
||||||
--tools none
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`--tools` is omitted intentionally — `--action update` reuses the tools configured during the first install.
|
||||||
|
|
||||||
**Mix channels — bmb on next, gds on stable:**
|
**Mix channels — bmb on next, gds on stable:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx bmad-method install --yes --action update \
|
npx bmad-method install --yes --action update \
|
||||||
--modules bmm,bmb,cis,gds \
|
--modules bmm,bmb,cis,gds \
|
||||||
--next=bmb \
|
--next=bmb
|
||||||
--tools none
|
|
||||||
```
|
```
|
||||||
|
|
||||||
:::caution[Rate limit on shared IPs]
|
:::caution[Rate limit on shared IPs]
|
||||||
|
|
@ -204,7 +205,7 @@ For cross-machine reproducibility, don't rely on rerunning the same `--modules`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx bmad-method install --yes --modules bmb,cis \
|
npx bmad-method install --yes --modules bmb,cis \
|
||||||
--pin bmb=v1.7.0 --pin cis=v0.4.2 --tools none
|
--pin bmb=v1.7.0 --pin cis=v0.4.2 --tools claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ Select **Yes**, then provide a source:
|
||||||
| Input Type | Example |
|
| Input Type | Example |
|
||||||
| --------------------- | ------------------------------------------------- |
|
| --------------------- | ------------------------------------------------- |
|
||||||
| HTTPS URL (any host) | `https://github.com/org/repo` |
|
| HTTPS URL (any host) | `https://github.com/org/repo` |
|
||||||
|
| HTTP URL (any host) | `http://host/org/repo` |
|
||||||
| HTTPS URL with subdir | `https://github.com/org/repo/tree/main/my-module` |
|
| HTTPS URL with subdir | `https://github.com/org/repo/tree/main/my-module` |
|
||||||
| SSH URL | `git@github.com:org/repo.git` |
|
| SSH URL | `git@github.com:org/repo.git` |
|
||||||
| Local path | `/Users/me/projects/my-module` |
|
| Local path | `/Users/me/projects/my-module` |
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ Chọn **Yes**, rồi nhập nguồn:
|
||||||
| Loại đầu vào | Ví dụ |
|
| Loại đầu vào | Ví dụ |
|
||||||
| --------------------- | ------------------------------------------------- |
|
| --------------------- | ------------------------------------------------- |
|
||||||
| HTTPS URL trên bất kỳ host nào | `https://github.com/org/repo` |
|
| HTTPS URL trên bất kỳ host nào | `https://github.com/org/repo` |
|
||||||
|
| HTTP URL trên bất kỳ host nào | `http://host/org/repo` |
|
||||||
| HTTPS URL trỏ vào một thư mục con | `https://github.com/org/repo/tree/main/my-module` |
|
| HTTPS URL trỏ vào một thư mục con | `https://github.com/org/repo/tree/main/my-module` |
|
||||||
| SSH URL | `git@github.com:org/repo.git` |
|
| SSH URL | `git@github.com:org/repo.git` |
|
||||||
| Đường dẫn cục bộ | `/Users/me/projects/my-module` |
|
| Đường dẫn cục bộ | `/Users/me/projects/my-module` |
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ Would you like to install from a custom source (Git URL or local path)?
|
||||||
| 输入类型 | 示例 |
|
| 输入类型 | 示例 |
|
||||||
| -------- | ---- |
|
| -------- | ---- |
|
||||||
| HTTPS URL(任意主机) | `https://github.com/org/repo` |
|
| HTTPS URL(任意主机) | `https://github.com/org/repo` |
|
||||||
|
| HTTP URL(任意主机) | `http://host/org/repo` |
|
||||||
| 带子目录的 HTTPS URL | `https://github.com/org/repo/tree/main/my-module` |
|
| 带子目录的 HTTPS URL | `https://github.com/org/repo/tree/main/my-module` |
|
||||||
| SSH URL | `git@github.com:org/repo.git` |
|
| SSH URL | `git@github.com:org/repo.git` |
|
||||||
| 本地路径 | `/Users/me/projects/my-module` |
|
| 本地路径 | `/Users/me/projects/my-module` |
|
||||||
|
|
|
||||||
|
|
@ -2773,6 +2773,94 @@ async function runTests() {
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Test Suite 42: --tools flag parsing & validation (#2326)
|
||||||
|
// ============================================================
|
||||||
|
console.log(`${colors.yellow}Test Suite 42: --tools flag parsing & validation${colors.reset}\n`);
|
||||||
|
try {
|
||||||
|
const { UI } = require('../tools/installer/ui');
|
||||||
|
const ui = new UI();
|
||||||
|
const known = new Set(['claude-code', 'cursor', 'windsurf']);
|
||||||
|
|
||||||
|
assert(
|
||||||
|
JSON.stringify(ui._parseToolsFlag('claude-code', known)) === JSON.stringify(['claude-code']),
|
||||||
|
'parseToolsFlag returns single ID',
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(
|
||||||
|
JSON.stringify(ui._parseToolsFlag('claude-code,cursor', known)) === JSON.stringify(['claude-code', 'cursor']),
|
||||||
|
'parseToolsFlag returns multiple IDs',
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(
|
||||||
|
JSON.stringify(ui._parseToolsFlag(' claude-code , cursor ', known)) === JSON.stringify(['claude-code', 'cursor']),
|
||||||
|
'parseToolsFlag trims whitespace',
|
||||||
|
);
|
||||||
|
|
||||||
|
let emptyErr;
|
||||||
|
try {
|
||||||
|
ui._parseToolsFlag('', known);
|
||||||
|
} catch (error) {
|
||||||
|
emptyErr = error;
|
||||||
|
}
|
||||||
|
assert(
|
||||||
|
emptyErr && emptyErr.expected === true && /empty/i.test(emptyErr.message),
|
||||||
|
'parseToolsFlag rejects empty string with expected=true',
|
||||||
|
);
|
||||||
|
|
||||||
|
let commasOnlyErr;
|
||||||
|
try {
|
||||||
|
ui._parseToolsFlag(' , , ', known);
|
||||||
|
} catch (error) {
|
||||||
|
commasOnlyErr = error;
|
||||||
|
}
|
||||||
|
assert(commasOnlyErr && commasOnlyErr.expected === true, 'parseToolsFlag rejects whitespace/comma-only input');
|
||||||
|
|
||||||
|
let noneErr;
|
||||||
|
try {
|
||||||
|
ui._parseToolsFlag('none', known);
|
||||||
|
} catch (error) {
|
||||||
|
noneErr = error;
|
||||||
|
}
|
||||||
|
assert(noneErr && noneErr.expected === true && /Unknown tool ID/.test(noneErr.message), 'parseToolsFlag rejects "none" as unknown ID');
|
||||||
|
|
||||||
|
let typoErr;
|
||||||
|
try {
|
||||||
|
ui._parseToolsFlag('claude-code,claude-cdoe', known);
|
||||||
|
} catch (error) {
|
||||||
|
typoErr = error;
|
||||||
|
}
|
||||||
|
const typoHeader = typoErr ? typoErr.message.split('\n')[0] : '';
|
||||||
|
assert(
|
||||||
|
typoErr && typoErr.expected === true && /claude-cdoe/.test(typoHeader) && !/claude-code/.test(typoHeader),
|
||||||
|
'parseToolsFlag reports only the unknown ID in error header (valid ones not listed as unknown)',
|
||||||
|
);
|
||||||
|
|
||||||
|
// --list-tools and --tools validation must agree on what counts as a valid ID.
|
||||||
|
const { formatPlatformList } = require('../tools/installer/ide/platform-codes');
|
||||||
|
const { IdeManager } = require('../tools/installer/ide/manager');
|
||||||
|
const ideManager42 = new IdeManager();
|
||||||
|
await ideManager42.ensureInitialized();
|
||||||
|
const validIds = new Set(ideManager42.getAvailableIdes().map((i) => i.value));
|
||||||
|
const listed = await formatPlatformList();
|
||||||
|
// Each entry line starts with ' *' (preferred) or ' ' (other), followed by the ID, then padding.
|
||||||
|
const entryLines = listed.split('\n').filter((l) => /^( \*| {2})[a-z]/.test(l));
|
||||||
|
const listedIds = entryLines.map((l) => l.trim().replace(/^\*/, '').split(/\s+/)[0]);
|
||||||
|
const missingFromList = [...validIds].filter((id) => !listedIds.includes(id));
|
||||||
|
const extraInList = listedIds.filter((id) => !validIds.has(id));
|
||||||
|
assert(
|
||||||
|
missingFromList.length === 0 && extraInList.length === 0,
|
||||||
|
'--list-tools output matches the IDs that --tools accepts',
|
||||||
|
`Missing from list: ${missingFromList.join(',') || '(none)'}; Extra in list: ${extraInList.join(',') || '(none)'}`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`${colors.red}Test Suite 42 setup failed: ${error.message}${colors.reset}`);
|
||||||
|
console.log(error.stack);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Summary
|
// Summary
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,9 @@ module.exports = {
|
||||||
['--modules <modules>', 'Comma-separated list of module IDs to install (e.g., "bmm,bmb")'],
|
['--modules <modules>', 'Comma-separated list of module IDs to install (e.g., "bmm,bmb")'],
|
||||||
[
|
[
|
||||||
'--tools <tools>',
|
'--tools <tools>',
|
||||||
'Comma-separated list of tool/IDE IDs to configure (e.g., "claude-code,cursor"). Use "none" to skip tool configuration.',
|
'Comma-separated list of tool/IDE IDs to configure (e.g., "claude-code,cursor"). Required for fresh non-interactive (--yes) installs. Run with --list-tools to see all valid IDs.',
|
||||||
],
|
],
|
||||||
|
['--list-tools', 'Print all supported tool/IDE IDs (with target directories) and exit.'],
|
||||||
['--action <type>', 'Action type for existing installations: install, update, or quick-update'],
|
['--action <type>', 'Action type for existing installations: install, update, or quick-update'],
|
||||||
['--user-name <name>', 'Name for agents to use (default: system username)'],
|
['--user-name <name>', 'Name for agents to use (default: system username)'],
|
||||||
['--communication-language <lang>', 'Language for agent communication (default: English)'],
|
['--communication-language <lang>', 'Language for agent communication (default: English)'],
|
||||||
|
|
@ -40,6 +41,12 @@ module.exports = {
|
||||||
],
|
],
|
||||||
action: async (options) => {
|
action: async (options) => {
|
||||||
try {
|
try {
|
||||||
|
if (options.listTools) {
|
||||||
|
const { formatPlatformList } = require('../ide/platform-codes');
|
||||||
|
process.stdout.write((await formatPlatformList()) + '\n');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
// Set debug flag as environment variable for all components
|
// Set debug flag as environment variable for all components
|
||||||
if (options.debug) {
|
if (options.debug) {
|
||||||
process.env.BMAD_DEBUG_MANIFEST = 'true';
|
process.env.BMAD_DEBUG_MANIFEST = 'true';
|
||||||
|
|
@ -81,7 +88,7 @@ module.exports = {
|
||||||
} else {
|
} else {
|
||||||
await prompts.log.error(`Installation failed: ${error.message}`);
|
await prompts.log.error(`Installation failed: ${error.message}`);
|
||||||
}
|
}
|
||||||
if (error.stack) {
|
if (error.stack && !error.expected) {
|
||||||
await prompts.log.message(error.stack);
|
await prompts.log.message(error.stack);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,50 @@ function clearCache() {
|
||||||
_cachedPlatformCodes = null;
|
_cachedPlatformCodes = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format the installable platform list for human-readable output (used by --list-tools).
|
||||||
|
* Sourced from IdeManager so this view matches what --tools accepts at install time
|
||||||
|
* (suspended platforms excluded).
|
||||||
|
* @returns {Promise<string>} Formatted multi-line string with id, name, target_dir, preferred flag.
|
||||||
|
*/
|
||||||
|
async function formatPlatformList() {
|
||||||
|
const { IdeManager } = require('./manager');
|
||||||
|
const ideManager = new IdeManager();
|
||||||
|
await ideManager.ensureInitialized();
|
||||||
|
|
||||||
|
const entries = ideManager.getAvailableIdes().map((ide) => {
|
||||||
|
const handler = ideManager.handlers.get(ide.value);
|
||||||
|
return {
|
||||||
|
id: ide.value,
|
||||||
|
name: ide.name,
|
||||||
|
targetDir: handler?.installerConfig?.target_dir || '',
|
||||||
|
preferred: ide.preferred,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const idWidth = Math.max(...entries.map((e) => e.id.length), 'ID'.length);
|
||||||
|
const nameWidth = Math.max(...entries.map((e) => e.name.length), 'Name'.length);
|
||||||
|
|
||||||
|
const pad = (s, w) => s + ' '.repeat(Math.max(0, w - s.length));
|
||||||
|
const lines = [
|
||||||
|
`Supported tool IDs (pass via --tools <id>[,<id>...]):`,
|
||||||
|
'',
|
||||||
|
` ${pad('ID', idWidth)} ${pad('Name', nameWidth)} Target dir`,
|
||||||
|
` ${pad('-'.repeat(idWidth), idWidth)} ${pad('-'.repeat(nameWidth), nameWidth)} ${'-'.repeat(10)}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const e of entries) {
|
||||||
|
const star = e.preferred ? ' *' : ' ';
|
||||||
|
lines.push(`${star}${pad(e.id, idWidth)} ${pad(e.name, nameWidth)} ${e.targetDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push('', '* = recommended / preferred', '', 'Example: bmad-method install --modules bmm --tools claude-code');
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
loadPlatformCodes,
|
loadPlatformCodes,
|
||||||
clearCache,
|
clearCache,
|
||||||
|
formatPlatformList,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,9 @@ class CustomModuleManager {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a user-provided source input into a structured descriptor.
|
* Parse a user-provided source input into a structured descriptor.
|
||||||
* Accepts local file paths, HTTPS Git URLs, and SSH Git URLs.
|
* Accepts local file paths, HTTPS Git URLs, HTTP Git URLs, and SSH Git URLs.
|
||||||
* For HTTPS URLs with deep paths (e.g., /tree/main/subdir), extracts the subdir.
|
* For HTTPS/HTTP URLs with deep paths (e.g., /tree/main/subdir), extracts the subdir.
|
||||||
|
* The original protocol (http or https) is preserved in the returned cloneUrl.
|
||||||
*
|
*
|
||||||
* @param {string} input - URL or local file path
|
* @param {string} input - URL or local file path
|
||||||
* @returns {Object} Parsed source descriptor:
|
* @returns {Object} Parsed source descriptor:
|
||||||
|
|
@ -127,11 +128,11 @@ class CustomModuleManager {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPS URL: https://host/owner/repo[/tree/branch/subdir][.git]
|
// HTTPS/HTTP URL: https://host/owner/repo[/tree/branch/subdir][.git]
|
||||||
const httpsMatch = trimmed.match(/^https?:\/\/([^/]+)\/([^/]+)\/([^/.]+?)(?:\.git)?(\/.*)?$/);
|
const httpsMatch = trimmed.match(/^(https?):\/\/([^/]+)\/([^/]+)\/([^/.]+?)(?:\.git)?(\/.*)?$/);
|
||||||
if (httpsMatch) {
|
if (httpsMatch) {
|
||||||
const [, host, owner, repo, remainder] = httpsMatch;
|
const [, protocol, host, owner, repo, remainder] = httpsMatch;
|
||||||
const cloneUrl = `https://${host}/${owner}/${repo}`;
|
const cloneUrl = `${protocol}://${host}/${owner}/${repo}`;
|
||||||
let subdir = null;
|
let subdir = null;
|
||||||
let urlRef = null; // branch/tag extracted from /tree/<ref>/subdir
|
let urlRef = null; // branch/tag extracted from /tree/<ref>/subdir
|
||||||
|
|
||||||
|
|
@ -311,7 +312,7 @@ class CustomModuleManager {
|
||||||
/**
|
/**
|
||||||
* Clone a custom module repository to cache.
|
* Clone a custom module repository to cache.
|
||||||
* Supports any Git host (GitHub, GitLab, Bitbucket, self-hosted, etc.).
|
* Supports any Git host (GitHub, GitLab, Bitbucket, self-hosted, etc.).
|
||||||
* @param {string} sourceInput - Git URL (HTTPS or SSH)
|
* @param {string} sourceInput - Git URL (HTTPS, HTTP, or SSH)
|
||||||
* @param {Object} [options] - Clone options
|
* @param {Object} [options] - Clone options
|
||||||
* @param {boolean} [options.silent] - Suppress spinner output
|
* @param {boolean} [options.silent] - Suppress spinner output
|
||||||
* @param {boolean} [options.skipInstall] - Skip npm install (for browsing before user confirms)
|
* @param {boolean} [options.skipInstall] - Skip npm install (for browsing before user confirms)
|
||||||
|
|
|
||||||
|
|
@ -200,12 +200,15 @@ class UI {
|
||||||
actionType = options.action;
|
actionType = options.action;
|
||||||
await prompts.log.info(`Using action from command-line: ${actionType}`);
|
await prompts.log.info(`Using action from command-line: ${actionType}`);
|
||||||
} else if (options.yes) {
|
} else if (options.yes) {
|
||||||
// Default to quick-update if available, otherwise first available choice
|
// Default to quick-update if available, unless flags that require the
|
||||||
|
// full update path are present (e.g. --custom-source which re-clones
|
||||||
|
// modules at a new version — quick-update skips that entirely).
|
||||||
if (choices.length === 0) {
|
if (choices.length === 0) {
|
||||||
throw new Error('No valid actions available for this installation');
|
throw new Error('No valid actions available for this installation');
|
||||||
}
|
}
|
||||||
const hasQuickUpdate = choices.some((c) => c.value === 'quick-update');
|
const hasQuickUpdate = choices.some((c) => c.value === 'quick-update');
|
||||||
actionType = hasQuickUpdate ? 'quick-update' : choices[0].value;
|
const needsFullUpdate = !!options.customSource;
|
||||||
|
actionType = hasQuickUpdate && !needsFullUpdate ? 'quick-update' : (choices.find((c) => c.value === 'update') || choices[0]).value;
|
||||||
await prompts.log.info(`Non-interactive mode (--yes): defaulting to ${actionType}`);
|
await prompts.log.info(`Non-interactive mode (--yes): defaulting to ${actionType}`);
|
||||||
} else {
|
} else {
|
||||||
actionType = await prompts.select({
|
actionType = await prompts.select({
|
||||||
|
|
@ -241,8 +244,11 @@ class UI {
|
||||||
.map((m) => m.trim())
|
.map((m) => m.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`);
|
await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`);
|
||||||
} else if (options.customSource) {
|
} else if (options.customSource && !options.yes) {
|
||||||
// Custom source without --modules: start with empty list (core added below)
|
// Custom source without --modules or --yes: start with empty list
|
||||||
|
// (only custom source modules + core will be installed).
|
||||||
|
// When --yes is also set, fall through to the --yes branch so all
|
||||||
|
// installed modules are included alongside the custom source modules.
|
||||||
selectedModules = [];
|
selectedModules = [];
|
||||||
} else if (options.yes) {
|
} else if (options.yes) {
|
||||||
selectedModules = await this.getDefaultModules(installedModuleIds);
|
selectedModules = await this.getDefaultModules(installedModuleIds);
|
||||||
|
|
@ -398,6 +404,37 @@ class UI {
|
||||||
* @param {Object} options - Command-line options
|
* @param {Object} options - Command-line options
|
||||||
* @returns {Object} Tool configuration
|
* @returns {Object} Tool configuration
|
||||||
*/
|
*/
|
||||||
|
_parseToolsFlag(toolsArg, allKnownValues) {
|
||||||
|
const selectedIdes = toolsArg
|
||||||
|
.split(',')
|
||||||
|
.map((t) => t.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
if (selectedIdes.length === 0) {
|
||||||
|
const err = new Error(
|
||||||
|
'--tools was passed empty. Provide at least one tool ID (e.g. --tools claude-code) or run with --list-tools to see valid IDs.',
|
||||||
|
);
|
||||||
|
err.expected = true;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unknown = selectedIdes.filter((id) => !allKnownValues.has(id));
|
||||||
|
if (unknown.length > 0) {
|
||||||
|
const err = new Error(
|
||||||
|
[
|
||||||
|
`Unknown tool ID${unknown.length === 1 ? '' : 's'}: ${unknown.join(', ')}`,
|
||||||
|
'',
|
||||||
|
'Run with --list-tools to see all valid IDs.',
|
||||||
|
'Common: claude-code, cursor, copilot, windsurf, cline',
|
||||||
|
].join('\n'),
|
||||||
|
);
|
||||||
|
err.expected = true;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedIdes;
|
||||||
|
}
|
||||||
|
|
||||||
async promptToolSelection(projectDir, options = {}) {
|
async promptToolSelection(projectDir, options = {}) {
|
||||||
const { ExistingInstall } = require('./core/existing-install');
|
const { ExistingInstall } = require('./core/existing-install');
|
||||||
const { Installer } = require('./core/installer');
|
const { Installer } = require('./core/installer');
|
||||||
|
|
@ -432,15 +469,10 @@ class UI {
|
||||||
const allTools = [...preferredIdes, ...otherIdes];
|
const allTools = [...preferredIdes, ...otherIdes];
|
||||||
|
|
||||||
// Non-interactive: handle --tools and --yes flags before interactive prompt
|
// Non-interactive: handle --tools and --yes flags before interactive prompt
|
||||||
if (options.tools) {
|
// Use !== undefined so an explicit --tools "" falls through to _parseToolsFlag and
|
||||||
if (options.tools.toLowerCase() === 'none') {
|
// gets a specific "passed empty" error instead of being silently ignored.
|
||||||
await prompts.log.info('Skipping tool configuration (--tools none)');
|
if (options.tools !== undefined) {
|
||||||
return { ides: [], skipIde: true };
|
const selectedIdes = this._parseToolsFlag(options.tools, allKnownValues);
|
||||||
}
|
|
||||||
const selectedIdes = options.tools
|
|
||||||
.split(',')
|
|
||||||
.map((t) => t.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`);
|
await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`);
|
||||||
await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
|
await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
|
||||||
return { ides: selectedIdes, skipIde: false };
|
return { ides: selectedIdes, skipIde: false };
|
||||||
|
|
@ -516,21 +548,13 @@ class UI {
|
||||||
|
|
||||||
let selectedIdes = [];
|
let selectedIdes = [];
|
||||||
|
|
||||||
// Check if tools are provided via command-line
|
// Check if tools are provided via command-line.
|
||||||
if (options.tools) {
|
// Use !== undefined so an explicit --tools "" still hits _parseToolsFlag's empty-value error.
|
||||||
// Check for explicit "none" value to skip tool installation
|
if (options.tools !== undefined) {
|
||||||
if (options.tools.toLowerCase() === 'none') {
|
selectedIdes = this._parseToolsFlag(options.tools, allKnownValues);
|
||||||
await prompts.log.info('Skipping tool configuration (--tools none)');
|
await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`);
|
||||||
return { ides: [], skipIde: true };
|
await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
|
||||||
} else {
|
return { ides: selectedIdes, skipIde: false };
|
||||||
selectedIdes = options.tools
|
|
||||||
.split(',')
|
|
||||||
.map((t) => t.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`);
|
|
||||||
await this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
|
|
||||||
return { ides: selectedIdes, skipIde: false };
|
|
||||||
}
|
|
||||||
} else if (options.yes) {
|
} else if (options.yes) {
|
||||||
// If --yes flag is set, skip tool prompt and use previously configured tools or empty
|
// If --yes flag is set, skip tool prompt and use previously configured tools or empty
|
||||||
if (configuredIdes.length > 0) {
|
if (configuredIdes.length > 0) {
|
||||||
|
|
@ -538,8 +562,18 @@ class UI {
|
||||||
await this.displaySelectedTools(configuredIdes, preferredIdes, allTools);
|
await this.displaySelectedTools(configuredIdes, preferredIdes, allTools);
|
||||||
return { ides: configuredIdes, skipIde: false };
|
return { ides: configuredIdes, skipIde: false };
|
||||||
} else {
|
} else {
|
||||||
await prompts.log.info('Skipping tool configuration (--yes flag, no previous tools)');
|
const err = new Error(
|
||||||
return { ides: [], skipIde: true };
|
[
|
||||||
|
'--tools is required for non-interactive install (--yes / -y) when no tools are previously configured.',
|
||||||
|
'',
|
||||||
|
'Common: claude-code, cursor, copilot, windsurf, cline',
|
||||||
|
'See all supported tools: bmad-method install --list-tools',
|
||||||
|
'',
|
||||||
|
'Example: bmad-method install --modules bmm --tools claude-code -y',
|
||||||
|
].join('\n'),
|
||||||
|
);
|
||||||
|
err.expected = true;
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue