Compare commits
12 Commits
0655f3fe1d
...
dded90b153
| Author | SHA1 | Date |
|---|---|---|
|
|
dded90b153 | |
|
|
1040c3c306 | |
|
|
ed9dea9058 | |
|
|
3d8a89c7e1 | |
|
|
8927e207e8 | |
|
|
1571a3cff4 | |
|
|
5c0dfd85ad | |
|
|
7422d3a0a3 | |
|
|
d2e7158c72 | |
|
|
ac04378a1a | |
|
|
fe2cbe9a78 | |
|
|
1cdc7f4a48 |
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"name": "bmad-method",
|
||||
"owner": {
|
||||
"name": "Brian (BMad) Madison"
|
||||
},
|
||||
"description": "Breakthrough Method of Agile AI-driven Development — a full-lifecycle framework with agents and workflows for analysis, planning, architecture, and implementation.",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/bmad-code-org/BMAD-METHOD",
|
||||
"repository": "https://github.com/bmad-code-org/BMAD-METHOD",
|
||||
"keywords": ["bmad", "agile", "ai", "orchestrator", "development", "methodology", "agents"],
|
||||
"plugins": [
|
||||
{
|
||||
"name": "bmad-pro-skills",
|
||||
"source": "./",
|
||||
"description": "Next level skills for power users — advanced prompting techniques, agent management, and more.",
|
||||
"version": "6.3.0",
|
||||
"author": {
|
||||
"name": "Brian (BMad) Madison"
|
||||
},
|
||||
"skills": [
|
||||
"./src/core-skills/bmad-help",
|
||||
"./src/core-skills/bmad-init",
|
||||
"./src/core-skills/bmad-brainstorming",
|
||||
"./src/core-skills/bmad-distillator",
|
||||
"./src/core-skills/bmad-party-mode",
|
||||
"./src/core-skills/bmad-shard-doc",
|
||||
"./src/core-skills/bmad-advanced-elicitation",
|
||||
"./src/core-skills/bmad-editorial-review-prose",
|
||||
"./src/core-skills/bmad-editorial-review-structure",
|
||||
"./src/core-skills/bmad-index-docs",
|
||||
"./src/core-skills/bmad-review-adversarial-general",
|
||||
"./src/core-skills/bmad-review-edge-case-hunter"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "bmad-method-lifecycle",
|
||||
"source": "./",
|
||||
"description": "Full-lifecycle AI development framework — agents and workflows for product analysis, planning, architecture, and implementation.",
|
||||
"version": "6.3.0",
|
||||
"author": {
|
||||
"name": "Brian (BMad) Madison"
|
||||
},
|
||||
"skills": [
|
||||
"./src/bmm-skills/1-analysis/bmad-product-brief",
|
||||
"./src/bmm-skills/1-analysis/bmad-agent-analyst",
|
||||
"./src/bmm-skills/1-analysis/bmad-agent-tech-writer",
|
||||
"./src/bmm-skills/1-analysis/bmad-document-project",
|
||||
"./src/bmm-skills/1-analysis/research/bmad-domain-research",
|
||||
"./src/bmm-skills/1-analysis/research/bmad-market-research",
|
||||
"./src/bmm-skills/1-analysis/research/bmad-technical-research",
|
||||
"./src/bmm-skills/2-plan-workflows/bmad-agent-pm",
|
||||
"./src/bmm-skills/2-plan-workflows/bmad-agent-ux-designer",
|
||||
"./src/bmm-skills/2-plan-workflows/bmad-create-prd",
|
||||
"./src/bmm-skills/2-plan-workflows/bmad-edit-prd",
|
||||
"./src/bmm-skills/2-plan-workflows/bmad-validate-prd",
|
||||
"./src/bmm-skills/2-plan-workflows/bmad-create-ux-design",
|
||||
"./src/bmm-skills/3-solutioning/bmad-agent-architect",
|
||||
"./src/bmm-skills/3-solutioning/bmad-create-architecture",
|
||||
"./src/bmm-skills/3-solutioning/bmad-check-implementation-readiness",
|
||||
"./src/bmm-skills/3-solutioning/bmad-create-epics-and-stories",
|
||||
"./src/bmm-skills/3-solutioning/bmad-generate-project-context",
|
||||
"./src/bmm-skills/4-implementation/bmad-agent-dev",
|
||||
"./src/bmm-skills/4-implementation/bmad-agent-sm",
|
||||
"./src/bmm-skills/4-implementation/bmad-agent-qa",
|
||||
"./src/bmm-skills/4-implementation/bmad-agent-quick-flow-solo-dev",
|
||||
"./src/bmm-skills/4-implementation/bmad-dev-story",
|
||||
"./src/bmm-skills/4-implementation/bmad-quick-dev",
|
||||
"./src/bmm-skills/4-implementation/bmad-sprint-planning",
|
||||
"./src/bmm-skills/4-implementation/bmad-sprint-status",
|
||||
"./src/bmm-skills/4-implementation/bmad-code-review",
|
||||
"./src/bmm-skills/4-implementation/bmad-create-story",
|
||||
"./src/bmm-skills/4-implementation/bmad-correct-course",
|
||||
"./src/bmm-skills/4-implementation/bmad-retrospective",
|
||||
"./src/bmm-skills/4-implementation/bmad-qa-generate-e2e-tests"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -37,7 +37,19 @@ Requires [Node.js](https://nodejs.org) v20+ and `npx` (included with npm).
|
|||
| `--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 | _bmad-output |
|
||||
| `--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 |
|
||||
|
||||
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
|
||||
|
||||
|
|
@ -141,6 +153,7 @@ Invalid values will either:
|
|||
|
||||
:::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
|
||||
|
|
|
|||
|
|
@ -166,9 +166,27 @@ def resolve_project_root_placeholder(value, project_root):
|
|||
"""Replace {project-root} placeholder with actual path."""
|
||||
if not value or not isinstance(value, str):
|
||||
return value
|
||||
if '{project-root}' in value:
|
||||
return value.replace('{project-root}', str(project_root))
|
||||
return value
|
||||
if '{project-root}' not in value:
|
||||
return value
|
||||
|
||||
# Strip the {project-root} token to inspect what remains, so we can
|
||||
# correctly handle absolute paths stored as "{project-root}//absolute/path"
|
||||
# (produced by the "{project-root}/{value}" template applied to an absolute value).
|
||||
suffix = value.replace('{project-root}', '', 1)
|
||||
|
||||
# Strip the one path separator that follows the token (if any)
|
||||
if suffix.startswith('/') or suffix.startswith('\\'):
|
||||
remainder = suffix[1:]
|
||||
else:
|
||||
remainder = suffix
|
||||
|
||||
if os.path.isabs(remainder):
|
||||
# The original value was an absolute path stored with a {project-root}/ prefix.
|
||||
# Return the absolute path directly — no joining needed.
|
||||
return remainder
|
||||
|
||||
# Relative path: join with project root and normalize to resolve any .. segments.
|
||||
return os.path.normpath(os.path.join(str(project_root), remainder))
|
||||
|
||||
|
||||
def parse_var_specs(vars_string):
|
||||
|
|
@ -222,9 +240,22 @@ def apply_result_template(var_def, raw_value, context):
|
|||
if not result_template:
|
||||
return raw_value
|
||||
|
||||
# If the user supplied an absolute path and the template would prefix it with
|
||||
# "{project-root}/", skip the template entirely to avoid producing a broken path
|
||||
# like "/my/project//absolute/path".
|
||||
if isinstance(raw_value, str) and os.path.isabs(raw_value):
|
||||
return raw_value
|
||||
|
||||
ctx = dict(context)
|
||||
ctx['value'] = raw_value
|
||||
return expand_template(result_template, ctx)
|
||||
result = expand_template(result_template, ctx)
|
||||
|
||||
# Normalize the resulting path to resolve any ".." segments (e.g. when the user
|
||||
# entered a relative path such as "../../outside-dir").
|
||||
if isinstance(result, str) and '{' not in result and os.path.isabs(result):
|
||||
result = os.path.normpath(result)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
|
|
|||
|
|
@ -110,6 +110,37 @@ class TestResolveProjectRootPlaceholder(unittest.TestCase):
|
|||
def test_non_string(self):
|
||||
self.assertEqual(resolve_project_root_placeholder(42, Path('/test')), 42)
|
||||
|
||||
def test_absolute_path_stored_with_prefix(self):
|
||||
"""Absolute output_folder entered by user is stored as '{project-root}//abs/path'
|
||||
by the '{project-root}/{value}' template. It must resolve to '/abs/path', not
|
||||
'/project//abs/path'."""
|
||||
result = resolve_project_root_placeholder(
|
||||
'{project-root}//Users/me/outside', Path('/Users/me/myproject')
|
||||
)
|
||||
self.assertEqual(result, '/Users/me/outside')
|
||||
|
||||
def test_relative_path_with_traversal_is_normalized(self):
|
||||
"""A relative path like '../../sibling' produces '{project-root}/../../sibling'
|
||||
after the template. It must resolve to the normalized absolute path, not the
|
||||
un-normalized string '/project/../../sibling'."""
|
||||
result = resolve_project_root_placeholder(
|
||||
'{project-root}/../../sibling', Path('/Users/me/myproject')
|
||||
)
|
||||
self.assertEqual(result, '/Users/sibling')
|
||||
|
||||
def test_relative_path_one_level_up(self):
|
||||
result = resolve_project_root_placeholder(
|
||||
'{project-root}/../outside-outputs', Path('/project/root')
|
||||
)
|
||||
self.assertEqual(result, '/project/outside-outputs')
|
||||
|
||||
def test_standard_relative_path_unchanged(self):
|
||||
"""Normal in-project relative paths continue to work correctly."""
|
||||
result = resolve_project_root_placeholder(
|
||||
'{project-root}/_bmad-output', Path('/project/root')
|
||||
)
|
||||
self.assertEqual(result, '/project/root/_bmad-output')
|
||||
|
||||
|
||||
class TestExpandTemplate(unittest.TestCase):
|
||||
|
||||
|
|
@ -147,6 +178,39 @@ class TestApplyResultTemplate(unittest.TestCase):
|
|||
result = apply_result_template(var_def, 'English', {})
|
||||
self.assertEqual(result, 'English')
|
||||
|
||||
def test_absolute_value_skips_project_root_template(self):
|
||||
"""When the user enters an absolute path, the '{project-root}/{value}' template
|
||||
must not be applied — doing so would produce '/project//absolute/path'."""
|
||||
var_def = {'result': '{project-root}/{value}'}
|
||||
result = apply_result_template(
|
||||
var_def, '/Users/me/shared-outputs', {'project-root': '/Users/me/myproject'}
|
||||
)
|
||||
self.assertEqual(result, '/Users/me/shared-outputs')
|
||||
|
||||
def test_relative_traversal_value_is_normalized(self):
|
||||
"""A relative path like '../../outside' combined with the project-root template
|
||||
must produce a clean normalized absolute path, not '/project/../../outside'."""
|
||||
var_def = {'result': '{project-root}/{value}'}
|
||||
result = apply_result_template(
|
||||
var_def, '../../outside-dir', {'project-root': '/Users/me/myproject'}
|
||||
)
|
||||
self.assertEqual(result, '/Users/outside-dir')
|
||||
|
||||
def test_relative_one_level_up_is_normalized(self):
|
||||
var_def = {'result': '{project-root}/{value}'}
|
||||
result = apply_result_template(
|
||||
var_def, '../sibling-outputs', {'project-root': '/project/root'}
|
||||
)
|
||||
self.assertEqual(result, '/project/sibling-outputs')
|
||||
|
||||
def test_normal_relative_value_unchanged(self):
|
||||
"""Standard in-project relative paths still produce the expected joined path."""
|
||||
var_def = {'result': '{project-root}/{value}'}
|
||||
result = apply_result_template(
|
||||
var_def, '_bmad-output', {'project-root': '/project/root'}
|
||||
)
|
||||
self.assertEqual(result, '/project/root/_bmad-output')
|
||||
|
||||
|
||||
class TestLoadModuleYaml(unittest.TestCase):
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ const path = require('node:path');
|
|||
const os = require('node:os');
|
||||
const fs = require('fs-extra');
|
||||
const { ConfigCollector } = require('../tools/cli/installers/lib/core/config-collector');
|
||||
const { applyDefaultCoreConfig, clearCoreConfigDefaultsCache, getDefaultCoreConfig } = require('../tools/cli/lib/core-config-defaults');
|
||||
const { ManifestGenerator } = require('../tools/cli/installers/lib/core/manifest-generator');
|
||||
const { IdeManager } = require('../tools/cli/installers/lib/ide/manager');
|
||||
const { clearCache, loadPlatformCodes } = require('../tools/cli/installers/lib/ide/platform-codes');
|
||||
|
|
@ -2028,6 +2029,38 @@ async function runTests() {
|
|||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Test Suite 34: Core Config Default Backfill
|
||||
// ============================================================
|
||||
console.log(`${colors.yellow}Test Suite 34: Core Config Default Backfill${colors.reset}\n`);
|
||||
|
||||
try {
|
||||
clearCoreConfigDefaultsCache();
|
||||
const defaults = await getDefaultCoreConfig();
|
||||
const normalized = applyDefaultCoreConfig(
|
||||
{
|
||||
user_name: '{user_name}',
|
||||
communication_language: 'Spanish',
|
||||
document_output_language: '',
|
||||
output_folder: '{output_folder}',
|
||||
},
|
||||
defaults,
|
||||
);
|
||||
|
||||
assert(normalized.appliedDefaults === true, 'Core config backfill reports when defaults were applied');
|
||||
assert(normalized.coreConfig.user_name === defaults.user_name, 'Core config backfill replaces unresolved user_name placeholder');
|
||||
assert(normalized.coreConfig.communication_language === 'Spanish', 'Core config backfill preserves existing valid values');
|
||||
assert(
|
||||
normalized.coreConfig.document_output_language === defaults.document_output_language,
|
||||
'Core config backfill replaces blank document output language',
|
||||
);
|
||||
assert(normalized.coreConfig.output_folder === defaults.output_folder, 'Core config backfill replaces unresolved output_folder');
|
||||
} catch (error) {
|
||||
assert(false, 'Core config default backfill test succeeds', error.message);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Summary
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
const os = require('node:os');
|
||||
const fs = require('fs-extra');
|
||||
const yaml = require('yaml');
|
||||
const prompts = require('./prompts');
|
||||
const { getModulePath } = require('./project-root');
|
||||
|
||||
let cachedCoreConfigDefaults = null;
|
||||
|
||||
function getFallbackUsername() {
|
||||
let safeUsername;
|
||||
try {
|
||||
safeUsername = os.userInfo().username;
|
||||
} catch {
|
||||
safeUsername = process.env.USER || process.env.USERNAME || 'User';
|
||||
}
|
||||
|
||||
if (typeof safeUsername !== 'string' || safeUsername.trim() === '') {
|
||||
return 'User';
|
||||
}
|
||||
|
||||
const normalizedUsername = safeUsername.trim();
|
||||
return normalizedUsername.charAt(0).toUpperCase() + normalizedUsername.slice(1);
|
||||
}
|
||||
|
||||
function normalizeDefaultString(value, fallback) {
|
||||
return typeof value === 'string' && value.trim() !== '' ? value.trim() : fallback;
|
||||
}
|
||||
|
||||
function isMissingOrUnresolvedCoreConfigValue(value) {
|
||||
return value == null || (typeof value === 'string' && (value.trim() === '' || /^\{[^}]+\}$/.test(value.trim())));
|
||||
}
|
||||
|
||||
function applyDefaultCoreConfig(coreConfig = {}, defaults = {}) {
|
||||
const normalizedConfig = { ...coreConfig };
|
||||
let appliedDefaults = false;
|
||||
|
||||
for (const [key, value] of Object.entries(defaults)) {
|
||||
if (isMissingOrUnresolvedCoreConfigValue(normalizedConfig[key])) {
|
||||
normalizedConfig[key] = value;
|
||||
appliedDefaults = true;
|
||||
}
|
||||
}
|
||||
|
||||
return { coreConfig: normalizedConfig, appliedDefaults };
|
||||
}
|
||||
|
||||
async function getDefaultCoreConfig() {
|
||||
if (cachedCoreConfigDefaults) {
|
||||
return { ...cachedCoreConfigDefaults };
|
||||
}
|
||||
|
||||
const fallbackDefaults = {
|
||||
user_name: getFallbackUsername(),
|
||||
communication_language: 'English',
|
||||
document_output_language: 'English',
|
||||
output_folder: '_bmad-output',
|
||||
};
|
||||
|
||||
try {
|
||||
const moduleYamlPath = getModulePath('core', 'module.yaml');
|
||||
const moduleConfig = yaml.parse(await fs.readFile(moduleYamlPath, 'utf8')) || {};
|
||||
|
||||
cachedCoreConfigDefaults = {
|
||||
user_name: normalizeDefaultString(moduleConfig.user_name?.default, fallbackDefaults.user_name),
|
||||
communication_language: normalizeDefaultString(moduleConfig.communication_language?.default, fallbackDefaults.communication_language),
|
||||
document_output_language: normalizeDefaultString(
|
||||
moduleConfig.document_output_language?.default,
|
||||
fallbackDefaults.document_output_language,
|
||||
),
|
||||
output_folder: normalizeDefaultString(moduleConfig.output_folder?.default, fallbackDefaults.output_folder),
|
||||
};
|
||||
} catch (error) {
|
||||
await prompts.log.warn(`Failed to load module.yaml, falling back to defaults: ${error.message}`);
|
||||
cachedCoreConfigDefaults = fallbackDefaults;
|
||||
}
|
||||
|
||||
return { ...cachedCoreConfigDefaults };
|
||||
}
|
||||
|
||||
function clearCoreConfigDefaultsCache() {
|
||||
cachedCoreConfigDefaults = null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
applyDefaultCoreConfig,
|
||||
clearCoreConfigDefaultsCache,
|
||||
getDefaultCoreConfig,
|
||||
isMissingOrUnresolvedCoreConfigValue,
|
||||
};
|
||||
|
|
@ -2,6 +2,7 @@ const path = require('node:path');
|
|||
const os = require('node:os');
|
||||
const fs = require('fs-extra');
|
||||
const { CLIUtils } = require('./cli-utils');
|
||||
const { applyDefaultCoreConfig, getDefaultCoreConfig: loadDefaultCoreConfig } = require('./core-config-defaults');
|
||||
const { CustomHandler } = require('../installers/lib/custom/handler');
|
||||
const { ExternalModuleManager } = require('../installers/lib/modules/external-manager');
|
||||
const prompts = require('./prompts');
|
||||
|
|
@ -47,6 +48,21 @@ class UI {
|
|||
}
|
||||
confirmedDirectory = expandedDir;
|
||||
await prompts.log.info(`Using directory from command-line: ${confirmedDirectory}`);
|
||||
} else if (options.yes) {
|
||||
// Default to current directory when --yes flag is set
|
||||
let cwd;
|
||||
try {
|
||||
cwd = process.cwd();
|
||||
} catch (error) {
|
||||
await prompts.log.error(`Failed to resolve current directory (--yes flag): ${error.message}`);
|
||||
throw new Error(`Unable to determine current directory: ${error.message}`);
|
||||
}
|
||||
const validation = this.validateDirectorySync(cwd);
|
||||
if (validation) {
|
||||
throw new Error(`Invalid current directory: ${validation}`);
|
||||
}
|
||||
confirmedDirectory = cwd;
|
||||
await prompts.log.info(`Using current directory (--yes flag): ${confirmedDirectory}`);
|
||||
} else {
|
||||
confirmedDirectory = await this.getConfirmedDirectory();
|
||||
}
|
||||
|
|
@ -823,6 +839,14 @@ class UI {
|
|||
return { existingInstall, installedModuleIds, bmadDir };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default core config values by reading from src/core/module.yaml
|
||||
* @returns {Object} Default core config with user_name, communication_language, document_output_language, output_folder
|
||||
*/
|
||||
async getDefaultCoreConfig() {
|
||||
return loadDefaultCoreConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect core configuration
|
||||
* @param {string} directory - Installation directory
|
||||
|
|
@ -866,27 +890,21 @@ class UI {
|
|||
(!options.userName || !options.communicationLanguage || !options.documentOutputLanguage || !options.outputFolder)
|
||||
) {
|
||||
await configCollector.collectModuleConfig('core', directory, false, true);
|
||||
} else if (options.yes) {
|
||||
const defaults = await this.getDefaultCoreConfig();
|
||||
const normalizedConfig = applyDefaultCoreConfig(configCollector.collectedConfig.core, defaults);
|
||||
configCollector.collectedConfig.core = normalizedConfig.coreConfig;
|
||||
if (normalizedConfig.appliedDefaults) {
|
||||
await prompts.log.info('Using default configuration (--yes flag)');
|
||||
}
|
||||
}
|
||||
} else if (options.yes) {
|
||||
// Use all defaults when --yes flag is set
|
||||
await configCollector.loadExistingConfig(directory);
|
||||
const existingConfig = configCollector.collectedConfig.core || {};
|
||||
|
||||
// If no existing config, use defaults
|
||||
if (Object.keys(existingConfig).length === 0) {
|
||||
let safeUsername;
|
||||
try {
|
||||
safeUsername = os.userInfo().username;
|
||||
} catch {
|
||||
safeUsername = process.env.USER || process.env.USERNAME || 'User';
|
||||
}
|
||||
const defaultUsername = safeUsername.charAt(0).toUpperCase() + safeUsername.slice(1);
|
||||
configCollector.collectedConfig.core = {
|
||||
user_name: defaultUsername,
|
||||
communication_language: 'English',
|
||||
document_output_language: 'English',
|
||||
output_folder: '_bmad-output',
|
||||
};
|
||||
const defaults = await this.getDefaultCoreConfig();
|
||||
const normalizedConfig = applyDefaultCoreConfig(existingConfig, defaults);
|
||||
configCollector.collectedConfig.core = normalizedConfig.coreConfig;
|
||||
if (normalizedConfig.appliedDefaults) {
|
||||
await prompts.log.info('Using default configuration (--yes flag)');
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Reference in New Issue