mirror of
https://github.com/bmad-code-org/BMAD-METHOD.git
synced 2026-09-19 08:11:52 +08:00
4405b817a9
* refactor: remove bmad-skill-manifest yaml; introduce four-layer central config.toml
- Agent essence moves from per-skill bmad-skill-manifest.yaml files
into each module.yaml's `agents:` block (code, name, title, icon,
description). Per-agent customize.toml remains the deep-behavior
source of truth.
- Installer emits four TOML files:
_bmad/config.toml team install answers + agent roster
_bmad/config.user.toml user install answers
_bmad/custom/config.toml team overrides stub
_bmad/custom/config.user.toml personal overrides stub
Prompts declare scope: user to route answers to config.user.toml.
- resolve_config.py merges four layers: base-team -> base-user ->
custom-team -> custom-user.
- Three consumer skills (party-mode, advanced-elicitation,
retrospective) switched from agent-manifest.csv to the resolver.
- installer.js mergeModuleHelpCatalogs now takes the in-memory
agent list from ManifestGenerator -- no CSV roundtrip.
- Deleted: 6 bmad-skill-manifest.yaml files, agent-manifest.csv
emission, collectAgents/getAgentsFromDirRecursive,
paths.agentManifest().
* fix(installer): strip core-key pollution from [modules.*]; soften config headers
- writeCentralConfig now always strips core-module keys from every
[modules.<code>] bucket, even when the module's schema is not
available in src/ (external / marketplace modules like cis, bmb).
Core values belong in [core] only; workflows read them directly.
- When the module's own schema IS available (built-in modules),
also drop any key it does not declare as a prompt — same
spread-pollution filter as before, now layered on top.
- Section-aware headers on both _bmad/config.toml and
_bmad/config.user.toml: [core] / [modules.*] values are
editable (installer reads them as defaults on next install);
[agents.*] is regenerated from module.yaml and will be wiped —
overrides for agents go in _bmad/custom/config*.toml instead.
* docs: cover central config.toml + Diataxis prose pass across three files
Document the new four-file central configuration surface (_bmad/config.toml,
config.user.toml, and custom/ overrides) alongside the existing per-skill
customize.toml. Make editing rules, scope partitioning, and when-to-use-which
guidance explicit.
- customize-bmad.md: new "Central Configuration" section with editing rules,
three worked examples (rebrand, fictional agent, module settings override),
and a "when to use which surface" table. Converted five h4 headers to
bold paragraph intros per style guide.
- expand-bmad-for-your-org.md: two-layer mental model extended to three;
new Recipe 5 with three variants (rebrand, custom crew, pinned team
settings); reinforcement table extended.
- named-agents.md: noted the dual customization surface — per-skill shapes
behavior, central config shapes roster identity.
Diataxis prose pass applied across all three files: banned vocabulary
check, em-dash cap, hypophora / metanoia / amplificatio / stakes-inflation
cleanup, rhythm and burstiness fixes. Structural conformance verified;
markdownlint and prettier clean.
* test+docs: add central config unit tests; fix stale recipe count
- test: two new suites (35 + 36) covering writeCentralConfig and
ensureCustomConfigStubs. Verifies scope partitioning (user_name
lands only in config.user.toml), core-key pollution stripping
from [modules.*], unknown-schema fallthrough (external modules
survive without schema), agent roster baked into config.toml
[agents.*] only, stub-preservation on re-install. 44 new
assertions.
- docs: fixed four stale "four recipes" references to say "five"
after Recipe 5 (Customize the Agent Roster) was added. Touches
frontmatter, opening paragraph, Combining Recipes paragraph,
and the named-agents cross-link blurb.
* fix: address PR review feedback on central config
- resolve_config.py argparse: three-layer → four-layer description
- SKILL/workflow/explanation docs: document all four layers including
_bmad/config.user.toml (was missing from merge-stack descriptions)
- customize-bmad.md + installer headers: drop the false "direct edits to
config.toml persist" claim; installer reads from per-module config.yaml,
not central TOML, so direct edits get clobbered. Route users to
_bmad/custom/config.toml for durable overrides
- writeCentralConfig: warn loudly when a module.yaml can't be parsed
(previously silent — user-scoped keys could mis-file into team config)
- writeCentralConfig: preserve [agents.*] blocks for modules that didn't
contribute fresh agents this run (e.g. quickUpdate skipping modules
whose source is unavailable) so the roster doesn't silently shrink
- add extractAgentBlocks helper + Test Suite 37 covering preservation
Addresses comments from augmentcode and coderabbitai on PR #2285.
133 lines
3.6 KiB
JavaScript
133 lines
3.6 KiB
JavaScript
const path = require('node:path');
|
|
const fs = require('../fs-native');
|
|
const { getProjectRoot } = require('../project-root');
|
|
const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');
|
|
|
|
class InstallPaths {
|
|
static async create(config) {
|
|
const srcDir = getProjectRoot();
|
|
await assertReadableDir(srcDir, 'BMAD source root');
|
|
|
|
const pkgPath = path.join(srcDir, 'package.json');
|
|
await assertReadableFile(pkgPath, 'package.json');
|
|
const version = require(pkgPath).version;
|
|
|
|
const projectRoot = path.resolve(config.directory);
|
|
await ensureWritableDir(projectRoot, 'project root');
|
|
|
|
const bmadDir = path.join(projectRoot, BMAD_FOLDER_NAME);
|
|
const isUpdate = await fs.pathExists(bmadDir);
|
|
|
|
const configDir = path.join(bmadDir, '_config');
|
|
const coreDir = path.join(bmadDir, 'core');
|
|
const scriptsDir = path.join(bmadDir, 'scripts');
|
|
const customDir = path.join(bmadDir, 'custom');
|
|
|
|
for (const [dir, label] of [
|
|
[bmadDir, 'bmad directory'],
|
|
[configDir, 'config directory'],
|
|
[coreDir, 'core module directory'],
|
|
[scriptsDir, 'shared scripts directory'],
|
|
[customDir, 'customizations directory'],
|
|
]) {
|
|
await ensureWritableDir(dir, label);
|
|
}
|
|
|
|
return new InstallPaths({
|
|
srcDir,
|
|
version,
|
|
projectRoot,
|
|
bmadDir,
|
|
configDir,
|
|
coreDir,
|
|
scriptsDir,
|
|
customDir,
|
|
isUpdate,
|
|
});
|
|
}
|
|
|
|
constructor(props) {
|
|
Object.assign(this, props);
|
|
Object.freeze(this);
|
|
}
|
|
|
|
manifestFile() {
|
|
return path.join(this.configDir, 'manifest.yaml');
|
|
}
|
|
centralConfig() {
|
|
return path.join(this.bmadDir, 'config.toml');
|
|
}
|
|
centralUserConfig() {
|
|
return path.join(this.bmadDir, 'config.user.toml');
|
|
}
|
|
filesManifest() {
|
|
return path.join(this.configDir, 'files-manifest.csv');
|
|
}
|
|
helpCatalog() {
|
|
return path.join(this.configDir, 'bmad-help.csv');
|
|
}
|
|
moduleDir(name) {
|
|
return path.join(this.bmadDir, name);
|
|
}
|
|
moduleConfig(name) {
|
|
return path.join(this.bmadDir, name, 'config.yaml');
|
|
}
|
|
}
|
|
|
|
async function assertReadableDir(dirPath, label) {
|
|
const stat = await fs.stat(dirPath).catch(() => null);
|
|
if (!stat) {
|
|
throw new Error(`${label} does not exist: ${dirPath}`);
|
|
}
|
|
if (!stat.isDirectory()) {
|
|
throw new Error(`${label} is not a directory: ${dirPath}`);
|
|
}
|
|
try {
|
|
await fs.access(dirPath, fs.constants.R_OK);
|
|
} catch {
|
|
throw new Error(`${label} is not readable: ${dirPath}`);
|
|
}
|
|
}
|
|
|
|
async function assertReadableFile(filePath, label) {
|
|
const stat = await fs.stat(filePath).catch(() => null);
|
|
if (!stat) {
|
|
throw new Error(`${label} does not exist: ${filePath}`);
|
|
}
|
|
if (!stat.isFile()) {
|
|
throw new Error(`${label} is not a file: ${filePath}`);
|
|
}
|
|
try {
|
|
await fs.access(filePath, fs.constants.R_OK);
|
|
} catch {
|
|
throw new Error(`${label} is not readable: ${filePath}`);
|
|
}
|
|
}
|
|
|
|
async function ensureWritableDir(dirPath, label) {
|
|
const stat = await fs.stat(dirPath).catch(() => null);
|
|
if (stat && !stat.isDirectory()) {
|
|
throw new Error(`${label} exists but is not a directory: ${dirPath}`);
|
|
}
|
|
|
|
try {
|
|
await fs.ensureDir(dirPath);
|
|
} catch (error) {
|
|
if (error.code === 'EACCES') {
|
|
throw new Error(`${label}: permission denied creating directory: ${dirPath}`);
|
|
}
|
|
if (error.code === 'ENOSPC') {
|
|
throw new Error(`${label}: no space left on device: ${dirPath}`);
|
|
}
|
|
throw new Error(`${label}: cannot create directory: ${dirPath} (${error.message})`);
|
|
}
|
|
|
|
try {
|
|
await fs.access(dirPath, fs.constants.R_OK | fs.constants.W_OK);
|
|
} catch {
|
|
throw new Error(`${label} is not writable: ${dirPath}`);
|
|
}
|
|
}
|
|
|
|
module.exports = { InstallPaths };
|