fix: rabitai PR fixes
This commit is contained in:
parent
fad8a7923e
commit
f197be10da
|
|
@ -11,7 +11,7 @@ BMAD supports running multiple independent projects within a single installation
|
|||
|
||||
## 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.
|
||||
A `.current_project` file in your `_bmad/` directory stores the active project name. This file is mutable local state: it should never be committed, because committing it can silently flip the active project context for other contributors and CI. Add `_bmad/.current_project` to your VCS ignore file, for example `.gitignore`. When set, all artifact output paths are redirected to project-specific subdirectories.
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
|
@ -45,4 +45,4 @@ Temporarily target a different project without changing the global context:
|
|||
|
||||
- Path traversal (`..`) is rejected
|
||||
- Absolute paths are rejected
|
||||
- Only alphanumeric characters, dots, dashes, underscores, and forward slashes are allowed
|
||||
- Only alphanumeric characters, dots, dashes, and underscores are allowed
|
||||
|
|
|
|||
|
|
@ -58,8 +58,9 @@ directories:
|
|||
# 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._\-/]+$
|
||||
# - REJECT if empty, equals '.', contains '..', starts with '/' or drive letter (e.g. C:)
|
||||
# - REJECT if it contains '/' or '\' because project_suffix must be a single directory name
|
||||
# - 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}
|
||||
|
|
@ -77,6 +78,6 @@ monorepo_context:
|
|||
- "#project:"
|
||||
- "#p:"
|
||||
validation:
|
||||
whitelist_regex: "^[a-zA-Z0-9._\\-/]+$"
|
||||
whitelist_regex: "^[a-zA-Z0-9._-]+$"
|
||||
reject_traversal: true
|
||||
reject_absolute: true
|
||||
|
|
|
|||
|
|
@ -8,15 +8,17 @@
|
|||
|
||||
### 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:
|
||||
- First check whether `{project-root}/_bmad-output/` exists and is readable.
|
||||
- If it does not exist, or listing it returns an `ENOENT`-style not-found error, treat the available project list as empty and continue with only `default (root)`.
|
||||
- If it exists, use your file listing or system capabilities to examine the `{project-root}/_bmad-output/` directory.
|
||||
- Identify all direct child 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.
|
||||
1. Display a formatted numbered list of the existing projects you found. If none exist yet, show only `default (root)`.
|
||||
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).
|
||||
|
|
|
|||
|
|
@ -20,13 +20,15 @@ The context file is the default hidden project selector file in `{project-root}/
|
|||
- 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 Absolute**: Check the raw user input before cleanup. Reject if it starts with `/`, starts with `\\`, or starts with a drive letter such as `C:`.
|
||||
- **Cleanup**: If the raw value starts with `_bmad-output/`, remove that single leading prefix once. Then trim leading and trailing slashes.
|
||||
- **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._/]+$`.
|
||||
- **Validate - No Separators**: Reject if the sanitized value still contains `/` or `\`. Project names must be single directory names, not nested paths.
|
||||
- **Validate - Empty/Whitespace**: Reject if empty, only whitespace, or exactly `.`.
|
||||
- **Validate - Whitelist**: Match against regex `^[-a-zA-Z0-9._]+$`.
|
||||
- **Validate - Reserved Names**: Reject if the sanitized value is any reserved BMAD artifact directory name: `planning-artifacts`, `implementation-artifacts`, `test-artifacts`, `knowledge`, `docs`, `assets`.
|
||||
- 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.`
|
||||
- Output: `Error: Invalid project name — use a single directory name containing only alphanumeric characters, dots, dashes, or underscores. Nested paths, reserved artifact names, absolute paths, and traversal (..) are forbidden.`
|
||||
- Halt.
|
||||
4. **Check for Existing Project**
|
||||
- Check if `{project-root}/_bmad-output/<sanitized_path>` already exists.
|
||||
|
|
|
|||
|
|
@ -17,9 +17,11 @@ The context file is the default hidden project selector file in `{project-root}/
|
|||
|
||||
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.
|
||||
- First check whether `{project-root}/_bmad-output/` exists and is readable.
|
||||
- If it exists, use your file listing capabilities to examine that 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`.
|
||||
- If `{project-root}/_bmad-output/` does not exist or is unreadable because it is missing, show only the `default (root)` project context.
|
||||
- Present options in a numbered list format:
|
||||
|
||||
```text
|
||||
|
|
@ -44,17 +46,19 @@ Enter a number to select, type an existing project name, or enter 'CLEAR' to res
|
|||
- 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 Absolute**: Check the raw user input before cleanup. Reject if it starts with `/`, starts with `\\`, or starts with a drive letter such as `C:`.
|
||||
- **Cleanup**: If the raw value starts with `_bmad-output/`, remove that single leading prefix once. Then trim leading and trailing slashes.
|
||||
- **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._/]+$`.
|
||||
- **Validate - No Separators**: Reject if the sanitized value still contains `/` or `\`. Project names must be single directory names, not nested paths.
|
||||
- **Validate - Empty/Whitespace**: Reject if empty, only whitespace, or exactly `.`.
|
||||
- **Validate - Whitelist**: Match against regex `^[-a-zA-Z0-9._]+$`.
|
||||
- **Validate - Reserved Names**: Reject if the sanitized value is any reserved BMAD artifact directory name: `planning-artifacts`, `implementation-artifacts`, `test-artifacts`, `knowledge`, `docs`, `assets`.
|
||||
- **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.`
|
||||
- Output: `Error: Invalid project name — use a single directory name containing only alphanumeric characters, dots, dashes, or underscores. Nested paths, reserved artifact names, absolute paths, and traversal (..) are forbidden.`
|
||||
- Halt.
|
||||
- **Validate Existence**: Check if `{project-root}/_bmad-output/<sanitized_path>` exists on disk.
|
||||
- If it does not exist:
|
||||
- **Validate Existence**: Check if `{project-root}/_bmad-output/<sanitized_path>` exists and is a directory.
|
||||
- If it does not exist, or exists but is not a directory:
|
||||
- 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>`
|
||||
|
|
|
|||
|
|
@ -1915,6 +1915,11 @@ async function runTests() {
|
|||
generatedConfig32.includes('inline_override_patterns:') && generatedConfig32.includes('- "#p:"'),
|
||||
'Generated bmm config preserves monorepo override patterns',
|
||||
);
|
||||
assert(generatedConfig32.includes('context_file:'), 'Generated bmm config preserves monorepo context_file');
|
||||
assert(generatedConfig32.includes('reject_traversal: true'), 'Generated bmm config preserves traversal protection');
|
||||
assert(generatedConfig32.includes('reject_absolute: true'), 'Generated bmm config preserves absolute-path protection');
|
||||
assert(generatedConfig32.includes('whitelist_regex: "^[a-zA-Z0-9._-]+$"'), 'Generated bmm config preserves monorepo whitelist regex');
|
||||
assert(!generatedConfig32.includes('\ncurrent_project:'), 'Generated bmm config does not materialize mutable current project state');
|
||||
} catch (error) {
|
||||
assert(false, 'Monorepo config generation test succeeds', error.message);
|
||||
} finally {
|
||||
|
|
@ -1924,59 +1929,132 @@ async function runTests() {
|
|||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Test 33: Context skills are discoverable as skills
|
||||
// Test 33: Installer manages monorepo helper files safely
|
||||
// ============================================================
|
||||
console.log(`${colors.yellow}Test Suite 33: Context Skill Discovery${colors.reset}\n`);
|
||||
console.log(`${colors.yellow}Test Suite 33: Installer Monorepo Helper Files${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>');
|
||||
tempFixture33 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-monorepo-helpers-'));
|
||||
const installer33 = new Installer();
|
||||
const projectDir33 = path.join(tempFixture33, 'project');
|
||||
await fs.ensureDir(projectDir33);
|
||||
|
||||
const sourceContextDir33 = path.join(projectRoot, 'src', 'bmm', 'workflows', '0-context');
|
||||
const targetContextDir33 = path.join(tempFixture33, 'bmm', 'workflows', '0-context');
|
||||
await fs.copy(sourceContextDir33, targetContextDir33);
|
||||
await installer33.ensureProjectGitignore(projectDir33);
|
||||
const gitignore33 = await fs.readFile(path.join(projectDir33, '.gitignore'), 'utf8');
|
||||
assert(gitignore33.includes('_bmad/.current_project'), 'Installer creates a .gitignore entry for local monorepo state');
|
||||
|
||||
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[]');
|
||||
await installer33.ensureProjectGitignore(projectDir33);
|
||||
const gitignore33Repeat = await fs.readFile(path.join(projectDir33, '.gitignore'), 'utf8');
|
||||
assert(
|
||||
newProjectSkill33 && newProjectSkill33.path.includes('workflows/0-context/bmad-project-new/SKILL.md'),
|
||||
gitignore33Repeat.match(/^_bmad\/\.current_project$/gm)?.length === 1,
|
||||
'Installer does not duplicate the .gitignore entry on repeat runs',
|
||||
);
|
||||
|
||||
const syntheticModule33 = `header: test
|
||||
|
||||
# --- Monorepo / Multi-Project Context Support ---
|
||||
monorepo_context:
|
||||
enabled: true
|
||||
context_file: ".current_project"
|
||||
|
||||
# --- Future Section ---
|
||||
future_key: true
|
||||
`;
|
||||
|
||||
const extractedBlock33 = installer33.extractMonorepoContextBlock(syntheticModule33);
|
||||
assert(extractedBlock33 && extractedBlock33.includes('monorepo_context:'), 'Monorepo block extraction keeps the monorepo section');
|
||||
assert(extractedBlock33 && !extractedBlock33.includes('future_key: true'), 'Monorepo block extraction stops before later sections');
|
||||
assert(
|
||||
installer33.extractMonorepoContextBlock('header: test\n# --- Monorepo / Multi-Project Context Support ---\n# comment only\n') ===
|
||||
null,
|
||||
'Monorepo block extraction ignores comment-only sections without a top-level monorepo_context key',
|
||||
);
|
||||
|
||||
const customModuleDir33 = path.join(tempFixture33, 'custom-bmm');
|
||||
await fs.ensureDir(customModuleDir33);
|
||||
await fs.writeFile(path.join(customModuleDir33, 'module.yaml'), syntheticModule33, 'utf8');
|
||||
installer33.moduleManager.setCustomModulePaths(new Map([['custom-bmm', customModuleDir33]]));
|
||||
const resolvedSchemaPath33 = await installer33.resolveModuleSchemaPath('custom-bmm');
|
||||
assert(
|
||||
resolvedSchemaPath33 === path.join(customModuleDir33, 'module.yaml'),
|
||||
'resolveModuleSchemaPath prefers live moduleManager custom module paths',
|
||||
);
|
||||
|
||||
await fs.ensureDir(path.join(tempFixture33, 'custom-bmm'));
|
||||
await installer33.generateModuleConfigs(tempFixture33, { core: {} });
|
||||
const generatedConfig33 = await fs.readFile(path.join(tempFixture33, 'custom-bmm', 'config.yaml'), 'utf8');
|
||||
assert(generatedConfig33.includes('monorepo_context:'), 'Static config blocks are still emitted for custom modules');
|
||||
assert(
|
||||
!generatedConfig33.includes('\n{}\n'),
|
||||
'Static config blocks replace serialized empty-object YAML instead of appending after {}',
|
||||
);
|
||||
} catch (error) {
|
||||
assert(false, 'Installer monorepo helper-file test succeeds', error.message);
|
||||
} finally {
|
||||
if (tempFixture33) await fs.remove(tempFixture33).catch(() => {});
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// ============================================================
|
||||
// Test 34: Context skills are discoverable as skills
|
||||
// ============================================================
|
||||
console.log(`${colors.yellow}Test Suite 34: Context Skill Discovery${colors.reset}\n`);
|
||||
|
||||
let tempFixture34;
|
||||
try {
|
||||
tempFixture34 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-context-skills-'));
|
||||
await fs.ensureDir(path.join(tempFixture34, '_config'));
|
||||
await fs.ensureDir(path.join(tempFixture34, 'bmm', 'agents'));
|
||||
await fs.writeFile(path.join(tempFixture34, 'bmm', 'agents', 'test.md'), '<agent name="Test" title="T"><persona>p</persona></agent>');
|
||||
|
||||
const sourceContextDir34 = path.join(projectRoot, 'src', 'bmm', 'workflows', '0-context');
|
||||
const targetContextDir34 = path.join(tempFixture34, 'bmm', 'workflows', '0-context');
|
||||
await fs.copy(sourceContextDir34, targetContextDir34);
|
||||
|
||||
const generator34 = new ManifestGenerator();
|
||||
await generator34.generateManifests(tempFixture34, ['bmm'], [], { ides: [] });
|
||||
|
||||
const newProjectSkill34 = generator34.skills.find((skill) => skill.canonicalId === 'bmad-project-new');
|
||||
const setProjectSkill34 = generator34.skills.find((skill) => skill.canonicalId === 'bmad-project-switch');
|
||||
const listProjectsSkill34 = generator34.skills.find((skill) => skill.canonicalId === 'bmad-project-list');
|
||||
|
||||
assert(newProjectSkill34 !== undefined, 'New project skill appears in skills[]');
|
||||
assert(setProjectSkill34 !== undefined, 'Set project context skill appears in skills[]');
|
||||
assert(listProjectsSkill34 !== undefined, 'List projects skill appears in skills[]');
|
||||
assert(
|
||||
newProjectSkill34 && newProjectSkill34.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'),
|
||||
setProjectSkill34 && setProjectSkill34.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'),
|
||||
listProjectsSkill34 && listProjectsSkill34.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')),
|
||||
!generator34.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')),
|
||||
!generator34.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')),
|
||||
!generator34.workflows.some((workflow) => workflow.path.includes('0-context/bmad-project-list')),
|
||||
'List projects skill does not appear in workflows[]',
|
||||
);
|
||||
assert(
|
||||
!generator34.skills.some((skill) => skill.canonicalId === '.current_project'),
|
||||
'Manifest generation is unchanged when no mutable current-project file exists in the fixture',
|
||||
);
|
||||
} catch (error) {
|
||||
assert(false, 'Context skill discovery test succeeds', error.message);
|
||||
} finally {
|
||||
if (tempFixture33) await fs.remove(tempFixture33).catch(() => {});
|
||||
if (tempFixture34) await fs.remove(tempFixture34).catch(() => {});
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
|
|
|||
|
|
@ -816,6 +816,7 @@ class Installer {
|
|||
// Create bmad directory structure
|
||||
spinner.message('Creating directory structure...');
|
||||
await this.createDirectoryStructure(bmadDir);
|
||||
await this.ensureProjectGitignore(projectDir);
|
||||
|
||||
// Cache custom modules if any
|
||||
if (customModulePaths && customModulePaths.size > 0) {
|
||||
|
|
@ -1455,6 +1456,8 @@ class Installer {
|
|||
throw new Error(`No BMAD installation found at ${bmadDir}`);
|
||||
}
|
||||
|
||||
await this.ensureProjectGitignore(projectDir);
|
||||
|
||||
spinner.message('Analyzing update requirements...');
|
||||
|
||||
// Compare versions and determine what needs updating
|
||||
|
|
@ -1981,6 +1984,35 @@ class Installer {
|
|||
await fs.ensureDir(path.join(bmadDir, '_config', 'custom'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure local-only BMAD state is ignored by version control.
|
||||
* @param {string} projectDir - Project root directory
|
||||
*/
|
||||
async ensureProjectGitignore(projectDir) {
|
||||
const gitignorePath = path.join(projectDir, '.gitignore');
|
||||
const ignoreEntry = `${BMAD_FOLDER_NAME}/.current_project`;
|
||||
|
||||
let existingContent = '';
|
||||
if (await fs.pathExists(gitignorePath)) {
|
||||
existingContent = await fs.readFile(gitignorePath, 'utf8');
|
||||
}
|
||||
|
||||
const existingLines = new Set(
|
||||
existingContent
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
|
||||
if (existingLines.has(ignoreEntry) || existingLines.has(`/${ignoreEntry}`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const separator = existingContent === '' || existingContent.endsWith('\n') ? '' : '\n';
|
||||
const nextContent = `${existingContent}${separator}${ignoreEntry}\n`;
|
||||
await fs.writeFile(gitignorePath, nextContent, 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate clean config.yaml files for each installed module
|
||||
* @param {string} bmadDir - BMAD installation directory
|
||||
|
|
@ -2069,8 +2101,13 @@ class Installer {
|
|||
|
||||
const staticConfigBlocks = await this.getStaticConfigBlocks(moduleName);
|
||||
if (staticConfigBlocks.length > 0) {
|
||||
yamlContent = yamlContent.trimEnd();
|
||||
yamlContent += `\n\n${staticConfigBlocks.join('\n\n')}\n`;
|
||||
const trimmedYamlContent = yamlContent.trim();
|
||||
if (trimmedYamlContent === '' || trimmedYamlContent === '{}') {
|
||||
yamlContent = `${staticConfigBlocks.join('\n\n')}\n`;
|
||||
} else {
|
||||
yamlContent = yamlContent.trimEnd();
|
||||
yamlContent += `\n\n${staticConfigBlocks.join('\n\n')}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Write the clean config file with POSIX-compliant final newline
|
||||
|
|
@ -2106,6 +2143,10 @@ class Installer {
|
|||
* @returns {Promise<string|null>} Path to module.yaml or null if not found
|
||||
*/
|
||||
async resolveModuleSchemaPath(moduleName) {
|
||||
if (this.moduleManager.customModulePaths?.has(moduleName)) {
|
||||
return path.join(this.moduleManager.customModulePaths.get(moduleName), 'module.yaml');
|
||||
}
|
||||
|
||||
if (this.configCollector.customModulePaths?.has(moduleName)) {
|
||||
return path.join(this.configCollector.customModulePaths.get(moduleName), 'module.yaml');
|
||||
}
|
||||
|
|
@ -2125,17 +2166,17 @@ class Installer {
|
|||
* @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();
|
||||
const afterHeader = content.slice(headerIndex);
|
||||
const nextSectionMatch = afterHeader.slice(1).match(/\n# --- .+ ---/);
|
||||
const endIndex = nextSectionMatch ? headerIndex + 1 + nextSectionMatch.index : content.length;
|
||||
const block = content.slice(headerIndex, endIndex).trimEnd();
|
||||
|
||||
// Sanity check: the block must contain the structured YAML key
|
||||
if (!block.includes('monorepo_context:')) {
|
||||
if (!/^\s*monorepo_context\s*:/m.test(block)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue