feat(bmm): add monorepo/multi-project support via skills
- Add monorepo_context config section to module.yaml for path resolution - Add bmad-project-list, bmad-project-new, bmad-project-switch skills - Config-level hook: LLM reads config and applies context before path vars - Inline overrides via #project:NAME or #p:NAME in invocations - Installer: ensure 0-context workflow dir is installed - Add docs/how-to/monorepo-setup.md - 100% backward compatible when no .current_project file Made-with: Cursor
This commit is contained in:
parent
42b1d0f657
commit
fad8a7923e
|
|
@ -0,0 +1,48 @@
|
|||
---
|
||||
title: "Monorepo / Multi-Project Setup"
|
||||
description: Run multiple independent projects within a single BMAD installation
|
||||
sidebar:
|
||||
order: 8
|
||||
---
|
||||
|
||||
# Monorepo / Multi-Project Setup
|
||||
|
||||
BMAD supports running multiple independent projects within a single installation. This is useful for monorepos, multi-app repositories, or any workspace where you want isolated artifact directories per project.
|
||||
|
||||
## How It Works
|
||||
|
||||
A `.current_project` file in your `_bmad/` directory stores the active project name. When set, all artifact output paths are redirected to project-specific subdirectories.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. List existing projects by invoking `skill:bmad-project-list`
|
||||
2. Create a new project by invoking `skill:bmad-project-new` with a project name
|
||||
3. Switch to an existing project by invoking `skill:bmad-project-switch` with a project name
|
||||
4. Clear project context by invoking `skill:bmad-project-switch` and entering `CLEAR`
|
||||
|
||||
## Directory Structure
|
||||
|
||||
When a project context is active, for example `my-app`:
|
||||
|
||||
- `_bmad-output/my-app/planning-artifacts/` for PRDs, briefs, UX designs
|
||||
- `_bmad-output/my-app/implementation-artifacts/` for sprint plans and stories
|
||||
- `_bmad-output/my-app/knowledge/` for long-term project knowledge
|
||||
|
||||
## Inline Overrides
|
||||
|
||||
Temporarily target a different project without changing the global context:
|
||||
|
||||
- `#project:my-app`
|
||||
- `#p:my-app`
|
||||
|
||||
**Precedence:**
|
||||
|
||||
1. Inline override (`#p:NAME`) — highest priority
|
||||
2. Global context file (`.current_project`)
|
||||
3. Default config paths
|
||||
|
||||
## Security
|
||||
|
||||
- Path traversal (`..`) is rejected
|
||||
- Absolute paths are rejected
|
||||
- Only alphanumeric characters, dots, dashes, underscores, and forward slashes are allowed
|
||||
|
|
@ -27,3 +27,15 @@ agent:
|
|||
- trigger: IR or fuzzy match on implementation-readiness
|
||||
exec: "skill:bmad-check-implementation-readiness"
|
||||
description: "[IR] Implementation Readiness: Ensure the PRD, UX, and Architecture and Epics and Stories List are all aligned"
|
||||
|
||||
- trigger: PN or fuzzy match on project-new
|
||||
exec: "skill:bmad-project-new"
|
||||
description: "[PN] Project New: Create a new project and make it the active project context"
|
||||
|
||||
- trigger: PS or fuzzy match on project-switch
|
||||
exec: "skill:bmad-project-switch"
|
||||
description: "[PS] Project Switch: Switch the active project context for monorepo support"
|
||||
|
||||
- trigger: PL or fuzzy match on project-list
|
||||
exec: "skill:bmad-project-list"
|
||||
description: "[PL] Project List: Show available project contexts"
|
||||
|
|
|
|||
|
|
@ -36,3 +36,15 @@ agent:
|
|||
- trigger: CR or fuzzy match on code-review
|
||||
exec: "skill:bmad-code-review"
|
||||
description: "[CR] Code Review: Initiate a comprehensive code review across multiple quality facets. For best results, use a fresh context and a different quality LLM if available"
|
||||
|
||||
- trigger: PN or fuzzy match on project-new
|
||||
exec: "skill:bmad-project-new"
|
||||
description: "[PN] Project New: Create a new project and make it the active project context"
|
||||
|
||||
- trigger: PS or fuzzy match on project-switch
|
||||
exec: "skill:bmad-project-switch"
|
||||
description: "[PS] Project Switch: Switch the active project context for monorepo support"
|
||||
|
||||
- trigger: PL or fuzzy match on project-list
|
||||
exec: "skill:bmad-project-list"
|
||||
description: "[PL] Project List: Show available project contexts"
|
||||
|
|
|
|||
|
|
@ -42,3 +42,15 @@ agent:
|
|||
- trigger: CC or fuzzy match on correct-course
|
||||
exec: "skill:bmad-correct-course"
|
||||
description: "[CC] Course Correction: Use this so we can determine how to proceed if major need for change is discovered mid implementation"
|
||||
|
||||
- trigger: PN or fuzzy match on project-new
|
||||
exec: "skill:bmad-project-new"
|
||||
description: "[PN] Project New: Create a new project and make it the active project context"
|
||||
|
||||
- trigger: PS or fuzzy match on project-switch
|
||||
exec: "skill:bmad-project-switch"
|
||||
description: "[PS] Project Switch: Switch the active project context for monorepo support"
|
||||
|
||||
- trigger: PL or fuzzy match on project-list
|
||||
exec: "skill:bmad-project-list"
|
||||
description: "[PL] Project List: Show available project contexts"
|
||||
|
|
|
|||
|
|
@ -34,3 +34,15 @@ agent:
|
|||
- trigger: CR or fuzzy match on code-review
|
||||
exec: "skill:bmad-code-review"
|
||||
description: "[CR] Code Review: Initiate a comprehensive code review across multiple quality facets. For best results, use a fresh context and a different quality LLM if available"
|
||||
|
||||
- trigger: PN or fuzzy match on project-new
|
||||
exec: "skill:bmad-project-new"
|
||||
description: "[PN] Project New: Create a new project and make it the active project context"
|
||||
|
||||
- trigger: PS or fuzzy match on project-switch
|
||||
exec: "skill:bmad-project-switch"
|
||||
description: "[PS] Project Switch: Switch the active project context for monorepo support"
|
||||
|
||||
- trigger: PL or fuzzy match on project-list
|
||||
exec: "skill:bmad-project-list"
|
||||
description: "[PL] Project List: Show available project contexts"
|
||||
|
|
|
|||
|
|
@ -35,3 +35,15 @@ agent:
|
|||
- trigger: CC or fuzzy match on correct-course
|
||||
exec: "skill:bmad-correct-course"
|
||||
description: "[CC] Course Correction: Use this so we can determine how to proceed if major need for change is discovered mid implementation"
|
||||
|
||||
- trigger: PN or fuzzy match on project-new
|
||||
exec: "skill:bmad-project-new"
|
||||
description: "[PN] Project New: Create a new project and make it the active project context"
|
||||
|
||||
- trigger: PS or fuzzy match on project-switch
|
||||
exec: "skill:bmad-project-switch"
|
||||
description: "[PS] Project Switch: Switch the active project context for monorepo support"
|
||||
|
||||
- trigger: PL or fuzzy match on project-list
|
||||
exec: "skill:bmad-project-list"
|
||||
description: "[PL] Project List: Show available project contexts"
|
||||
|
|
|
|||
|
|
@ -48,3 +48,35 @@ directories:
|
|||
- "{planning_artifacts}"
|
||||
- "{implementation_artifacts}"
|
||||
- "{project_knowledge}"
|
||||
|
||||
# --- Monorepo / Multi-Project Context Support ---
|
||||
# IMPORTANT: When loading this config, apply monorepo context resolution BEFORE using any path variables.
|
||||
#
|
||||
# Resolution order:
|
||||
# 1. INLINE OVERRIDE: Scan user's invocation for #project:NAME or #p:NAME (case-insensitive).
|
||||
# If found, set project_suffix = NAME.
|
||||
# 2. FILE FALLBACK: If no inline override, check if {project-root}/_bmad/.current_project exists.
|
||||
# If so, read its single-line content as project_suffix (trim whitespace/newlines).
|
||||
# 3. VALIDATE (if project_suffix is set):
|
||||
# - REJECT if empty, contains '..', starts with '/' or drive letter (e.g. C:)
|
||||
# - REJECT if fails whitelist regex: ^[a-zA-Z0-9._\-/]+$
|
||||
# - On rejection: output "Security Error: Invalid project context" and HALT
|
||||
# 4. OVERRIDE PATHS (if valid project_suffix):
|
||||
# - output_folder = {project-root}/_bmad-output/{project_suffix}
|
||||
# - planning_artifacts = {output_folder}/planning-artifacts
|
||||
# - implementation_artifacts = {output_folder}/implementation-artifacts
|
||||
# - project_knowledge = {output_folder}/knowledge
|
||||
# - sprint_status_file = {output_folder}/sprint-status.yaml
|
||||
# - project_name = {project_suffix}
|
||||
# 5. If no project_suffix is set, use the default paths from config as-is.
|
||||
|
||||
monorepo_context:
|
||||
enabled: true
|
||||
context_file: ".current_project"
|
||||
inline_override_patterns:
|
||||
- "#project:"
|
||||
- "#p:"
|
||||
validation:
|
||||
whitelist_regex: "^[a-zA-Z0-9._\\-/]+$"
|
||||
reject_traversal: true
|
||||
reject_absolute: true
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
name: bmad-project-list
|
||||
description: 'List available projects for monorepo support. Use when the user says "project list", "list projects", or "show projects"'
|
||||
---
|
||||
|
||||
Follow the instructions in [workflow.md](workflow.md).
|
||||
|
|
@ -0,0 +1 @@
|
|||
type: skill
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
# Project List
|
||||
|
||||
**Goal:** List the available project contexts for BMAD artifacts.
|
||||
|
||||
**Your Role:** Configuration Assistant.
|
||||
|
||||
## WORKFLOW ARCHITECTURE
|
||||
|
||||
### 1. Identify Projects
|
||||
|
||||
- Use your file listing or system capabilities to examine the `{project-root}/_bmad-output/` directory.
|
||||
- Identify all subdirectories within this path. Each subdirectory represents an available project context, except:
|
||||
- Ignore hidden directories starting with `.`
|
||||
- Ignore standard BMAD artifact directories: `planning-artifacts`, `implementation-artifacts`, `test-artifacts`, `knowledge`, `docs`, `assets`
|
||||
- The root `_bmad-output/` directory itself represents the `default (root)` unset project context.
|
||||
|
||||
### 2. Output Results
|
||||
|
||||
1. Display a formatted numbered list of the existing projects you found.
|
||||
2. Clearly indicate the `default (root)` project context, usually as `[0]`.
|
||||
3. Check whether an active project is currently set by reading the active project selector file in `{project-root}/_bmad/` (or as configured in `monorepo_context.context_file`). If it exists, indicate which project from the list is active, for example by adding `(ACTIVE)` next to the entry.
|
||||
4. Inform the user that they can switch projects with PS (Project Switch) or create a new one with PN (Project New).
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
name: bmad-project-new
|
||||
description: 'Create a new project for monorepo support. Use when the user says "project new", "new project", or "create project"'
|
||||
---
|
||||
|
||||
Follow the instructions in [workflow.md](workflow.md).
|
||||
|
|
@ -0,0 +1 @@
|
|||
type: skill
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
# Project New
|
||||
|
||||
**Goal:** Create a new project context with the standard BMAD artifact structure and make it active.
|
||||
|
||||
**Your Role:** Configuration Assistant.
|
||||
|
||||
## WORKFLOW ARCHITECTURE
|
||||
|
||||
This is a single-step workflow that creates directories and updates the active project context file.
|
||||
|
||||
### 1. Configuration Loading
|
||||
|
||||
Load and read full config from `{project-root}/_bmad/bmm/config.yaml` and resolve variables and artifact paths.
|
||||
The context file is the default hidden project selector file in `{project-root}/_bmad/` (or as configured in `monorepo_context.context_file`).
|
||||
|
||||
### 2. Project Creation
|
||||
|
||||
1. **Analyze Request**: Determine the requested new project name from the user's initial invocation, such as "new project my-app".
|
||||
2. **Wait for Input (If Missing)**: If the user did not provide a project name:
|
||||
- Ask: `What should the new project be called?`
|
||||
- Wait for input.
|
||||
3. **Validate Project Name**
|
||||
- **Cleanup**: Remove leading and trailing slashes and any occurrences of `_bmad-output/`.
|
||||
- **Validate - No Traversal**: Reject if path contains `..`.
|
||||
- **Validate - No Absolute**: Reject if path starts with `/` or a drive letter such as `C:`.
|
||||
- **Validate - Empty/Whitespace**: Reject if empty or only whitespace.
|
||||
- **Validate - Whitelist**: Match against regex `^[-a-zA-Z0-9._/]+$`.
|
||||
- If invalid:
|
||||
- Output: `Error: Invalid project name — must be a relative path and contain only alphanumeric characters, dots, dashes, underscores, or slashes. Traversal (..) is strictly forbidden.`
|
||||
- Halt.
|
||||
4. **Check for Existing Project**
|
||||
- Check if `{project-root}/_bmad-output/<sanitized_path>` already exists.
|
||||
- If it already exists:
|
||||
- Ask: `The project <sanitized_path> already exists. Do you want to switch to it instead? (Yes/No)`
|
||||
- Wait for input.
|
||||
- If `Yes`:
|
||||
- Write the active project selector file in `{project-root}/_bmad/` with content `<sanitized_path>`
|
||||
- Output: `Project context set to: <sanitized_path>. Artifacts will go to _bmad-output/<sanitized_path>/.`
|
||||
- Halt.
|
||||
- If `No`:
|
||||
- Inform the user to choose another name or use PS (Project Switch) to switch to an existing project.
|
||||
- Halt.
|
||||
5. **Create Project Structure**
|
||||
- Create directory: `{project-root}/_bmad-output/<sanitized_path>/`
|
||||
- Create directory: `{project-root}/_bmad-output/<sanitized_path>/planning-artifacts/`
|
||||
- Create directory: `{project-root}/_bmad-output/<sanitized_path>/implementation-artifacts/`
|
||||
- Create directory: `{project-root}/_bmad-output/<sanitized_path>/knowledge/`
|
||||
- Write the active project selector file in `{project-root}/_bmad/` with content `<sanitized_path>`
|
||||
- Output: `Created project <sanitized_path> and set it as active. Artifacts will go to _bmad-output/<sanitized_path>/.`
|
||||
|
||||
### 3. Verification
|
||||
|
||||
- Display the full resolved output path for confirmation.
|
||||
- Mention that the user can use PS (Project Switch) to switch projects later and PL (Project List) to list available projects.
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
name: bmad-project-switch
|
||||
description: 'Switch the active project for monorepo support. Use when the user says "project switch", "switch project", or "choose project"'
|
||||
---
|
||||
|
||||
Follow the instructions in [workflow.md](workflow.md).
|
||||
|
|
@ -0,0 +1 @@
|
|||
type: skill
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
# Project Switch
|
||||
|
||||
**Goal:** Switch the active project path for BMAD artifacts.
|
||||
|
||||
**Your Role:** Configuration Assistant.
|
||||
|
||||
## WORKFLOW ARCHITECTURE
|
||||
|
||||
This is a single-step workflow that updates a local state file.
|
||||
|
||||
### 1. Configuration Loading
|
||||
|
||||
Load and read full config from `{project-root}/_bmad/bmm/config.yaml` and resolve variables and artifact paths.
|
||||
The context file is the default hidden project selector file in `{project-root}/_bmad/` (or as configured in `monorepo_context.context_file`).
|
||||
|
||||
### 2. Context Management
|
||||
|
||||
1. **Analyze Request**: Determine the requested project name from the user's initial invocation, such as "switch project my-app".
|
||||
2. **Wait for Input (If Missing)**: If the user did not provide a project name:
|
||||
- Use your file listing capabilities to examine the `{project-root}/_bmad-output/` directory.
|
||||
- Output a formatted list of the existing projects you found, explicitly noting the `default (root)` project context.
|
||||
Exclude hidden directories and standard BMAD artifact folders: `planning-artifacts`, `implementation-artifacts`, `test-artifacts`, `knowledge`, `docs`, `assets`.
|
||||
- Present options in a numbered list format:
|
||||
|
||||
```text
|
||||
Select a project:
|
||||
[0] default (root) - Current location for standard artifacts
|
||||
[1] project-name-1
|
||||
[2] project-name-2
|
||||
...
|
||||
|
||||
Enter a number to select, type an existing project name, or enter 'CLEAR' to reset to root:
|
||||
```
|
||||
|
||||
- Wait for input.
|
||||
3. **Process Input**: Once a project name is established:
|
||||
- **Case: CLEAR**
|
||||
- Delete the active project selector file in `{project-root}/_bmad/`
|
||||
- Output: `Project context cleared. Artifacts will go to root _bmad-output/.`
|
||||
- Halt.
|
||||
- **Case: Numeric Selection**
|
||||
- Map the number to the corresponding project from your earlier listing.
|
||||
- If number is `0`, treat it as the `CLEAR` case.
|
||||
- If the number is out of range, show an error and ask again.
|
||||
- If valid, use that project name as the path.
|
||||
- **Case: Path Provided** (text input)
|
||||
- **Cleanup**: Remove leading and trailing slashes and any occurrences of `_bmad-output/`.
|
||||
- **Validate - No Traversal**: Reject if path contains `..`.
|
||||
- **Validate - No Absolute**: Reject if path starts with `/` or a drive letter such as `C:`.
|
||||
- **Validate - Empty/Whitespace**: Reject if empty or only whitespace.
|
||||
- **Validate - Whitelist**: Match against regex `^[-a-zA-Z0-9._/]+$`.
|
||||
- **Check Results**
|
||||
- If invalid:
|
||||
- Output: `Error: Invalid project name — must be a relative path and contain only alphanumeric characters, dots, dashes, underscores, or slashes. Traversal (..) is strictly forbidden.`
|
||||
- Halt.
|
||||
- **Validate Existence**: Check if `{project-root}/_bmad-output/<sanitized_path>` exists on disk.
|
||||
- If it does not exist:
|
||||
- Output: `Error: Project <sanitized_path> does not exist. Use PN (Project New) to create it first, or PL (Project List) to see available projects.`
|
||||
- Halt.
|
||||
- Write the active project selector file in `{project-root}/_bmad/` with content `<sanitized_path>`
|
||||
- Output: `Project context set to: <sanitized_path>. Artifacts will go to _bmad-output/<sanitized_path>/.`
|
||||
|
||||
### 3. Verification
|
||||
|
||||
- Display the full resolved output path for confirmation.
|
||||
|
||||
## Inline Project Overrides
|
||||
|
||||
You can also temporarily run a command against a different project without changing the global context file. Use the `#project:NAME` or `#p:NAME` syntax in your command invocation.
|
||||
|
||||
**Examples:**
|
||||
|
||||
- `create-prd #project:my-app`
|
||||
- `sprint-planning #p:admin-portal`
|
||||
|
||||
**Precedence:**
|
||||
|
||||
1. **Inline Override** (`#p:NAME`) — highest priority
|
||||
2. **Global Context File** (the active project selector file in `{project-root}/_bmad/`)
|
||||
3. **Default Config** (if neither is present)
|
||||
|
|
@ -15,6 +15,7 @@ const path = require('node:path');
|
|||
const os = require('node:os');
|
||||
const fs = require('fs-extra');
|
||||
const { YamlXmlBuilder } = require('../tools/cli/lib/yaml-xml-builder');
|
||||
const { Installer } = require('../tools/cli/installers/lib/core/installer');
|
||||
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');
|
||||
|
|
@ -1868,6 +1869,118 @@ async function runTests() {
|
|||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Test 32: Monorepo config block emission
|
||||
// ============================================================
|
||||
console.log(`${colors.yellow}Test Suite 32: Monorepo Config Generation${colors.reset}\n`);
|
||||
|
||||
let tempFixture32;
|
||||
try {
|
||||
tempFixture32 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-monorepo-config-'));
|
||||
await fs.ensureDir(path.join(tempFixture32, 'bmm'));
|
||||
|
||||
const installer32 = new Installer();
|
||||
const sourceModule32 = await fs.readFile(path.join(projectRoot, 'src', 'bmm', 'module.yaml'), 'utf8');
|
||||
const extractedBlock32 = installer32.extractMonorepoContextBlock(sourceModule32);
|
||||
|
||||
assert(
|
||||
extractedBlock32 && extractedBlock32.includes('monorepo_context:'),
|
||||
'Installer extracts monorepo context block from module schema',
|
||||
);
|
||||
assert(extractedBlock32 && extractedBlock32.includes('INLINE OVERRIDE'), 'Extracted monorepo block preserves instructional comments');
|
||||
|
||||
await installer32.generateModuleConfigs(tempFixture32, {
|
||||
core: {
|
||||
user_name: 'TestUser',
|
||||
communication_language: 'English',
|
||||
document_output_language: 'English',
|
||||
output_folder: '_bmad-output',
|
||||
},
|
||||
bmm: {
|
||||
project_name: 'demo-project',
|
||||
user_skill_level: 'intermediate',
|
||||
planning_artifacts: '{project-root}/_bmad-output/planning-artifacts',
|
||||
implementation_artifacts: '{project-root}/_bmad-output/implementation-artifacts',
|
||||
project_knowledge: '{project-root}/docs',
|
||||
},
|
||||
});
|
||||
|
||||
const generatedConfig32 = await fs.readFile(path.join(tempFixture32, 'bmm', 'config.yaml'), 'utf8');
|
||||
assert(
|
||||
generatedConfig32.includes('# --- Monorepo / Multi-Project Context Support ---'),
|
||||
'Generated bmm config preserves monorepo instruction header',
|
||||
);
|
||||
assert(generatedConfig32.includes('monorepo_context:'), 'Generated bmm config includes monorepo_context YAML');
|
||||
assert(
|
||||
generatedConfig32.includes('inline_override_patterns:') && generatedConfig32.includes('- "#p:"'),
|
||||
'Generated bmm config preserves monorepo override patterns',
|
||||
);
|
||||
} catch (error) {
|
||||
assert(false, 'Monorepo config generation test succeeds', error.message);
|
||||
} finally {
|
||||
if (tempFixture32) await fs.remove(tempFixture32).catch(() => {});
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Test 33: Context skills are discoverable as skills
|
||||
// ============================================================
|
||||
console.log(`${colors.yellow}Test Suite 33: Context Skill Discovery${colors.reset}\n`);
|
||||
|
||||
let tempFixture33;
|
||||
try {
|
||||
tempFixture33 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-context-skills-'));
|
||||
await fs.ensureDir(path.join(tempFixture33, '_config'));
|
||||
await fs.ensureDir(path.join(tempFixture33, 'bmm', 'agents'));
|
||||
await fs.writeFile(path.join(tempFixture33, 'bmm', 'agents', 'test.md'), '<agent name="Test" title="T"><persona>p</persona></agent>');
|
||||
|
||||
const sourceContextDir33 = path.join(projectRoot, 'src', 'bmm', 'workflows', '0-context');
|
||||
const targetContextDir33 = path.join(tempFixture33, 'bmm', 'workflows', '0-context');
|
||||
await fs.copy(sourceContextDir33, targetContextDir33);
|
||||
|
||||
const generator33 = new ManifestGenerator();
|
||||
await generator33.generateManifests(tempFixture33, ['bmm'], [], { ides: [] });
|
||||
|
||||
const newProjectSkill33 = generator33.skills.find((skill) => skill.canonicalId === 'bmad-project-new');
|
||||
const setProjectSkill33 = generator33.skills.find((skill) => skill.canonicalId === 'bmad-project-switch');
|
||||
const listProjectsSkill33 = generator33.skills.find((skill) => skill.canonicalId === 'bmad-project-list');
|
||||
|
||||
assert(newProjectSkill33 !== undefined, 'New project skill appears in skills[]');
|
||||
assert(setProjectSkill33 !== undefined, 'Set project context skill appears in skills[]');
|
||||
assert(listProjectsSkill33 !== undefined, 'List projects skill appears in skills[]');
|
||||
assert(
|
||||
newProjectSkill33 && newProjectSkill33.path.includes('workflows/0-context/bmad-project-new/SKILL.md'),
|
||||
'New project skill keeps its workflow directory path',
|
||||
);
|
||||
assert(
|
||||
setProjectSkill33 && setProjectSkill33.path.includes('workflows/0-context/bmad-project-switch/SKILL.md'),
|
||||
'Set project skill keeps its workflow directory path',
|
||||
);
|
||||
assert(
|
||||
listProjectsSkill33 && listProjectsSkill33.path.includes('workflows/0-context/bmad-project-list/SKILL.md'),
|
||||
'List projects skill keeps its workflow directory path',
|
||||
);
|
||||
assert(
|
||||
!generator33.workflows.some((workflow) => workflow.path.includes('0-context/bmad-project-new')),
|
||||
'New project skill does not appear in workflows[]',
|
||||
);
|
||||
assert(
|
||||
!generator33.workflows.some((workflow) => workflow.path.includes('0-context/bmad-project-switch')),
|
||||
'Set project context skill does not appear in workflows[]',
|
||||
);
|
||||
assert(
|
||||
!generator33.workflows.some((workflow) => workflow.path.includes('0-context/bmad-project-list')),
|
||||
'List projects skill does not appear in workflows[]',
|
||||
);
|
||||
} catch (error) {
|
||||
assert(false, 'Context skill discovery test succeeds', error.message);
|
||||
} finally {
|
||||
if (tempFixture33) await fs.remove(tempFixture33).catch(() => {});
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Summary
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -2067,6 +2067,12 @@ class Installer {
|
|||
}
|
||||
}
|
||||
|
||||
const staticConfigBlocks = await this.getStaticConfigBlocks(moduleName);
|
||||
if (staticConfigBlocks.length > 0) {
|
||||
yamlContent = yamlContent.trimEnd();
|
||||
yamlContent += `\n\n${staticConfigBlocks.join('\n\n')}\n`;
|
||||
}
|
||||
|
||||
// Write the clean config file with POSIX-compliant final newline
|
||||
const content = header + yamlContent;
|
||||
await fs.writeFile(configPath, content.endsWith('\n') ? content : content + '\n', 'utf8');
|
||||
|
|
@ -2077,6 +2083,65 @@ class Installer {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return static config blocks that should be preserved from a module schema
|
||||
* even though they are not collected interactively.
|
||||
* @param {string} moduleName - Module name
|
||||
* @returns {Promise<string[]>} Raw YAML/comment blocks to append to config.yaml
|
||||
*/
|
||||
async getStaticConfigBlocks(moduleName) {
|
||||
const moduleSchemaPath = await this.resolveModuleSchemaPath(moduleName);
|
||||
if (!moduleSchemaPath || !(await fs.pathExists(moduleSchemaPath))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const content = await fs.readFile(moduleSchemaPath, 'utf8');
|
||||
const monorepoBlock = this.extractMonorepoContextBlock(content);
|
||||
return monorepoBlock ? [monorepoBlock] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the source module.yaml path for a module.
|
||||
* @param {string} moduleName - Module name
|
||||
* @returns {Promise<string|null>} Path to module.yaml or null if not found
|
||||
*/
|
||||
async resolveModuleSchemaPath(moduleName) {
|
||||
if (this.configCollector.customModulePaths?.has(moduleName)) {
|
||||
return path.join(this.configCollector.customModulePaths.get(moduleName), 'module.yaml');
|
||||
}
|
||||
|
||||
const standardPath = path.join(getModulePath(moduleName), 'module.yaml');
|
||||
if (await fs.pathExists(standardPath)) {
|
||||
return standardPath;
|
||||
}
|
||||
|
||||
const moduleSourcePath = await this.moduleManager.findModuleSource(moduleName, { silent: true });
|
||||
return moduleSourcePath ? path.join(moduleSourcePath, 'module.yaml') : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the monorepo context instructions and YAML block from module.yaml.
|
||||
* @param {string} content - Raw module.yaml content
|
||||
* @returns {string|null} Preserved block or null when absent
|
||||
*/
|
||||
extractMonorepoContextBlock(content) {
|
||||
// Capture from the header comment through to the end of the monorepo_context YAML block.
|
||||
// The block is expected at the end of the file, after any other config sections.
|
||||
const headerIndex = content.indexOf('# --- Monorepo / Multi-Project Context Support ---');
|
||||
if (headerIndex === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const block = content.slice(headerIndex).trimEnd();
|
||||
|
||||
// Sanity check: the block must contain the structured YAML key
|
||||
if (!block.includes('monorepo_context:')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install core with resolved dependencies
|
||||
* @param {string} bmadDir - BMAD installation directory
|
||||
|
|
|
|||
Loading…
Reference in New Issue