feat(codex): install BMAD as Codex custom prompts
This commit is contained in:
parent
2b809e56a4
commit
5d9a519e3a
|
|
@ -47,12 +47,12 @@ Follow the installer prompts, then open your AI IDE (Claude Code, Cursor, etc.)
|
|||
**Non-Interactive Installation** (for CI/CD):
|
||||
|
||||
```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/)
|
||||
|
||||
> **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
|
||||
|
||||
|
|
|
|||
|
|
@ -47,12 +47,12 @@ npx bmad-method install
|
|||
**非交互式安装**(用于 CI/CD):
|
||||
|
||||
```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/)
|
||||
|
||||
> **不确定该做什么?** 运行 `/bmad-help` — 它会准确告诉你下一步做什么以及什么是可选的。你也可以问诸如 `/bmad-help 我刚刚完成了架构设计,接下来该做什么?` 之类的问题。
|
||||
> **不确定该做什么?** 运行 `/bmad-help`(Codex CLI 使用 `/prompts:bmad-help`)— 它会准确告诉你下一步做什么以及什么是可选的。你也可以问诸如 `/bmad-help 我刚刚完成了架构设计,接下来该做什么?` 之类的问题。
|
||||
|
||||
## 模块
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ Requires [Node.js](https://nodejs.org) v20+ and `npx` (included with npm).
|
|||
|------|-------------|---------|
|
||||
| `--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` |
|
||||
| `--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` |
|
||||
| `--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).
|
||||
|
||||
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
|
||||
|
||||
| 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` |
|
||||
| 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` |
|
||||
|
|
@ -83,7 +86,7 @@ Run `npx bmad-method install` interactively once to see the full current list of
|
|||
npx bmad-method install \
|
||||
--directory "${GITHUB_WORKSPACE}" \
|
||||
--modules bmm \
|
||||
--tools claude-code \
|
||||
--tools codex \
|
||||
--user-name "CI Bot" \
|
||||
--communication-language English \
|
||||
--document-output-language English \
|
||||
|
|
@ -115,7 +118,7 @@ npx bmad-method install \
|
|||
--directory ~/projects/myapp \
|
||||
--modules bmm \
|
||||
--custom-content ~/my-custom-module,~/another-module \
|
||||
--tools claude-code
|
||||
--tools codex
|
||||
```
|
||||
|
||||
## What You Get
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ sidebar:
|
|||
|------|-------------|---------|
|
||||
| `--directory <path>` | 安装目录 | `--directory ~/projects/myapp` |
|
||||
| `--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` |
|
||||
| `--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)。
|
||||
|
||||
对于 `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` |
|
||||
| 仅使用默认值 | 使用 `-y` 接受所有默认值 | `npx bmad-method install --yes` |
|
||||
| 不包含工具 | 跳过工具/IDE 配置 | `npx bmad-method install --modules bmm --tools none` |
|
||||
|
|
@ -83,7 +86,7 @@ sidebar:
|
|||
npx bmad-method install \
|
||||
--directory "${GITHUB_WORKSPACE}" \
|
||||
--modules bmm \
|
||||
--tools claude-code \
|
||||
--tools codex \
|
||||
--user-name "CI Bot" \
|
||||
--communication-language English \
|
||||
--document-output-language English \
|
||||
|
|
@ -115,7 +118,7 @@ npx bmad-method install \
|
|||
--directory ~/projects/myapp \
|
||||
--modules bmm \
|
||||
--custom-content ~/my-custom-module,~/another-module \
|
||||
--tools claude-code
|
||||
--tools codex
|
||||
```
|
||||
|
||||
## 安装结果
|
||||
|
|
|
|||
|
|
@ -54,6 +54,13 @@ async function createTestBmadFixture() {
|
|||
// Minimal workflow manifest (generators check for this)
|
||||
await fs.ensureDir(path.join(fixtureDir, '_config'));
|
||||
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)
|
||||
const minimalAgent = [
|
||||
|
|
@ -78,6 +85,16 @@ async function createTestBmadFixture() {
|
|||
await fs.ensureDir(path.join(fixtureDir, 'bmm', 'agents'));
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -550,29 +567,38 @@ async function runTests() {
|
|||
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 {
|
||||
clearCache();
|
||||
const platformCodes11 = await loadPlatformCodes();
|
||||
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(
|
||||
Array.isArray(codexInstaller?.legacy_targets) && codexInstaller.legacy_targets.includes('.codex/prompts'),
|
||||
'Codex installer cleans legacy prompt output',
|
||||
Array.isArray(codexInstaller?.legacy_targets) && codexInstaller.legacy_targets.includes('.agents/skills'),
|
||||
'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-'));
|
||||
tempHome11 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-codex-home-'));
|
||||
process.env.HOME = tempHome11;
|
||||
const installedBmadDir11 = await createTestBmadFixture();
|
||||
const legacyDir11 = path.join(tempProjectDir11, '.codex', 'prompts');
|
||||
const legacyDir11 = path.join(tempProjectDir11, '.agents', 'skills');
|
||||
await fs.ensureDir(legacyDir11);
|
||||
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');
|
||||
|
||||
const skillFile11 = path.join(tempProjectDir11, '.agents', 'skills', 'bmad-master', 'SKILL.md');
|
||||
assert(await fs.pathExists(skillFile11), 'Codex install writes SKILL.md directory output');
|
||||
const promptFile11 = path.join(tempProjectDir11, '.codex', 'prompts', 'bmad-master.md');
|
||||
assert(await fs.pathExists(promptFile11), 'Codex install writes slash command prompt files');
|
||||
|
||||
// Verify name frontmatter matches directory name
|
||||
const skillContent11 = await fs.readFile(skillFile11, 'utf8');
|
||||
const nameMatch11 = skillContent11.match(/^name:\s*(.+)$/m);
|
||||
assert(nameMatch11 && nameMatch11[1].trim() === 'bmad-master', 'Codex skill name frontmatter matches directory name exactly');
|
||||
const promptContent11 = await fs.readFile(promptFile11, 'utf8');
|
||||
const nameMatch11 = promptContent11.match(/^name:\s*['"]?bmad-master['"]?\s*$/m);
|
||||
assert(nameMatch11, 'Codex prompt frontmatter keeps bmad command name');
|
||||
|
||||
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(tempHome11);
|
||||
await fs.remove(installedBmadDir11);
|
||||
} 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('');
|
||||
|
|
@ -615,9 +655,9 @@ async function runTests() {
|
|||
const installedBmadDir12 = await createTestBmadFixture();
|
||||
|
||||
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.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();
|
||||
await ideManager12.ensureInitialized();
|
||||
|
|
@ -625,11 +665,11 @@ async function runTests() {
|
|||
silent: true,
|
||||
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?.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(installedBmadDir12);
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
|||
*/
|
||||
async detect(projectDir) {
|
||||
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)) {
|
||||
try {
|
||||
const entries = await fs.readdir(dir);
|
||||
|
|
@ -70,8 +70,8 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
|||
const conflict = await this.findAncestorConflict(projectDir);
|
||||
if (conflict) {
|
||||
await prompts.log.error(
|
||||
`Found existing BMAD skills in ancestor installation: ${conflict}\n` +
|
||||
` ${this.name} inherits skills from parent directories, so this would cause duplicates.\n` +
|
||||
`Found existing BMAD files in ancestor installation: ${conflict}\n` +
|
||||
` ${this.name} inherits commands from parent directories, so this would cause duplicates.\n` +
|
||||
` Please remove the BMAD files from that directory first:\n` +
|
||||
` rm -rf "${conflict}"/bmad*`,
|
||||
);
|
||||
|
|
@ -120,11 +120,11 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
|||
// Skip targets with explicitly empty artifact_types and no verbatim skills
|
||||
// This prevents creating empty directories when no artifacts will be written
|
||||
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 } };
|
||||
}
|
||||
|
||||
const targetPath = path.join(projectDir, target_dir);
|
||||
const targetPath = this.resolveTargetPath(projectDir, target_dir);
|
||||
await this.ensureDir(targetPath);
|
||||
|
||||
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)
|
||||
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);
|
||||
|
|
@ -638,15 +648,8 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
|||
async installVerbatimSkills(projectDir, bmadDir, targetPath, config) {
|
||||
const bmadFolderName = path.basename(bmadDir);
|
||||
const bmadPrefix = bmadFolderName + '/';
|
||||
const csvPath = path.join(bmadDir, '_config', 'skill-manifest.csv');
|
||||
|
||||
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,
|
||||
});
|
||||
const records = await this.loadSkillManifestRecords(bmadDir);
|
||||
if (!records || records.length === 0) return 0;
|
||||
|
||||
let count = 0;
|
||||
|
||||
|
|
@ -699,6 +702,119 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
|||
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
|
||||
* @param {Object} results - Installation results
|
||||
|
|
@ -766,6 +882,13 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
|||
} else if (this.installerConfig?.target_dir) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
* @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
|
||||
*/
|
||||
async cleanupTarget(projectDir, targetDir, options = {}) {
|
||||
const targetPath = path.join(projectDir, targetDir);
|
||||
const targetPath = this.resolveTargetPath(projectDir, targetDir);
|
||||
|
||||
if (!(await fs.pathExists(targetPath))) {
|
||||
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
|
||||
*/
|
||||
async findAncestorConflict(projectDir) {
|
||||
const targetDir = this.installerConfig?.target_dir;
|
||||
if (!targetDir) return null;
|
||||
const targetDirs = [];
|
||||
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));
|
||||
let current = path.dirname(resolvedProject);
|
||||
const root = path.parse(current).root;
|
||||
|
||||
while (current !== root && current.length > root.length) {
|
||||
const candidatePath = path.join(current, targetDir);
|
||||
try {
|
||||
if (await fs.pathExists(candidatePath)) {
|
||||
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
|
||||
for (const targetDir of targetDirs) {
|
||||
// Ancestor conflicts only apply to project-relative directories.
|
||||
if (this.isGlobalPath(targetDir) || path.isAbsolute(targetDir)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let current = path.dirname(resolvedProject);
|
||||
const root = path.parse(current).root;
|
||||
|
||||
while (current !== root && current.length > root.length) {
|
||||
const candidatePath = path.join(current, targetDir);
|
||||
try {
|
||||
if (await fs.pathExists(candidatePath)) {
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -70,13 +70,14 @@ platforms:
|
|||
description: "OpenAI Codex integration"
|
||||
installer:
|
||||
legacy_targets:
|
||||
- .codex/prompts
|
||||
- ~/.codex/prompts
|
||||
target_dir: .agents/skills
|
||||
- .agents/skills
|
||||
target_dir: .codex/prompts
|
||||
template_type: default
|
||||
skill_format: true
|
||||
ancestor_conflict_check: true
|
||||
artifact_types: [agents, workflows, tasks]
|
||||
install_skill_manifest_as_artifacts: true
|
||||
sync_targets:
|
||||
- ~/.codex/prompts
|
||||
|
||||
codebuddy:
|
||||
name: "CodeBuddy"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -7,7 +7,7 @@ Scope: migrate the BMAD-supported platforms that fully support the Agent Skills
|
|||
Current branch status:
|
||||
|
||||
- `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.
|
||||
|
||||
|
|
@ -26,15 +26,16 @@ Support assumption: full Agent Skills support. BMAD has already migrated from `.
|
|||
|
||||
## 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`
|
||||
|
||||
- [x] Confirm current implementation still matches Codex CLI skills expectations
|
||||
- [x] Confirm legacy cleanup for project and global `.codex/prompts`
|
||||
- [x] Confirm current implementation matches Codex CLI prompt command expectations
|
||||
- [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 reinstall/upgrade from legacy prompt output
|
||||
- [x] Confirm ancestor conflict protection because Codex inherits parent-directory `.agents/skills`
|
||||
- [x] Test reinstall/upgrade from legacy skills output
|
||||
- [x] Confirm ancestor conflict protection because Codex can inherit parent-directory `.codex/prompts`
|
||||
- [x] Implement/extend automated tests as needed
|
||||
|
||||
## Cursor
|
||||
|
|
|
|||
Loading…
Reference in New Issue