feat(codex): install BMAD as Codex custom prompts

This commit is contained in:
philosophy 2026-03-11 12:13:13 +08:00
parent 2b809e56a4
commit 5d9a519e3a
9 changed files with 291 additions and 77 deletions

View File

@ -47,12 +47,12 @@ Follow the installer prompts, then open your AI IDE (Claude Code, Cursor, etc.)
**Non-Interactive Installation** (for CI/CD): **Non-Interactive Installation** (for CI/CD):
```bash ```bash
npx bmad-method install --directory /path/to/project --modules bmm --tools claude-code --yes npx bmad-method install --directory /path/to/project --modules bmm --tools codex --yes
``` ```
[See all installation options](https://docs.bmad-method.org/how-to/non-interactive-installation/) [See all installation options](https://docs.bmad-method.org/how-to/non-interactive-installation/)
> **Not sure what to do?** Run `/bmad-help` — it tells you exactly what's next and what's optional. You can also ask questions like `/bmad-help I just finished the architecture, what do I do next?` > **Not sure what to do?** Run `/bmad-help` (Codex CLI: `/prompts:bmad-help`) — it tells you exactly what's next and what's optional. You can also ask questions like `/bmad-help I just finished the architecture, what do I do next?`
## Modules ## Modules

View File

@ -47,12 +47,12 @@ npx bmad-method install
**非交互式安装**(用于 CI/CD **非交互式安装**(用于 CI/CD
```bash ```bash
npx bmad-method install --directory /path/to/project --modules bmm --tools claude-code --yes npx bmad-method install --directory /path/to/project --modules bmm --tools codex --yes
``` ```
[查看所有安装选项](http://docs.bmad-method.org/how-to/non-interactive-installation/) [查看所有安装选项](http://docs.bmad-method.org/how-to/non-interactive-installation/)
> **不确定该做什么?** 运行 `/bmad-help` — 它会准确告诉你下一步做什么以及什么是可选的。你也可以问诸如 `/bmad-help 我刚刚完成了架构设计,接下来该做什么?` 之类的问题。 > **不确定该做什么?** 运行 `/bmad-help`Codex CLI 使用 `/prompts:bmad-help`— 它会准确告诉你下一步做什么以及什么是可选的。你也可以问诸如 `/bmad-help 我刚刚完成了架构设计,接下来该做什么?` 之类的问题。
## 模块 ## 模块

View File

@ -26,7 +26,7 @@ Requires [Node.js](https://nodejs.org) v20+ and `npx` (included with npm).
|------|-------------|---------| |------|-------------|---------|
| `--directory <path>` | Installation directory | `--directory ~/projects/myapp` | | `--directory <path>` | Installation directory | `--directory ~/projects/myapp` |
| `--modules <modules>` | Comma-separated module IDs | `--modules bmm,bmb` | | `--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` | | `--tools <tools>` | Comma-separated tool/IDE IDs (use `none` to skip) | `--tools codex,cursor` or `--tools none` |
| `--custom-content <paths>` | Comma-separated paths to custom modules | `--custom-content ~/my-module,~/another-module` | | `--custom-content <paths>` | Comma-separated paths to custom modules | `--custom-content ~/my-module,~/another-module` |
| `--action <type>` | Action for existing installations: `install` (default), `update`, `quick-update`, or `compile-agents` | `--action quick-update` | | `--action <type>` | Action for existing installations: `install` (default), `update`, `quick-update`, or `compile-agents` | `--action quick-update` |
@ -63,11 +63,14 @@ Available tool IDs for the `--tools` flag:
Run `npx bmad-method install` interactively once to see the full current list of supported tools, or check the [platform codes configuration](https://github.com/bmad-code-org/BMAD-METHOD/blob/main/tools/cli/installers/lib/ide/platform-codes.yaml). Run `npx bmad-method install` interactively once to see the full current list of supported tools, or check the [platform codes configuration](https://github.com/bmad-code-org/BMAD-METHOD/blob/main/tools/cli/installers/lib/ide/platform-codes.yaml).
For `codex`, BMAD installs slash-command prompt files into `.codex/prompts` and syncs them to `~/.codex/prompts`.
Use them in Codex CLI as `/prompts:bmad-help`, `/prompts:bmad-master`, etc.
## Installation Modes ## Installation Modes
| Mode | Description | Example | | Mode | Description | Example |
|------|-------------|---------| |------|-------------|---------|
| Fully non-interactive | Provide all flags to skip all prompts | `npx bmad-method install --directory . --modules bmm --tools claude-code --yes` | | Fully non-interactive | Provide all flags to skip all prompts | `npx bmad-method install --directory . --modules bmm --tools codex --yes` |
| Semi-interactive | Provide some flags; BMad prompts for the rest | `npx bmad-method install --directory . --modules bmm` | | 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` | | 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` | | Without tools | Skip tool/IDE configuration | `npx bmad-method install --modules bmm --tools none` |
@ -83,7 +86,7 @@ Run `npx bmad-method install` interactively once to see the full current list of
npx bmad-method install \ npx bmad-method install \
--directory "${GITHUB_WORKSPACE}" \ --directory "${GITHUB_WORKSPACE}" \
--modules bmm \ --modules bmm \
--tools claude-code \ --tools codex \
--user-name "CI Bot" \ --user-name "CI Bot" \
--communication-language English \ --communication-language English \
--document-output-language English \ --document-output-language English \
@ -115,7 +118,7 @@ npx bmad-method install \
--directory ~/projects/myapp \ --directory ~/projects/myapp \
--modules bmm \ --modules bmm \
--custom-content ~/my-custom-module,~/another-module \ --custom-content ~/my-custom-module,~/another-module \
--tools claude-code --tools codex
``` ```
## What You Get ## What You Get

View File

@ -26,7 +26,7 @@ sidebar:
|------|-------------|---------| |------|-------------|---------|
| `--directory <path>` | 安装目录 | `--directory ~/projects/myapp` | | `--directory <path>` | 安装目录 | `--directory ~/projects/myapp` |
| `--modules <modules>` | 逗号分隔的模块 ID | `--modules bmm,bmb` | | `--modules <modules>` | 逗号分隔的模块 ID | `--modules bmm,bmb` |
| `--tools <tools>` | 逗号分隔的工具/IDE ID使用 `none` 跳过) | `--tools claude-code,cursor` 或 `--tools none` | | `--tools <tools>` | 逗号分隔的工具/IDE ID使用 `none` 跳过) | `--tools codex,cursor` 或 `--tools none` |
| `--custom-content <paths>` | 逗号分隔的自定义模块路径 | `--custom-content ~/my-module,~/another-module` | | `--custom-content <paths>` | 逗号分隔的自定义模块路径 | `--custom-content ~/my-module,~/another-module` |
| `--action <type>` | 对现有安装的操作:`install`(默认)、`update`、`quick-update` 或 `compile-agents` | `--action quick-update` | | `--action <type>` | 对现有安装的操作:`install`(默认)、`update`、`quick-update` 或 `compile-agents` | `--action quick-update` |
@ -63,11 +63,14 @@ sidebar:
运行一次 `npx bmad-method install` 交互式安装以查看完整的当前支持工具列表,或查看 [平台代码配置](https://github.com/bmad-code-org/BMAD-METHOD/blob/main/tools/cli/installers/lib/ide/platform-codes.yaml)。 运行一次 `npx bmad-method install` 交互式安装以查看完整的当前支持工具列表,或查看 [平台代码配置](https://github.com/bmad-code-org/BMAD-METHOD/blob/main/tools/cli/installers/lib/ide/platform-codes.yaml)。
对于 `codex`BMAD 会把 slash 命令提示文件安装到 `.codex/prompts`,并同步到 `~/.codex/prompts`
在 Codex CLI 中用 `/prompts:bmad-help`、`/prompts:bmad-master` 等方式调用。
## 安装模式 ## 安装模式
| 模式 | 描述 | 示例 | | 模式 | 描述 | 示例 |
|------|-------------|---------| |------|-------------|---------|
| 完全非交互式 | 提供所有标志以跳过所有提示 | `npx bmad-method install --directory . --modules bmm --tools claude-code --yes` | | 完全非交互式 | 提供所有标志以跳过所有提示 | `npx bmad-method install --directory . --modules bmm --tools codex --yes` |
| 半交互式 | 提供部分标志BMad 提示其余部分 | `npx bmad-method install --directory . --modules bmm` | | 半交互式 | 提供部分标志BMad 提示其余部分 | `npx bmad-method install --directory . --modules bmm` |
| 仅使用默认值 | 使用 `-y` 接受所有默认值 | `npx bmad-method install --yes` | | 仅使用默认值 | 使用 `-y` 接受所有默认值 | `npx bmad-method install --yes` |
| 不包含工具 | 跳过工具/IDE 配置 | `npx bmad-method install --modules bmm --tools none` | | 不包含工具 | 跳过工具/IDE 配置 | `npx bmad-method install --modules bmm --tools none` |
@ -83,7 +86,7 @@ sidebar:
npx bmad-method install \ npx bmad-method install \
--directory "${GITHUB_WORKSPACE}" \ --directory "${GITHUB_WORKSPACE}" \
--modules bmm \ --modules bmm \
--tools claude-code \ --tools codex \
--user-name "CI Bot" \ --user-name "CI Bot" \
--communication-language English \ --communication-language English \
--document-output-language English \ --document-output-language English \
@ -115,7 +118,7 @@ npx bmad-method install \
--directory ~/projects/myapp \ --directory ~/projects/myapp \
--modules bmm \ --modules bmm \
--custom-content ~/my-custom-module,~/another-module \ --custom-content ~/my-custom-module,~/another-module \
--tools claude-code --tools codex
``` ```
## 安装结果 ## 安装结果

View File

@ -54,6 +54,13 @@ async function createTestBmadFixture() {
// Minimal workflow manifest (generators check for this) // Minimal workflow manifest (generators check for this)
await fs.ensureDir(path.join(fixtureDir, '_config')); await fs.ensureDir(path.join(fixtureDir, '_config'));
await fs.writeFile(path.join(fixtureDir, '_config', 'workflow-manifest.csv'), ''); await fs.writeFile(path.join(fixtureDir, '_config', 'workflow-manifest.csv'), '');
await fs.writeFile(
path.join(fixtureDir, '_config', 'skill-manifest.csv'),
[
'canonicalId,name,description,module,path,install_to_bmad',
'"bmad-help","bmad-help","Help workflow","core","_bmad/core/tasks/bmad-help/SKILL.md","true"',
].join('\n'),
);
// Minimal compiled agent for core/agents (contains <agent tag and frontmatter) // Minimal compiled agent for core/agents (contains <agent tag and frontmatter)
const minimalAgent = [ const minimalAgent = [
@ -78,6 +85,16 @@ async function createTestBmadFixture() {
await fs.ensureDir(path.join(fixtureDir, 'bmm', 'agents')); await fs.ensureDir(path.join(fixtureDir, 'bmm', 'agents'));
await fs.writeFile(path.join(fixtureDir, 'bmm', 'agents', 'test-bmm-agent.md'), minimalAgent); await fs.writeFile(path.join(fixtureDir, 'bmm', 'agents', 'test-bmm-agent.md'), minimalAgent);
// Minimal skill fixture (referenced by skill-manifest.csv)
await fs.ensureDir(path.join(fixtureDir, 'core', 'tasks', 'bmad-help'));
await fs.writeFile(
path.join(fixtureDir, 'core', 'tasks', 'bmad-help', 'SKILL.md'),
['---', 'name: bmad-help', 'description: Help workflow', '---', '', 'Follow the instructions in [workflow.md](workflow.md).', ''].join(
'\n',
),
);
await fs.writeFile(path.join(fixtureDir, 'core', 'tasks', 'bmad-help', 'workflow.md'), '# Help workflow\n');
return fixtureDir; return fixtureDir;
} }
@ -550,29 +567,38 @@ async function runTests() {
console.log(''); console.log('');
// ============================================================ // ============================================================
// Test 11: Codex Native Skills Install // Test 11: Codex Slash Commands Install
// ============================================================ // ============================================================
console.log(`${colors.yellow}Test Suite 11: Codex Native Skills${colors.reset}\n`); console.log(`${colors.yellow}Test Suite 11: Codex Slash Commands${colors.reset}\n`);
let originalHome11 = process.env.HOME;
let tempHome11 = null;
try { try {
clearCache(); clearCache();
const platformCodes11 = await loadPlatformCodes(); const platformCodes11 = await loadPlatformCodes();
const codexInstaller = platformCodes11.platforms.codex?.installer; const codexInstaller = platformCodes11.platforms.codex?.installer;
assert(codexInstaller?.target_dir === '.agents/skills', 'Codex target_dir uses native skills path'); assert(codexInstaller?.target_dir === '.codex/prompts', 'Codex target_dir uses slash command prompt path');
assert(codexInstaller?.skill_format === true, 'Codex installer enables native skill output'); assert(codexInstaller?.skill_format !== true, 'Codex installer uses flat prompt file output');
assert(codexInstaller?.ancestor_conflict_check === true, 'Codex installer enables ancestor conflict checks'); assert(codexInstaller?.ancestor_conflict_check === true, 'Codex installer enables ancestor conflict checks');
assert( assert(
Array.isArray(codexInstaller?.legacy_targets) && codexInstaller.legacy_targets.includes('.codex/prompts'), Array.isArray(codexInstaller?.legacy_targets) && codexInstaller.legacy_targets.includes('.agents/skills'),
'Codex installer cleans legacy prompt output', 'Codex installer cleans legacy skills output',
);
assert(
Array.isArray(codexInstaller?.sync_targets) && codexInstaller.sync_targets.includes('~/.codex/prompts'),
'Codex installer syncs prompts to user codex prompt directory',
); );
const tempProjectDir11 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-codex-test-')); const tempProjectDir11 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-codex-test-'));
tempHome11 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-codex-home-'));
process.env.HOME = tempHome11;
const installedBmadDir11 = await createTestBmadFixture(); const installedBmadDir11 = await createTestBmadFixture();
const legacyDir11 = path.join(tempProjectDir11, '.codex', 'prompts'); const legacyDir11 = path.join(tempProjectDir11, '.agents', 'skills');
await fs.ensureDir(legacyDir11); await fs.ensureDir(legacyDir11);
await fs.writeFile(path.join(legacyDir11, 'bmad-legacy.md'), 'legacy\n'); await fs.writeFile(path.join(legacyDir11, 'bmad-legacy.md'), 'legacy\n');
@ -585,20 +611,34 @@ async function runTests() {
assert(result11.success === true, 'Codex setup succeeds against temp project'); assert(result11.success === true, 'Codex setup succeeds against temp project');
const skillFile11 = path.join(tempProjectDir11, '.agents', 'skills', 'bmad-master', 'SKILL.md'); const promptFile11 = path.join(tempProjectDir11, '.codex', 'prompts', 'bmad-master.md');
assert(await fs.pathExists(skillFile11), 'Codex install writes SKILL.md directory output'); assert(await fs.pathExists(promptFile11), 'Codex install writes slash command prompt files');
// Verify name frontmatter matches directory name const promptContent11 = await fs.readFile(promptFile11, 'utf8');
const skillContent11 = await fs.readFile(skillFile11, 'utf8'); const nameMatch11 = promptContent11.match(/^name:\s*['"]?bmad-master['"]?\s*$/m);
const nameMatch11 = skillContent11.match(/^name:\s*(.+)$/m); assert(nameMatch11, 'Codex prompt frontmatter keeps bmad command name');
assert(nameMatch11 && nameMatch11[1].trim() === 'bmad-master', 'Codex skill name frontmatter matches directory name exactly');
assert(!(await fs.pathExists(legacyDir11)), 'Codex setup removes legacy prompts dir'); const bmadHelpPrompt11 = path.join(tempProjectDir11, '.codex', 'prompts', 'bmad-help.md');
assert(await fs.pathExists(bmadHelpPrompt11), 'Codex install converts skill-manifest entries into prompt commands');
const globalPrompt11 = path.join(tempHome11, '.codex', 'prompts', 'bmad-master.md');
assert(await fs.pathExists(globalPrompt11), 'Codex install syncs prompt commands to ~/.codex/prompts');
const globalHelpPrompt11 = path.join(tempHome11, '.codex', 'prompts', 'bmad-help.md');
assert(await fs.pathExists(globalHelpPrompt11), 'Codex global sync includes bmad-help prompt');
assert(!(await fs.pathExists(legacyDir11)), 'Codex setup removes legacy skills dir');
await fs.remove(tempProjectDir11); await fs.remove(tempProjectDir11);
await fs.remove(tempHome11);
await fs.remove(installedBmadDir11); await fs.remove(installedBmadDir11);
} catch (error) { } catch (error) {
assert(false, 'Codex native skills migration test succeeds', error.message); assert(false, 'Codex slash command migration test succeeds', error.message);
} finally {
process.env.HOME = originalHome11;
if (tempHome11) {
await fs.remove(tempHome11);
}
} }
console.log(''); console.log('');
@ -615,9 +655,9 @@ async function runTests() {
const installedBmadDir12 = await createTestBmadFixture(); const installedBmadDir12 = await createTestBmadFixture();
await fs.ensureDir(path.join(parentProjectDir12, '.git')); await fs.ensureDir(path.join(parentProjectDir12, '.git'));
await fs.ensureDir(path.join(parentProjectDir12, '.agents', 'skills', 'bmad-existing')); await fs.ensureDir(path.join(parentProjectDir12, '.codex', 'prompts'));
await fs.ensureDir(childProjectDir12); await fs.ensureDir(childProjectDir12);
await fs.writeFile(path.join(parentProjectDir12, '.agents', 'skills', 'bmad-existing', 'SKILL.md'), 'legacy\n'); await fs.writeFile(path.join(parentProjectDir12, '.codex', 'prompts', 'bmad-existing.md'), 'legacy\n');
const ideManager12 = new IdeManager(); const ideManager12 = new IdeManager();
await ideManager12.ensureInitialized(); await ideManager12.ensureInitialized();
@ -625,11 +665,11 @@ async function runTests() {
silent: true, silent: true,
selectedModules: ['bmm'], selectedModules: ['bmm'],
}); });
const expectedConflictDir12 = await fs.realpath(path.join(parentProjectDir12, '.agents', 'skills')); const expectedConflictDir12 = await fs.realpath(path.join(parentProjectDir12, '.codex', 'prompts'));
assert(result12.success === false, 'Codex setup refuses install when ancestor skills already exist'); assert(result12.success === false, 'Codex setup refuses install when ancestor prompts already exist');
assert(result12.handlerResult?.reason === 'ancestor-conflict', 'Codex ancestor rejection reports ancestor-conflict reason'); assert(result12.handlerResult?.reason === 'ancestor-conflict', 'Codex ancestor rejection reports ancestor-conflict reason');
assert(result12.handlerResult?.conflictDir === expectedConflictDir12, 'Codex ancestor rejection points at ancestor .agents/skills dir'); assert(result12.handlerResult?.conflictDir === expectedConflictDir12, 'Codex ancestor rejection points at ancestor .codex/prompts dir');
await fs.remove(tempRoot12); await fs.remove(tempRoot12);
await fs.remove(installedBmadDir12); await fs.remove(installedBmadDir12);

View File

@ -43,7 +43,7 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
*/ */
async detect(projectDir) { async detect(projectDir) {
if (this.installerConfig?.skill_format && this.configDir) { if (this.installerConfig?.skill_format && this.configDir) {
const dir = path.join(projectDir || process.cwd(), this.configDir); const dir = this.resolveTargetPath(projectDir || process.cwd(), this.configDir);
if (await fs.pathExists(dir)) { if (await fs.pathExists(dir)) {
try { try {
const entries = await fs.readdir(dir); const entries = await fs.readdir(dir);
@ -70,8 +70,8 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
const conflict = await this.findAncestorConflict(projectDir); const conflict = await this.findAncestorConflict(projectDir);
if (conflict) { if (conflict) {
await prompts.log.error( await prompts.log.error(
`Found existing BMAD skills in ancestor installation: ${conflict}\n` + `Found existing BMAD files in ancestor installation: ${conflict}\n` +
` ${this.name} inherits skills from parent directories, so this would cause duplicates.\n` + ` ${this.name} inherits commands from parent directories, so this would cause duplicates.\n` +
` Please remove the BMAD files from that directory first:\n` + ` Please remove the BMAD files from that directory first:\n` +
` rm -rf "${conflict}"/bmad*`, ` rm -rf "${conflict}"/bmad*`,
); );
@ -120,11 +120,11 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
// Skip targets with explicitly empty artifact_types and no verbatim skills // Skip targets with explicitly empty artifact_types and no verbatim skills
// This prevents creating empty directories when no artifacts will be written // This prevents creating empty directories when no artifacts will be written
const skipStandardArtifacts = Array.isArray(artifact_types) && artifact_types.length === 0; const skipStandardArtifacts = Array.isArray(artifact_types) && artifact_types.length === 0;
if (skipStandardArtifacts && !config.skill_format) { if (skipStandardArtifacts && !config.skill_format && !config.install_skill_manifest_as_artifacts) {
return { success: true, results: { agents: 0, workflows: 0, tasks: 0, tools: 0, skills: 0 } }; return { success: true, results: { agents: 0, workflows: 0, tasks: 0, tools: 0, skills: 0 } };
} }
const targetPath = path.join(projectDir, target_dir); const targetPath = this.resolveTargetPath(projectDir, target_dir);
await this.ensureDir(targetPath); await this.ensureDir(targetPath);
const selectedModules = options.selectedModules || []; const selectedModules = options.selectedModules || [];
@ -156,9 +156,19 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
} }
} }
// Install verbatim skills (type: skill)
if (config.install_skill_manifest_as_artifacts) {
results.skills += await this.installSkillManifestArtifacts(targetPath, bmadDir, template_type, config);
}
// Install verbatim skills (type: skill) // Install verbatim skills (type: skill)
if (config.skill_format) { if (config.skill_format) {
results.skills = await this.installVerbatimSkills(projectDir, bmadDir, targetPath, config); results.skills += await this.installVerbatimSkills(projectDir, bmadDir, targetPath, config);
}
// Optionally mirror generated BMAD commands to additional directories.
if (Array.isArray(config.sync_targets) && config.sync_targets.length > 0) {
await this.syncBmadArtifacts(targetPath, projectDir, config.sync_targets, options);
} }
await this.printSummary(results, target_dir, options); await this.printSummary(results, target_dir, options);
@ -638,15 +648,8 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
async installVerbatimSkills(projectDir, bmadDir, targetPath, config) { async installVerbatimSkills(projectDir, bmadDir, targetPath, config) {
const bmadFolderName = path.basename(bmadDir); const bmadFolderName = path.basename(bmadDir);
const bmadPrefix = bmadFolderName + '/'; const bmadPrefix = bmadFolderName + '/';
const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv'); const records = await this.loadSkillManifestRecords(bmadDir);
if (!records || records.length === 0) return 0;
if (!(await fs.pathExists(csvPath))) return 0;
const csvContent = await fs.readFile(csvPath, 'utf8');
const records = csv.parse(csvContent, {
columns: true,
skip_empty_lines: true,
});
let count = 0; let count = 0;
@ -699,6 +702,119 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
return count; return count;
} }
/**
* Load skill-manifest CSV records.
* @param {string} bmadDir - BMAD installation directory
* @returns {Promise<Array>} Parsed CSV records
*/
async loadSkillManifestRecords(bmadDir) {
const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv');
if (!(await fs.pathExists(csvPath))) return [];
const csvContent = await fs.readFile(csvPath, 'utf8');
return csv.parse(csvContent, {
columns: true,
skip_empty_lines: true,
});
}
/**
* Install skill-manifest entries as flat command files.
* This is useful for platforms that support slash-command prompts but not
* native skill directory formats.
* @param {string} targetPath - Target directory path
* @param {string} bmadDir - BMAD installation directory
* @param {string} templateType - Template type to use
* @param {Object} config - Installation configuration
* @returns {Promise<number>} Count of skills written as command files
*/
async installSkillManifestArtifacts(targetPath, bmadDir, templateType, config = {}) {
const records = await this.loadSkillManifestRecords(bmadDir);
if (!records || records.length === 0) return 0;
const { content: template, extension } = await this.loadTemplate(templateType, 'skill', config, 'default-skill');
const bmadPrefix = `${this.bmadFolderName}/`;
let count = 0;
for (const record of records) {
const canonicalId = record?.canonicalId;
if (!canonicalId) continue;
let relativePath = String(record.path || '').replaceAll('\\', '/');
if (relativePath.startsWith(bmadPrefix)) {
relativePath = relativePath.slice(bmadPrefix.length);
} else if (relativePath.startsWith('_bmad/')) {
relativePath = relativePath.slice(6);
} else if (relativePath.startsWith('bmad/')) {
relativePath = relativePath.slice(5);
}
const artifact = {
type: 'skill',
name: canonicalId,
module: record.module || 'core',
description: record.description || `${canonicalId} skill`,
path: relativePath,
relativePath,
canonicalId,
};
const content = this.renderTemplate(template, artifact);
const filename = this.generateFilename(artifact, 'skill', extension);
const filePath = path.join(targetPath, filename);
await this.writeFile(filePath, content);
count++;
}
return count;
}
/**
* Mirror generated BMAD files into additional target directories.
* Only BMAD-prefixed entries are synced to avoid touching user files.
* @param {string} sourcePath - Primary target directory
* @param {string} projectDir - Project directory
* @param {Array<string>} syncTargets - Additional target directories
* @param {Object} options - Setup options
*/
async syncBmadArtifacts(sourcePath, projectDir, syncTargets, options = {}) {
let sourceEntries = [];
try {
sourceEntries = await fs.readdir(sourcePath);
} catch {
return;
}
const bmadEntries = sourceEntries.filter((entry) => typeof entry === 'string' && entry.startsWith('bmad'));
if (bmadEntries.length === 0) return;
for (const syncTarget of syncTargets) {
const targetPath = this.resolveTargetPath(projectDir, syncTarget);
if (path.resolve(targetPath) === path.resolve(sourcePath)) continue;
await this.ensureDir(targetPath);
// Remove stale BMAD entries before sync so mirrored directories stay exact.
const existing = await fs.readdir(targetPath);
for (const entry of existing) {
if (typeof entry === 'string' && entry.startsWith('bmad') && !entry.startsWith('bmad-os-')) {
await fs.remove(path.join(targetPath, entry));
}
}
for (const entry of bmadEntries) {
await fs.copy(path.join(sourcePath, entry), path.join(targetPath, entry), { overwrite: true });
}
if (!options.silent) {
await prompts.log.message(` Synced BMAD files to ${syncTarget}`);
}
}
}
/** /**
* Print installation summary * Print installation summary
* @param {Object} results - Installation results * @param {Object} results - Installation results
@ -766,6 +882,13 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
} else if (this.installerConfig?.target_dir) { } else if (this.installerConfig?.target_dir) {
await this.cleanupTarget(projectDir, this.installerConfig.target_dir, options); await this.cleanupTarget(projectDir, this.installerConfig.target_dir, options);
} }
// Clean any mirrored targets used for command synchronization.
if (Array.isArray(this.installerConfig?.sync_targets)) {
for (const syncTarget of this.installerConfig.sync_targets) {
await this.cleanupTarget(projectDir, syncTarget, options);
}
}
} }
/** /**
@ -777,6 +900,21 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
return p.startsWith('~') || path.isAbsolute(p); return p.startsWith('~') || path.isAbsolute(p);
} }
/**
* Resolve a configured target path to an absolute path.
* Supports project-relative, absolute, and ~/home-relative targets.
* @param {string} projectDir - Project directory
* @param {string} targetDir - Configured target directory
* @returns {string} Absolute resolved path
*/
resolveTargetPath(projectDir, targetDir) {
if (!targetDir) return projectDir;
if (targetDir === '~') return os.homedir();
if (targetDir.startsWith('~/')) return path.join(os.homedir(), targetDir.slice(2));
if (path.isAbsolute(targetDir)) return targetDir;
return path.join(projectDir, targetDir);
}
/** /**
* Warn about stale BMAD files in a global legacy directory (never auto-deletes) * Warn about stale BMAD files in a global legacy directory (never auto-deletes)
* @param {string} legacyDir - Legacy directory path (may start with ~) * @param {string} legacyDir - Legacy directory path (may start with ~)
@ -809,7 +947,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
* @param {string} targetDir - Target directory to clean * @param {string} targetDir - Target directory to clean
*/ */
async cleanupTarget(projectDir, targetDir, options = {}) { async cleanupTarget(projectDir, targetDir, options = {}) {
const targetPath = path.join(projectDir, targetDir); const targetPath = this.resolveTargetPath(projectDir, targetDir);
if (!(await fs.pathExists(targetPath))) { if (!(await fs.pathExists(targetPath))) {
return; return;
@ -984,29 +1122,46 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
* @returns {Promise<string|null>} Path to conflicting directory, or null if clean * @returns {Promise<string|null>} Path to conflicting directory, or null if clean
*/ */
async findAncestorConflict(projectDir) { async findAncestorConflict(projectDir) {
const targetDir = this.installerConfig?.target_dir; const targetDirs = [];
if (!targetDir) return null; if (this.installerConfig?.target_dir) {
targetDirs.push(this.installerConfig.target_dir);
}
if (Array.isArray(this.installerConfig?.targets)) {
for (const target of this.installerConfig.targets) {
if (target?.target_dir) targetDirs.push(target.target_dir);
}
}
if (targetDirs.length === 0) return null;
const resolvedProject = await fs.realpath(path.resolve(projectDir)); const resolvedProject = await fs.realpath(path.resolve(projectDir));
let current = path.dirname(resolvedProject);
const root = path.parse(current).root;
while (current !== root && current.length > root.length) { for (const targetDir of targetDirs) {
const candidatePath = path.join(current, targetDir); // Ancestor conflicts only apply to project-relative directories.
try { if (this.isGlobalPath(targetDir) || path.isAbsolute(targetDir)) {
if (await fs.pathExists(candidatePath)) { continue;
const entries = await fs.readdir(candidatePath); }
const hasBmad = entries.some(
(e) => typeof e === 'string' && e.toLowerCase().startsWith('bmad') && !e.toLowerCase().startsWith('bmad-os-'), let current = path.dirname(resolvedProject);
); const root = path.parse(current).root;
if (hasBmad) {
return candidatePath; while (current !== root && current.length > root.length) {
} const candidatePath = path.join(current, targetDir);
} try {
} catch { if (await fs.pathExists(candidatePath)) {
// Can't read directory — skip const entries = await fs.readdir(candidatePath);
const hasBmad = entries.some(
(e) => typeof e === 'string' && e.toLowerCase().startsWith('bmad') && !e.toLowerCase().startsWith('bmad-os-'),
);
if (hasBmad) {
return candidatePath;
}
}
} catch {
// Can't read directory — skip
}
current = path.dirname(current);
} }
current = path.dirname(current);
} }
return null; return null;

View File

@ -70,13 +70,14 @@ platforms:
description: "OpenAI Codex integration" description: "OpenAI Codex integration"
installer: installer:
legacy_targets: legacy_targets:
- .codex/prompts - .agents/skills
- ~/.codex/prompts target_dir: .codex/prompts
target_dir: .agents/skills
template_type: default template_type: default
skill_format: true
ancestor_conflict_check: true ancestor_conflict_check: true
artifact_types: [agents, workflows, tasks] artifact_types: [agents, workflows, tasks]
install_skill_manifest_as_artifacts: true
sync_targets:
- ~/.codex/prompts
codebuddy: codebuddy:
name: "CodeBuddy" name: "CodeBuddy"

View File

@ -0,0 +1,11 @@
---
name: '{{name}}'
description: '{{description}}'
---
# {{name}}
Read the entire skill file at: {project-root}/{{bmadFolderName}}/{{path}}
Follow all instructions in the skill file exactly as written.
If the skill references additional files (for example `workflow.md`), load and follow those as well.

View File

@ -7,7 +7,7 @@ Scope: migrate the BMAD-supported platforms that fully support the Agent Skills
Current branch status: Current branch status:
- `Claude Code` has already been moved to `.claude/skills` - `Claude Code` has already been moved to `.claude/skills`
- `Codex CLI` has already been moved to `.agents/skills` - `Codex CLI` now uses `.codex/prompts` slash commands (not Agent Skills format)
This checklist now includes those completed platforms plus the remaining full-support platforms. This checklist now includes those completed platforms plus the remaining full-support platforms.
@ -26,15 +26,16 @@ Support assumption: full Agent Skills support. BMAD has already migrated from `.
## Codex CLI ## Codex CLI
Support assumption: full Agent Skills support. BMAD has already migrated from `.codex/prompts` to `.agents/skills`. Support assumption: Codex custom slash commands are file-based prompts. BMAD targets `.codex/prompts` and syncs to `~/.codex/prompts` for Codex discovery.
**Install:** `npm install -g @openai/codex` **Install:** `npm install -g @openai/codex`
- [x] Confirm current implementation still matches Codex CLI skills expectations - [x] Confirm current implementation matches Codex CLI prompt command expectations
- [x] Confirm legacy cleanup for project and global `.codex/prompts` - [x] Ensure skill-manifest entries (for example `bmad-help`) are also emitted as prompt commands
- [x] Confirm legacy cleanup for prior project-local `.agents/skills` output and global `~/.codex/prompts` warnings
- [x] Test fresh install - [x] Test fresh install
- [x] Test reinstall/upgrade from legacy prompt output - [x] Test reinstall/upgrade from legacy skills output
- [x] Confirm ancestor conflict protection because Codex inherits parent-directory `.agents/skills` - [x] Confirm ancestor conflict protection because Codex can inherit parent-directory `.codex/prompts`
- [x] Implement/extend automated tests as needed - [x] Implement/extend automated tests as needed
## Cursor ## Cursor