Compare commits
9 Commits
a95b3765f3
...
c6aa986d16
| Author | SHA1 | Date |
|---|---|---|
|
|
c6aa986d16 | |
|
|
dd8c54cc9f | |
|
|
4ded43d707 | |
|
|
181aeac04a | |
|
|
f7466c2530 | |
|
|
0c660aa0e9 | |
|
|
b46e20dc3a | |
|
|
6bf6a2b6d2 | |
|
|
6dc292c850 |
|
|
@ -45,10 +45,12 @@
|
|||
"release:minor": "gh workflow run \"Manual Release\" -f version_bump=minor",
|
||||
"release:patch": "gh workflow run \"Manual Release\" -f version_bump=patch",
|
||||
"release:watch": "gh run watch",
|
||||
"test": "npm run test:schemas && npm run test:install && npm run validate:schemas && npm run lint && npm run lint:md && npm run format:check",
|
||||
"test": "npm run test:schemas && npm run test:modules && npm run test:install && npm run validate:schemas && npm run validate:modules && npm run lint && npm run lint:md && npm run format:check",
|
||||
"test:coverage": "c8 --reporter=text --reporter=html npm run test:schemas",
|
||||
"test:install": "node test/test-installation-components.js",
|
||||
"test:modules": "node test/test-module-schema.js",
|
||||
"test:schemas": "node test/test-agent-schema.js",
|
||||
"validate:modules": "node tools/validate-module-schema.js",
|
||||
"validate:schemas": "node tools/validate-agent-schema.js"
|
||||
},
|
||||
"lint-staged": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
code: bmm
|
||||
name: "BMad Method Agile-AI Driven-Development"
|
||||
description: "AI-driven agile development framework"
|
||||
header: "AI-driven agile development framework"
|
||||
subheader: "Configure the BMad Method module for AI-powered agile project development"
|
||||
default_selected: true # This module will be selected by default for new installations
|
||||
|
||||
# Variables from Core Config inserted:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ description: 'Discovery & Understanding - Understand what user wants to edit and
|
|||
|
||||
# File references (ONLY variables used in this step)
|
||||
altStepFile: './step-e-01b-legacy-conversion.md'
|
||||
prdPurpose: '{project-root}/src/bmm/workflows/2-plan-workflows/create-prd/data/prd-purpose.md'
|
||||
prdPurpose: '{project-root}/_bmad/bmm/workflows/2-plan-workflows/create-prd/data/prd-purpose.md'
|
||||
advancedElicitationTask: '{project-root}/_bmad/core/workflows/advanced-elicitation/workflow.xml'
|
||||
partyModeWorkflow: '{project-root}/_bmad/core/workflows/party-mode/workflow.md'
|
||||
---
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ description: 'Legacy PRD Conversion Assessment - Analyze legacy PRD and propose
|
|||
# File references (ONLY variables used in this step)
|
||||
nextStepFile: './step-e-02-review.md'
|
||||
prdFile: '{prd_file_path}'
|
||||
prdPurpose: '{project-root}/src/bmm/workflows/2-plan-workflows/create-prd/data/prd-purpose.md'
|
||||
prdPurpose: '{project-root}/_bmad/bmm/workflows/2-plan-workflows/create-prd/data/prd-purpose.md'
|
||||
---
|
||||
|
||||
# Step E-1B: Legacy PRD Conversion Assessment
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ description: 'Deep Review & Analysis - Thoroughly review existing PRD and prepar
|
|||
nextStepFile: './step-e-03-edit.md'
|
||||
prdFile: '{prd_file_path}'
|
||||
validationReport: '{validation_report_path}' # If provided
|
||||
prdPurpose: '{project-root}/src/bmm/workflows/2-plan-workflows/create-prd/data/prd-purpose.md'
|
||||
prdPurpose: '{project-root}/_bmad/bmm/workflows/2-plan-workflows/create-prd/data/prd-purpose.md'
|
||||
advancedElicitationTask: '{project-root}/_bmad/core/workflows/advanced-elicitation/workflow.xml'
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ description: 'Edit & Update - Apply changes to PRD following approved change pla
|
|||
# File references (ONLY variables used in this step)
|
||||
nextStepFile: './step-e-04-complete.md'
|
||||
prdFile: '{prd_file_path}'
|
||||
prdPurpose: '{project-root}/src/bmm/workflows/2-plan-workflows/create-prd/data/prd-purpose.md'
|
||||
prdPurpose: '{project-root}/_bmad/bmm/workflows/2-plan-workflows/create-prd/data/prd-purpose.md'
|
||||
---
|
||||
|
||||
# Step E-3: Edit & Update
|
||||
|
|
|
|||
11
test/fixtures/module-schema/invalid/code-format/number-start-code.module.yaml
vendored
Normal file
11
test/fixtures/module-schema/invalid/code-format/number-start-code.module.yaml
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Test: Code field starts with a number
|
||||
# Expected: FAIL
|
||||
# Error code: invalid_string
|
||||
# Error path: code
|
||||
|
||||
code: 123-module
|
||||
name: Test Module
|
||||
header: Test Header
|
||||
subheader: Test Subheader
|
||||
default_selected: false
|
||||
|
||||
11
test/fixtures/module-schema/invalid/code-format/placeholder-code.module.yaml
vendored
Normal file
11
test/fixtures/module-schema/invalid/code-format/placeholder-code.module.yaml
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Test: Code field contains placeholder text (the main bug we're fixing)
|
||||
# Expected: FAIL
|
||||
# Error code: invalid_string
|
||||
# Error path: code
|
||||
|
||||
code: "{module_code}"
|
||||
name: Test Module
|
||||
header: Test Header
|
||||
subheader: Test Subheader
|
||||
default_selected: false
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# Test: Code field too short (minimum 2 characters)
|
||||
# Expected: FAIL
|
||||
# Error code: invalid_string
|
||||
# Error path: code
|
||||
|
||||
code: x
|
||||
name: Test Module
|
||||
header: Test Header
|
||||
subheader: Test Subheader
|
||||
default_selected: false
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# Test: Code field with underscores (should be kebab-case)
|
||||
# Expected: FAIL
|
||||
# Error code: invalid_string
|
||||
# Error path: code
|
||||
|
||||
code: test_module
|
||||
name: Test Module
|
||||
header: Test Header
|
||||
subheader: Test Subheader
|
||||
default_selected: false
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# Test: Code field with uppercase letters
|
||||
# Expected: FAIL
|
||||
# Error code: invalid_string
|
||||
# Error path: code
|
||||
|
||||
code: TestModule
|
||||
name: Test Module
|
||||
header: Test Header
|
||||
subheader: Test Subheader
|
||||
default_selected: false
|
||||
|
||||
11
test/fixtures/module-schema/invalid/required-fields/missing-code.module.yaml
vendored
Normal file
11
test/fixtures/module-schema/invalid/required-fields/missing-code.module.yaml
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Test: Missing required code field
|
||||
# Expected: FAIL
|
||||
# Error code: invalid_type
|
||||
# Error path: code
|
||||
# Error message: Required
|
||||
|
||||
name: Test Module
|
||||
header: Test Header
|
||||
subheader: Test Subheader
|
||||
default_selected: false
|
||||
|
||||
11
test/fixtures/module-schema/invalid/required-fields/missing-header.module.yaml
vendored
Normal file
11
test/fixtures/module-schema/invalid/required-fields/missing-header.module.yaml
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Test: Missing required header field
|
||||
# Expected: FAIL
|
||||
# Error code: invalid_type
|
||||
# Error path: header
|
||||
# Error message: Required
|
||||
|
||||
code: test-module
|
||||
name: Test Module
|
||||
subheader: Test Subheader
|
||||
default_selected: false
|
||||
|
||||
11
test/fixtures/module-schema/invalid/required-fields/missing-name.module.yaml
vendored
Normal file
11
test/fixtures/module-schema/invalid/required-fields/missing-name.module.yaml
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Test: Missing required name field
|
||||
# Expected: FAIL
|
||||
# Error code: invalid_type
|
||||
# Error path: name
|
||||
# Error message: Required
|
||||
|
||||
code: test-module
|
||||
header: Test Header
|
||||
subheader: Test Subheader
|
||||
default_selected: false
|
||||
|
||||
11
test/fixtures/module-schema/invalid/required-fields/missing-subheader.module.yaml
vendored
Normal file
11
test/fixtures/module-schema/invalid/required-fields/missing-subheader.module.yaml
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Test: Missing required subheader field
|
||||
# Expected: FAIL
|
||||
# Error code: invalid_type
|
||||
# Error path: subheader
|
||||
# Error message: Required
|
||||
|
||||
code: test-module
|
||||
name: Test Module
|
||||
header: Test Header
|
||||
default_selected: false
|
||||
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# Test: Variable with empty prompt
|
||||
# Expected: FAIL
|
||||
# Error code: custom
|
||||
# Error path: my_variable
|
||||
# Error message: my_variable.prompt must be a non-empty string
|
||||
|
||||
code: test-module
|
||||
name: Test Module
|
||||
header: Test Header
|
||||
subheader: Test Subheader
|
||||
default_selected: false
|
||||
|
||||
my_variable:
|
||||
prompt: " "
|
||||
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# Test: Variable without prompt or inherit
|
||||
# Expected: FAIL
|
||||
# Error code: custom
|
||||
# Error path: my_variable
|
||||
# Error message: my_variable must have a 'prompt' or 'inherit' field
|
||||
|
||||
code: test-module
|
||||
name: Test Module
|
||||
header: Test Header
|
||||
subheader: Test Subheader
|
||||
default_selected: false
|
||||
|
||||
my_variable:
|
||||
required: true
|
||||
result: some result
|
||||
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# Test: Valid module with default_selected set to true
|
||||
# Expected: PASS
|
||||
|
||||
code: core-like
|
||||
name: Core Module
|
||||
header: Core Header
|
||||
subheader: Module with default_selected true
|
||||
default_selected: true
|
||||
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# Test: Valid module with only required fields
|
||||
# Expected: PASS
|
||||
|
||||
code: my-module
|
||||
name: My Module Name
|
||||
header: Module Header
|
||||
subheader: Short description of what this module does
|
||||
default_selected: false
|
||||
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# Test: Valid module with minimum 2-character code
|
||||
# Expected: PASS
|
||||
|
||||
code: ab
|
||||
name: Two Letter Module
|
||||
header: Short Code Header
|
||||
subheader: The shortest valid code is 2 characters
|
||||
default_selected: true
|
||||
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# Test: Valid module with inherit variable
|
||||
# Expected: PASS
|
||||
|
||||
code: inherit-mod
|
||||
name: Inherit Module
|
||||
header: Module With Inherited Variables
|
||||
subheader: Uses inherit instead of prompt
|
||||
default_selected: false
|
||||
|
||||
inherited_var:
|
||||
inherit: other-module.some_var
|
||||
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# Test: Valid module with variable definition (prompt style)
|
||||
# Expected: PASS
|
||||
|
||||
code: var-module
|
||||
name: Variable Module
|
||||
header: Module With Variables
|
||||
subheader: Demonstrates variable definitions
|
||||
default_selected: false
|
||||
|
||||
project_name:
|
||||
prompt: What is your project name?
|
||||
required: true
|
||||
result: The project name is {project_name}
|
||||
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# Test: Valid module with single-select and multi-select
|
||||
# Expected: PASS
|
||||
|
||||
code: select-mod
|
||||
name: Select Module
|
||||
header: Module With Selects
|
||||
subheader: Demonstrates single-select and multi-select
|
||||
default_selected: false
|
||||
|
||||
environment:
|
||||
prompt: Select your target environment
|
||||
single-select:
|
||||
- value: dev
|
||||
label: Development
|
||||
- value: staging
|
||||
label: Staging
|
||||
- value: prod
|
||||
label: Production
|
||||
|
||||
features:
|
||||
prompt:
|
||||
- What features do you want?
|
||||
- You can select multiple options.
|
||||
multi-select:
|
||||
- value: auth
|
||||
label: Authentication
|
||||
- value: api
|
||||
label: REST API
|
||||
- value: db
|
||||
label: Database
|
||||
|
||||
|
|
@ -0,0 +1,345 @@
|
|||
/**
|
||||
* Module Schema Validation Test Runner
|
||||
*
|
||||
* Runs all test fixtures and verifies expected outcomes.
|
||||
* Reports pass/fail for each test and overall coverage statistics.
|
||||
*
|
||||
* Usage: node test/test-module-schema.js
|
||||
* Exit codes: 0 = all tests pass, 1 = test failures
|
||||
*/
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const yaml = require('yaml');
|
||||
const { validateModuleFile } = require('../tools/schema/module.js');
|
||||
const { glob } = require('glob');
|
||||
|
||||
// ANSI color codes
|
||||
const colors = {
|
||||
reset: '\u001B[0m',
|
||||
green: '\u001B[32m',
|
||||
red: '\u001B[31m',
|
||||
yellow: '\u001B[33m',
|
||||
blue: '\u001B[34m',
|
||||
cyan: '\u001B[36m',
|
||||
dim: '\u001B[2m',
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse test metadata from YAML comments
|
||||
* @param {string} filePath
|
||||
* @returns {{shouldPass: boolean, errorExpectation?: object}}
|
||||
*/
|
||||
function parseTestMetadata(filePath) {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
let shouldPass = true;
|
||||
const errorExpectation = {};
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes('Expected: PASS')) {
|
||||
shouldPass = true;
|
||||
} else if (line.includes('Expected: FAIL')) {
|
||||
shouldPass = false;
|
||||
}
|
||||
|
||||
// Parse error metadata
|
||||
const codeMatch = line.match(/^# Error code: (.+)$/);
|
||||
if (codeMatch) {
|
||||
errorExpectation.code = codeMatch[1].trim();
|
||||
}
|
||||
|
||||
const pathMatch = line.match(/^# Error path: (.+)$/);
|
||||
if (pathMatch) {
|
||||
errorExpectation.path = pathMatch[1].trim();
|
||||
}
|
||||
|
||||
const messageMatch = line.match(/^# Error message: (.+)$/);
|
||||
if (messageMatch) {
|
||||
errorExpectation.message = messageMatch[1].trim();
|
||||
}
|
||||
|
||||
const minimumMatch = line.match(/^# Error minimum: (\d+)$/);
|
||||
if (minimumMatch) {
|
||||
errorExpectation.minimum = parseInt(minimumMatch[1], 10);
|
||||
}
|
||||
|
||||
const expectedMatch = line.match(/^# Error expected: (.+)$/);
|
||||
if (expectedMatch) {
|
||||
errorExpectation.expected = expectedMatch[1].trim();
|
||||
}
|
||||
|
||||
const receivedMatch = line.match(/^# Error received: (.+)$/);
|
||||
if (receivedMatch) {
|
||||
errorExpectation.received = receivedMatch[1].trim();
|
||||
}
|
||||
|
||||
const keysMatch = line.match(/^# Error keys: \[(.+)\]$/);
|
||||
if (keysMatch) {
|
||||
errorExpectation.keys = keysMatch[1].split(',').map((k) => k.trim().replaceAll(/['"]/g, ''));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
shouldPass,
|
||||
errorExpectation: Object.keys(errorExpectation).length > 0 ? errorExpectation : null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert dot-notation path string to array (handles array indices)
|
||||
* e.g., "module.dependencies[0]" => ["module", "dependencies", 0]
|
||||
*/
|
||||
function parsePathString(pathString) {
|
||||
return pathString
|
||||
.replaceAll(/\[(\d+)\]/g, '.$1')
|
||||
.split('.')
|
||||
.map((part) => {
|
||||
const num = parseInt(part, 10);
|
||||
return isNaN(num) ? part : num;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate error against expectations
|
||||
* @param {object} error - Zod error issue
|
||||
* @param {object} expectation - Expected error structure
|
||||
* @returns {{valid: boolean, reason?: string}}
|
||||
*/
|
||||
function validateError(error, expectation) {
|
||||
if (expectation.code && error.code !== expectation.code) {
|
||||
return { valid: false, reason: `Expected code "${expectation.code}", got "${error.code}"` };
|
||||
}
|
||||
|
||||
if (expectation.path) {
|
||||
const expectedPath = parsePathString(expectation.path);
|
||||
const actualPath = error.path;
|
||||
|
||||
if (JSON.stringify(expectedPath) !== JSON.stringify(actualPath)) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: `Expected path ${JSON.stringify(expectedPath)}, got ${JSON.stringify(actualPath)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (expectation.code === 'custom' && expectation.message && error.message !== expectation.message) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: `Expected message "${expectation.message}", got "${error.message}"`,
|
||||
};
|
||||
}
|
||||
|
||||
if (expectation.minimum !== undefined && error.minimum !== expectation.minimum) {
|
||||
return { valid: false, reason: `Expected minimum ${expectation.minimum}, got ${error.minimum}` };
|
||||
}
|
||||
|
||||
if (expectation.expected && error.expected !== expectation.expected) {
|
||||
return { valid: false, reason: `Expected type "${expectation.expected}", got "${error.expected}"` };
|
||||
}
|
||||
|
||||
if (expectation.received && error.received !== expectation.received) {
|
||||
return { valid: false, reason: `Expected received "${expectation.received}", got "${error.received}"` };
|
||||
}
|
||||
|
||||
if (expectation.keys) {
|
||||
const expectedKeys = expectation.keys.sort();
|
||||
const actualKeys = (error.keys || []).sort();
|
||||
if (JSON.stringify(expectedKeys) !== JSON.stringify(actualKeys)) {
|
||||
return {
|
||||
valid: false,
|
||||
reason: `Expected keys ${JSON.stringify(expectedKeys)}, got ${JSON.stringify(actualKeys)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a single test case
|
||||
* @param {string} filePath
|
||||
* @returns {{passed: boolean, message: string}}
|
||||
*/
|
||||
function runTest(filePath) {
|
||||
try {
|
||||
const metadata = parseTestMetadata(filePath);
|
||||
const { shouldPass, errorExpectation } = metadata;
|
||||
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
let moduleData;
|
||||
|
||||
try {
|
||||
moduleData = yaml.parse(fileContent);
|
||||
} catch (parseError) {
|
||||
if (shouldPass) {
|
||||
return {
|
||||
passed: false,
|
||||
message: `Expected PASS but got YAML parse error: ${parseError.message}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
passed: true,
|
||||
message: 'Got expected YAML parse error',
|
||||
};
|
||||
}
|
||||
|
||||
const result = validateModuleFile(filePath, moduleData);
|
||||
|
||||
if (result.success && shouldPass) {
|
||||
return {
|
||||
passed: true,
|
||||
message: 'Validation passed as expected',
|
||||
};
|
||||
}
|
||||
|
||||
if (!result.success && !shouldPass) {
|
||||
const actualError = result.error?.issues?.[0];
|
||||
if (!actualError) {
|
||||
return {
|
||||
passed: false,
|
||||
message: 'Expected validation error issues, but validator returned none',
|
||||
};
|
||||
}
|
||||
|
||||
if (errorExpectation) {
|
||||
const validation = validateError(actualError, errorExpectation);
|
||||
|
||||
if (!validation.valid) {
|
||||
return {
|
||||
passed: false,
|
||||
message: `Error validation failed: ${validation.reason}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
passed: true,
|
||||
message: `Got expected error (${errorExpectation.code}): ${actualError.message}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
passed: true,
|
||||
message: `Got expected validation error: ${actualError.message}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (result.success && !shouldPass) {
|
||||
return {
|
||||
passed: false,
|
||||
message: 'Expected validation to FAIL but it PASSED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!result.success && shouldPass) {
|
||||
return {
|
||||
passed: false,
|
||||
message: `Expected validation to PASS but it FAILED: ${result.error?.issues?.[0]?.message ?? 'Unknown error'}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
passed: false,
|
||||
message: 'Unexpected test state',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
passed: false,
|
||||
message: `Test execution error: ${error.message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main test runner
|
||||
*/
|
||||
async function main() {
|
||||
console.log(`${colors.cyan}╔═══════════════════════════════════════════════════════════╗${colors.reset}`);
|
||||
console.log(`${colors.cyan}║ Module Schema Validation Test Suite ║${colors.reset}`);
|
||||
console.log(`${colors.cyan}╚═══════════════════════════════════════════════════════════╝${colors.reset}\n`);
|
||||
|
||||
const testFiles = await glob('test/fixtures/module-schema/**/*.module.yaml', {
|
||||
cwd: path.join(__dirname, '..'),
|
||||
absolute: true,
|
||||
});
|
||||
|
||||
if (testFiles.length === 0) {
|
||||
console.log(`${colors.yellow}⚠️ No test fixtures found${colors.reset}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Found ${colors.cyan}${testFiles.length}${colors.reset} test fixture(s)\n`);
|
||||
|
||||
// Group tests by category
|
||||
const categories = {};
|
||||
for (const testFile of testFiles) {
|
||||
const relativePath = path.relative(path.join(__dirname, 'fixtures/module-schema'), testFile);
|
||||
const parts = relativePath.split(path.sep);
|
||||
const validInvalid = parts[0]; // 'valid' or 'invalid'
|
||||
const category = parts[1] || 'general';
|
||||
|
||||
const categoryKey = `${validInvalid}/${category}`;
|
||||
if (!categories[categoryKey]) {
|
||||
categories[categoryKey] = [];
|
||||
}
|
||||
categories[categoryKey].push(testFile);
|
||||
}
|
||||
|
||||
// Run tests by category
|
||||
let totalTests = 0;
|
||||
let passedTests = 0;
|
||||
const failures = [];
|
||||
|
||||
for (const [categoryKey, files] of Object.entries(categories).sort()) {
|
||||
const [validInvalid, category] = categoryKey.split('/');
|
||||
const categoryLabel = category.replaceAll('-', ' ').toUpperCase();
|
||||
const validLabel = validInvalid === 'valid' ? '✅' : '❌';
|
||||
|
||||
console.log(`${colors.blue}${validLabel} ${categoryLabel} (${validInvalid})${colors.reset}`);
|
||||
|
||||
for (const testFile of files) {
|
||||
totalTests++;
|
||||
const testName = path.basename(testFile, '.module.yaml');
|
||||
const result = runTest(testFile);
|
||||
|
||||
if (result.passed) {
|
||||
passedTests++;
|
||||
console.log(` ${colors.green}✓${colors.reset} ${testName} ${colors.dim}${result.message}${colors.reset}`);
|
||||
} else {
|
||||
console.log(` ${colors.red}✗${colors.reset} ${testName} ${colors.red}${result.message}${colors.reset}`);
|
||||
failures.push({
|
||||
file: path.relative(process.cwd(), testFile),
|
||||
message: result.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log(`${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}`);
|
||||
console.log(`${colors.cyan}Test Results:${colors.reset}`);
|
||||
console.log(` Total: ${totalTests}`);
|
||||
console.log(` Passed: ${colors.green}${passedTests}${colors.reset}`);
|
||||
console.log(` Failed: ${passedTests === totalTests ? colors.green : colors.red}${totalTests - passedTests}${colors.reset}`);
|
||||
console.log(`${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}\n`);
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.log(`${colors.red}❌ FAILED TESTS:${colors.reset}\n`);
|
||||
for (const failure of failures) {
|
||||
console.log(`${colors.red}✗${colors.reset} ${failure.file}`);
|
||||
console.log(` ${failure.message}\n`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`${colors.green}✨ All tests passed!${colors.reset}\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Run
|
||||
main().catch((error) => {
|
||||
console.error(`${colors.red}Fatal error:${colors.reset}`, error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -32,6 +32,16 @@ modules:
|
|||
type: bmad-org
|
||||
npmPackage: bmad-game-dev-studio
|
||||
|
||||
bmad-method-test-architecture-enterprise:
|
||||
url: https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise
|
||||
module-definition: src/module.yaml
|
||||
code: tea
|
||||
name: "Test Architect"
|
||||
description: "Master Test Architect for quality strategy, test automation, and release gates"
|
||||
defaultSelected: false
|
||||
type: bmad-org
|
||||
npmPackage: bmad-method-test-architecture-enterprise
|
||||
|
||||
# TODO: Enable once fixes applied:
|
||||
|
||||
# whiteport-design-system:
|
||||
|
|
|
|||
|
|
@ -132,12 +132,12 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
|||
*/
|
||||
async writeAgentArtifacts(targetPath, artifacts, templateType, config = {}) {
|
||||
// Try to load platform-specific template, fall back to default-agent
|
||||
const template = await this.loadTemplate(templateType, 'agent', config, 'default-agent');
|
||||
const { content: template, extension } = await this.loadTemplate(templateType, 'agent', config, 'default-agent');
|
||||
let count = 0;
|
||||
|
||||
for (const artifact of artifacts) {
|
||||
const content = this.renderTemplate(template, artifact);
|
||||
const filename = this.generateFilename(artifact, 'agent');
|
||||
const filename = this.generateFilename(artifact, 'agent', extension);
|
||||
const filePath = path.join(targetPath, filename);
|
||||
await this.writeFile(filePath, content);
|
||||
count++;
|
||||
|
|
@ -167,9 +167,10 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
|||
|
||||
// Fall back to default templates if specific ones don't exist
|
||||
const finalTemplateType = artifact.isYamlWorkflow ? 'default-workflow-yaml' : 'default-workflow';
|
||||
const template = await this.loadTemplate(workflowTemplateType, 'workflow', config, finalTemplateType);
|
||||
// workflowTemplateType already contains full name (e.g., 'gemini-workflow-yaml'), so pass empty artifactType
|
||||
const { content: template, extension } = await this.loadTemplate(workflowTemplateType, '', config, finalTemplateType);
|
||||
const content = this.renderTemplate(template, artifact);
|
||||
const filename = this.generateFilename(artifact, 'workflow');
|
||||
const filename = this.generateFilename(artifact, 'workflow', extension);
|
||||
const filePath = path.join(targetPath, filename);
|
||||
await this.writeFile(filePath, content);
|
||||
count++;
|
||||
|
|
@ -185,34 +186,47 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
|
|||
* @param {string} artifactType - Artifact type (agent, workflow, task, tool)
|
||||
* @param {Object} config - Installation configuration
|
||||
* @param {string} fallbackTemplateType - Fallback template type if requested template not found
|
||||
* @returns {Promise<string>} Template content
|
||||
* @returns {Promise<{content: string, extension: string}>} Template content and extension
|
||||
*/
|
||||
async loadTemplate(templateType, artifactType, config = {}, fallbackTemplateType = null) {
|
||||
const { header_template, body_template } = config;
|
||||
|
||||
// Check for separate header/body templates
|
||||
if (header_template || body_template) {
|
||||
return await this.loadSplitTemplates(templateType, artifactType, header_template, body_template);
|
||||
const content = await this.loadSplitTemplates(templateType, artifactType, header_template, body_template);
|
||||
// Allow config to override extension, default to .md
|
||||
const ext = config.extension || '.md';
|
||||
const normalizedExt = ext.startsWith('.') ? ext : `.${ext}`;
|
||||
return { content, extension: normalizedExt };
|
||||
}
|
||||
|
||||
// Load combined template
|
||||
const templateName = `${templateType}-${artifactType}.md`;
|
||||
const templatePath = path.join(__dirname, 'templates', 'combined', templateName);
|
||||
// Load combined template - try multiple extensions
|
||||
// If artifactType is empty, templateType already contains full name (e.g., 'gemini-workflow-yaml')
|
||||
const templateBaseName = artifactType ? `${templateType}-${artifactType}` : templateType;
|
||||
const templateDir = path.join(__dirname, 'templates', 'combined');
|
||||
const extensions = ['.md', '.toml', '.yaml', '.yml'];
|
||||
|
||||
if (await fs.pathExists(templatePath)) {
|
||||
return await fs.readFile(templatePath, 'utf8');
|
||||
for (const ext of extensions) {
|
||||
const templatePath = path.join(templateDir, templateBaseName + ext);
|
||||
if (await fs.pathExists(templatePath)) {
|
||||
const content = await fs.readFile(templatePath, 'utf8');
|
||||
return { content, extension: ext };
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to default template (if provided)
|
||||
if (fallbackTemplateType) {
|
||||
const fallbackPath = path.join(__dirname, 'templates', 'combined', `${fallbackTemplateType}.md`);
|
||||
if (await fs.pathExists(fallbackPath)) {
|
||||
return await fs.readFile(fallbackPath, 'utf8');
|
||||
for (const ext of extensions) {
|
||||
const fallbackPath = path.join(templateDir, `${fallbackTemplateType}${ext}`);
|
||||
if (await fs.pathExists(fallbackPath)) {
|
||||
const content = await fs.readFile(fallbackPath, 'utf8');
|
||||
return { content, extension: ext };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ultimate fallback - minimal template
|
||||
return this.getDefaultTemplate(artifactType);
|
||||
return { content: this.getDefaultTemplate(artifactType), extension: '.md' };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -326,13 +340,26 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
|
|||
* Generate filename for artifact
|
||||
* @param {Object} artifact - Artifact data
|
||||
* @param {string} artifactType - Artifact type (agent, workflow, task, tool)
|
||||
* @param {string} extension - File extension to use (e.g., '.md', '.toml')
|
||||
* @returns {string} Generated filename
|
||||
*/
|
||||
generateFilename(artifact, artifactType) {
|
||||
generateFilename(artifact, artifactType, extension = '.md') {
|
||||
const { toDashPath } = require('./shared/path-utils');
|
||||
// toDashPath already handles the .agent.md suffix for agents correctly
|
||||
// No need to add it again here
|
||||
return toDashPath(artifact.relativePath);
|
||||
|
||||
// Reuse central logic to ensure consistent naming conventions
|
||||
const standardName = toDashPath(artifact.relativePath);
|
||||
|
||||
// Clean up potential double extensions from source files (e.g. .yaml.md -> .md)
|
||||
const baseName = standardName.replace(/\.(yaml|yml)\.md$/, '.md');
|
||||
|
||||
// If using default markdown, preserve the bmad-agent- prefix for agents
|
||||
if (extension === '.md') {
|
||||
return baseName;
|
||||
}
|
||||
|
||||
// For other extensions (e.g., .toml), replace .md extension
|
||||
// Note: agent prefix is preserved even with non-markdown extensions
|
||||
return baseName.replace(/\.md$/, extension);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -14,18 +14,14 @@
|
|||
# installer: Installation configuration (optional - omit for custom installers)
|
||||
|
||||
platforms:
|
||||
# ============================================================================
|
||||
# CLI Tools
|
||||
# ============================================================================
|
||||
|
||||
claude-code:
|
||||
name: "Claude Code"
|
||||
preferred: true
|
||||
category: cli
|
||||
description: "Anthropic's official CLI for Claude"
|
||||
antigravity:
|
||||
name: "Google Antigravity"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "Google's AI development environment"
|
||||
installer:
|
||||
target_dir: .claude/commands
|
||||
template_type: default
|
||||
target_dir: .agent/workflows
|
||||
template_type: antigravity
|
||||
|
||||
auggie:
|
||||
name: "Auggie"
|
||||
|
|
@ -36,37 +32,15 @@ platforms:
|
|||
target_dir: .augment/commands
|
||||
template_type: default
|
||||
|
||||
gemini:
|
||||
name: "Gemini CLI"
|
||||
preferred: false
|
||||
claude-code:
|
||||
name: "Claude Code"
|
||||
preferred: true
|
||||
category: cli
|
||||
description: "Google's CLI for Gemini"
|
||||
description: "Anthropic's official CLI for Claude"
|
||||
installer:
|
||||
target_dir: .gemini/commands
|
||||
template_type: gemini
|
||||
|
||||
# ============================================================================
|
||||
# IDEs
|
||||
# ============================================================================
|
||||
|
||||
cursor:
|
||||
name: "Cursor"
|
||||
preferred: true
|
||||
category: ide
|
||||
description: "AI-first code editor"
|
||||
installer:
|
||||
target_dir: .cursor/commands
|
||||
target_dir: .claude/commands
|
||||
template_type: default
|
||||
|
||||
windsurf:
|
||||
name: "Windsurf"
|
||||
preferred: true
|
||||
category: ide
|
||||
description: "AI-powered IDE with cascade flows"
|
||||
installer:
|
||||
target_dir: .windsurf/workflows
|
||||
template_type: windsurf
|
||||
|
||||
cline:
|
||||
name: "Cline"
|
||||
preferred: false
|
||||
|
|
@ -76,15 +50,40 @@ platforms:
|
|||
target_dir: .clinerules/workflows
|
||||
template_type: windsurf
|
||||
|
||||
roo:
|
||||
name: "Roo Cline"
|
||||
codex:
|
||||
name: "Codex"
|
||||
preferred: false
|
||||
category: cli
|
||||
description: "OpenAI Codex integration"
|
||||
# No installer config - uses custom codex.js
|
||||
|
||||
crush:
|
||||
name: "Crush"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "Enhanced Cline fork"
|
||||
description: "AI development assistant"
|
||||
installer:
|
||||
target_dir: .roo/commands
|
||||
target_dir: .crush/commands
|
||||
template_type: default
|
||||
|
||||
cursor:
|
||||
name: "Cursor"
|
||||
preferred: true
|
||||
category: ide
|
||||
description: "AI-first code editor"
|
||||
installer:
|
||||
target_dir: .cursor/commands
|
||||
template_type: default
|
||||
|
||||
gemini:
|
||||
name: "Gemini CLI"
|
||||
preferred: false
|
||||
category: cli
|
||||
description: "Google's CLI for Gemini"
|
||||
installer:
|
||||
target_dir: .gemini/commands
|
||||
template_type: gemini
|
||||
|
||||
github-copilot:
|
||||
name: "GitHub Copilot"
|
||||
preferred: false
|
||||
|
|
@ -99,24 +98,6 @@ platforms:
|
|||
template_type: vscode_settings
|
||||
artifact_types: []
|
||||
|
||||
opencode:
|
||||
name: "OpenCode"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "OpenCode terminal coding assistant"
|
||||
installer:
|
||||
target_dir: .opencode/command
|
||||
template_type: opencode
|
||||
|
||||
crush:
|
||||
name: "Crush"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "AI development assistant"
|
||||
installer:
|
||||
target_dir: .crush/commands
|
||||
template_type: default
|
||||
|
||||
iflow:
|
||||
name: "iFlow"
|
||||
preferred: false
|
||||
|
|
@ -126,6 +107,29 @@ platforms:
|
|||
target_dir: .iflow/commands
|
||||
template_type: default
|
||||
|
||||
kilo:
|
||||
name: "KiloCoder"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "AI coding platform"
|
||||
# No installer config - uses custom kilo.js (creates .kilocodemodes file)
|
||||
|
||||
kiro-cli:
|
||||
name: "Kiro CLI"
|
||||
preferred: false
|
||||
category: cli
|
||||
description: "Kiro command-line interface"
|
||||
# No installer config - uses custom kiro-cli.js (YAML→JSON conversion)
|
||||
|
||||
opencode:
|
||||
name: "OpenCode"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "OpenCode terminal coding assistant"
|
||||
installer:
|
||||
target_dir: .opencode/command
|
||||
template_type: opencode
|
||||
|
||||
qwen:
|
||||
name: "QwenCoder"
|
||||
preferred: false
|
||||
|
|
@ -135,6 +139,15 @@ platforms:
|
|||
target_dir: .qwen/commands
|
||||
template_type: default
|
||||
|
||||
roo:
|
||||
name: "Roo Cline"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "Enhanced Cline fork"
|
||||
installer:
|
||||
target_dir: .roo/commands
|
||||
template_type: default
|
||||
|
||||
rovo-dev:
|
||||
name: "Rovo Dev"
|
||||
preferred: false
|
||||
|
|
@ -153,41 +166,14 @@ platforms:
|
|||
target_dir: .trae/rules
|
||||
template_type: trae
|
||||
|
||||
antigravity:
|
||||
name: "Google Antigravity"
|
||||
preferred: false
|
||||
windsurf:
|
||||
name: "Windsurf"
|
||||
preferred: true
|
||||
category: ide
|
||||
description: "Google's AI development environment"
|
||||
description: "AI-powered IDE with cascade flows"
|
||||
installer:
|
||||
target_dir: .agent/workflows
|
||||
template_type: antigravity
|
||||
# Note: Antigravity uses .agent/workflows/ directory (not .antigravity/)
|
||||
|
||||
# ============================================================================
|
||||
# Custom Installers (no installer config - use custom file)
|
||||
# These have unique installation logic that doesn't fit the config-driven model
|
||||
# ============================================================================
|
||||
|
||||
codex:
|
||||
name: "Codex"
|
||||
preferred: false
|
||||
category: cli
|
||||
description: "OpenAI Codex integration"
|
||||
# No installer config - uses custom codex.js
|
||||
|
||||
kilo:
|
||||
name: "KiloCoder"
|
||||
preferred: false
|
||||
category: ide
|
||||
description: "AI coding platform"
|
||||
# No installer config - uses custom kilo.js (creates .kilocodemodes file)
|
||||
|
||||
kiro-cli:
|
||||
name: "Kiro CLI"
|
||||
preferred: false
|
||||
category: cli
|
||||
description: "Kiro command-line interface"
|
||||
# No installer config - uses custom kiro-cli.js (YAML→JSON conversion)
|
||||
target_dir: .windsurf/workflows
|
||||
template_type: windsurf
|
||||
|
||||
# ============================================================================
|
||||
# Installer Config Schema
|
||||
|
|
|
|||
|
|
@ -32,7 +32,9 @@ class AgentCommandGenerator {
|
|||
// Use relativePath if available (for nested agents), otherwise just name with .md
|
||||
const agentPathInModule = agent.relativePath || `${agent.name}.md`;
|
||||
// Calculate the relative agent path (e.g., bmm/agents/pm.md)
|
||||
let agentRelPath = agent.path;
|
||||
let agentRelPath = agent.path || '';
|
||||
// Normalize path separators for cross-platform compatibility
|
||||
agentRelPath = agentRelPath.replaceAll('\\', '/');
|
||||
// Remove _bmad/ prefix if present to get relative path from project root
|
||||
// Handle both absolute paths (/path/to/_bmad/...) and relative paths (_bmad/...)
|
||||
if (agentRelPath.includes('_bmad/')) {
|
||||
|
|
@ -132,9 +134,9 @@ class AgentCommandGenerator {
|
|||
|
||||
/**
|
||||
* Write agent launcher artifacts using dash format (NEW STANDARD)
|
||||
* Creates flat files like: bmad-bmm-pm.agent.md
|
||||
* Creates flat files like: bmad-agent-bmm-pm.md
|
||||
*
|
||||
* The .agent.md suffix distinguishes agents from workflows/tasks/tools.
|
||||
* The bmad-agent- prefix distinguishes agents from workflows/tasks/tools.
|
||||
*
|
||||
* @param {string} baseCommandsDir - Base commands directory for the IDE
|
||||
* @param {Array} artifacts - Agent launcher artifacts
|
||||
|
|
@ -145,7 +147,7 @@ class AgentCommandGenerator {
|
|||
|
||||
for (const artifact of artifacts) {
|
||||
if (artifact.type === 'agent-launcher') {
|
||||
// Convert relativePath to dash format: bmm/agents/pm.md → bmad-bmm-pm.agent.md
|
||||
// Convert relativePath to dash format: bmm/agents/pm.md → bmad-agent-bmm-pm.md
|
||||
const flatName = toDashPath(artifact.relativePath);
|
||||
const launcherPath = path.join(baseCommandsDir, flatName);
|
||||
await fs.ensureDir(path.dirname(launcherPath));
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@
|
|||
* Provides utilities to convert hierarchical paths to flat naming conventions.
|
||||
*
|
||||
* DASH-BASED NAMING (new standard):
|
||||
* - Agents: bmad-module-name.agent.md (with .agent.md suffix)
|
||||
* - Agents: bmad-agent-module-name.md (with bmad-agent- prefix)
|
||||
* - Workflows/Tasks/Tools: bmad-module-name.md
|
||||
*
|
||||
* Example outputs:
|
||||
* - cis/agents/storymaster.md → bmad-cis-storymaster.agent.md
|
||||
* - cis/agents/storymaster.md → bmad-agent-cis-storymaster.md
|
||||
* - bmm/workflows/plan-project.md → bmad-bmm-plan-project.md
|
||||
* - bmm/tasks/create-story.md → bmad-bmm-create-story.md
|
||||
* - core/agents/brainstorming.md → bmad-brainstorming.agent.md
|
||||
* - core/agents/brainstorming.md → bmad-agent-brainstorming.md (core agents skip module name)
|
||||
*/
|
||||
|
||||
// Type segments - agents are included in naming, others are filtered out
|
||||
|
|
@ -20,37 +20,38 @@ const AGENT_SEGMENT = 'agents';
|
|||
|
||||
/**
|
||||
* Convert hierarchical path to flat dash-separated name (NEW STANDARD)
|
||||
* Converts: 'bmm', 'agents', 'pm' → 'bmad-bmm-pm.agent.md'
|
||||
* Converts: 'bmm', 'agents', 'pm' → 'bmad-agent-bmm-pm.md'
|
||||
* Converts: 'bmm', 'workflows', 'correct-course' → 'bmad-bmm-correct-course.md'
|
||||
* Converts: 'core', 'agents', 'brainstorming' → 'bmad-brainstorming.agent.md' (core items skip module prefix)
|
||||
* Converts: 'core', 'agents', 'brainstorming' → 'bmad-agent-brainstorming.md' (core agents skip module name)
|
||||
*
|
||||
* @param {string} module - Module name (e.g., 'bmm', 'core')
|
||||
* @param {string} type - Artifact type ('agents', 'workflows', 'tasks', 'tools')
|
||||
* @param {string} name - Artifact name (e.g., 'pm', 'brainstorming')
|
||||
* @returns {string} Flat filename like 'bmad-bmm-pm.agent.md' or 'bmad-bmm-correct-course.md'
|
||||
* @returns {string} Flat filename like 'bmad-agent-bmm-pm.md' or 'bmad-bmm-correct-course.md'
|
||||
*/
|
||||
function toDashName(module, type, name) {
|
||||
const isAgent = type === AGENT_SEGMENT;
|
||||
|
||||
// For core module, skip the module prefix: use 'bmad-name.md' instead of 'bmad-core-name.md'
|
||||
// For core module, skip the module name: use 'bmad-agent-name.md' instead of 'bmad-agent-core-name.md'
|
||||
if (module === 'core') {
|
||||
return isAgent ? `bmad-${name}.agent.md` : `bmad-${name}.md`;
|
||||
return isAgent ? `bmad-agent-${name}.md` : `bmad-${name}.md`;
|
||||
}
|
||||
|
||||
// Module artifacts: bmad-module-name.md or bmad-module-name.agent.md
|
||||
// Module artifacts: bmad-module-name.md or bmad-agent-module-name.md
|
||||
// eslint-disable-next-line unicorn/prefer-string-replace-all -- regex replace is intentional here
|
||||
const dashName = name.replace(/\//g, '-'); // Flatten nested paths
|
||||
return isAgent ? `bmad-${module}-${dashName}.agent.md` : `bmad-${module}-${dashName}.md`;
|
||||
return isAgent ? `bmad-agent-${module}-${dashName}.md` : `bmad-${module}-${dashName}.md`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert relative path to flat dash-separated name
|
||||
* Converts: 'bmm/agents/pm.md' → 'bmad-bmm-pm.agent.md'
|
||||
* Converts: 'bmm/agents/pm.md' → 'bmad-agent-bmm-pm.md'
|
||||
* Converts: 'bmm/agents/tech-writer/tech-writer.md' → 'bmad-agent-bmm-tech-writer.md' (uses folder name)
|
||||
* Converts: 'bmm/workflows/correct-course.md' → 'bmad-bmm-correct-course.md'
|
||||
* Converts: 'core/agents/brainstorming.md' → 'bmad-brainstorming.agent.md' (core items skip module prefix)
|
||||
* Converts: 'core/agents/brainstorming.md' → 'bmad-agent-brainstorming.md' (core agents skip module name)
|
||||
*
|
||||
* @param {string} relativePath - Path like 'bmm/agents/pm.md'
|
||||
* @returns {string} Flat filename like 'bmad-bmm-pm.agent.md' or 'bmad-brainstorming.md'
|
||||
* @returns {string} Flat filename like 'bmad-agent-bmm-pm.md' or 'bmad-brainstorming.md'
|
||||
*/
|
||||
function toDashPath(relativePath) {
|
||||
if (!relativePath || typeof relativePath !== 'string') {
|
||||
|
|
@ -63,20 +64,30 @@ function toDashPath(relativePath) {
|
|||
|
||||
const module = parts[0];
|
||||
const type = parts[1];
|
||||
const name = parts.slice(2).join('-');
|
||||
let name;
|
||||
|
||||
// For agents, if nested in a folder (more than 3 parts), use the folder name only
|
||||
// e.g., 'bmm/agents/tech-writer/tech-writer' → 'tech-writer' (not 'tech-writer-tech-writer')
|
||||
if (type === 'agents' && parts.length > 3) {
|
||||
// Use the folder name (parts[2]) as the name, ignore the file name
|
||||
name = parts[2];
|
||||
} else {
|
||||
// For non-nested or non-agents, join all parts after type
|
||||
name = parts.slice(2).join('-');
|
||||
}
|
||||
|
||||
return toDashName(module, type, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create custom agent dash name
|
||||
* Creates: 'bmad-custom-fred-commit-poet.agent.md'
|
||||
* Creates: 'bmad-custom-agent-fred-commit-poet.md'
|
||||
*
|
||||
* @param {string} agentName - Custom agent name
|
||||
* @returns {string} Flat filename like 'bmad-custom-fred-commit-poet.agent.md'
|
||||
* @returns {string} Flat filename like 'bmad-custom-agent-fred-commit-poet.md'
|
||||
*/
|
||||
function customAgentDashName(agentName) {
|
||||
return `bmad-custom-${agentName}.agent.md`;
|
||||
return `bmad-custom-agent-${agentName}.md`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -90,9 +101,9 @@ function isDashFormat(filename) {
|
|||
|
||||
/**
|
||||
* Extract parts from a dash-formatted filename
|
||||
* Parses: 'bmad-bmm-pm.agent.md' → { prefix: 'bmad', module: 'bmm', type: 'agents', name: 'pm' }
|
||||
* Parses: 'bmad-agent-bmm-pm.md' → { prefix: 'bmad', module: 'bmm', type: 'agents', name: 'pm' }
|
||||
* Parses: 'bmad-bmm-correct-course.md' → { prefix: 'bmad', module: 'bmm', type: 'workflows', name: 'correct-course' }
|
||||
* Parses: 'bmad-brainstorming.agent.md' → { prefix: 'bmad', module: 'core', type: 'agents', name: 'brainstorming' } (core agents)
|
||||
* Parses: 'bmad-agent-brainstorming.md' → { prefix: 'bmad', module: 'core', type: 'agents', name: 'brainstorming' } (core agents)
|
||||
* Parses: 'bmad-brainstorming.md' → { prefix: 'bmad', module: 'core', type: 'workflows', name: 'brainstorming' } (core workflows)
|
||||
*
|
||||
* @param {string} filename - Dash-formatted filename
|
||||
|
|
@ -106,27 +117,27 @@ function parseDashName(filename) {
|
|||
return null;
|
||||
}
|
||||
|
||||
// Check if this is an agent file (has .agent suffix)
|
||||
const isAgent = withoutExt.endsWith('.agent');
|
||||
// Check if this is an agent file (has 'agent' as second part)
|
||||
const isAgent = parts[1] === 'agent';
|
||||
|
||||
if (isAgent) {
|
||||
// This is an agent file
|
||||
// Format: bmad-name.agent (core) or bmad-module-name.agent
|
||||
// Format: bmad-agent-name (core) or bmad-agent-module-name
|
||||
if (parts.length === 3) {
|
||||
// Core agent: bmad-name.agent
|
||||
// Core agent: bmad-agent-name
|
||||
return {
|
||||
prefix: parts[0],
|
||||
module: 'core',
|
||||
type: 'agents',
|
||||
name: parts[1],
|
||||
name: parts[2],
|
||||
};
|
||||
} else {
|
||||
// Module agent: bmad-module-name.agent
|
||||
// Module agent: bmad-agent-module-name
|
||||
return {
|
||||
prefix: parts[0],
|
||||
module: parts[1],
|
||||
module: parts[2],
|
||||
type: 'agents',
|
||||
name: parts.slice(2).join('-'),
|
||||
name: parts.slice(3).join('-'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ Follow all instructions in the ${type} file exactly as written.
|
|||
* Write task/tool artifacts using dash format (NEW STANDARD)
|
||||
* Creates flat files like: bmad-bmm-bmad-help.md
|
||||
*
|
||||
* Note: Tasks/tools do NOT have .agent.md suffix - only agents do.
|
||||
* Note: Tasks/tools do NOT have bmad-agent- prefix - only agents do.
|
||||
*
|
||||
* @param {string} baseCommandsDir - Base commands directory for the IDE
|
||||
* @param {Array} artifacts - Task/tool artifacts with relativePath
|
||||
|
|
|
|||
|
|
@ -68,7 +68,9 @@ class WorkflowCommandGenerator {
|
|||
for (const workflow of allWorkflows) {
|
||||
const commandContent = await this.generateCommandContent(workflow, bmadDir);
|
||||
// Calculate the relative workflow path (e.g., bmm/workflows/4-implementation/sprint-planning/workflow.yaml)
|
||||
let workflowRelPath = workflow.path;
|
||||
let workflowRelPath = workflow.path || '';
|
||||
// Normalize path separators for cross-platform compatibility
|
||||
workflowRelPath = workflowRelPath.replaceAll('\\', '/');
|
||||
// Remove _bmad/ prefix if present to get relative path from project root
|
||||
// Handle both absolute paths (/path/to/_bmad/...) and relative paths (_bmad/...)
|
||||
if (workflowRelPath.includes('_bmad/')) {
|
||||
|
|
@ -76,9 +78,15 @@ class WorkflowCommandGenerator {
|
|||
if (parts.length > 1) {
|
||||
workflowRelPath = parts.slice(1).join('/');
|
||||
}
|
||||
} else if (workflowRelPath.includes('/src/')) {
|
||||
// Normalize source paths (e.g. .../src/bmm/...) to relative module path (e.g. bmm/...)
|
||||
const match = workflowRelPath.match(/\/src\/([^/]+)\/(.+)/);
|
||||
if (match) {
|
||||
workflowRelPath = `${match[1]}/${match[2]}`;
|
||||
}
|
||||
}
|
||||
// Determine if this is a YAML workflow
|
||||
const isYamlWorkflow = workflow.path.endsWith('.yaml') || workflow.path.endsWith('.yml');
|
||||
// Determine if this is a YAML workflow (use normalized path which is guaranteed to be a string)
|
||||
const isYamlWorkflow = workflowRelPath.endsWith('.yaml') || workflowRelPath.endsWith('.yml');
|
||||
artifacts.push({
|
||||
type: 'workflow-command',
|
||||
isYamlWorkflow: isYamlWorkflow, // For template selection
|
||||
|
|
@ -284,7 +292,7 @@ When running any workflow:
|
|||
* Write workflow command artifacts using dash format (NEW STANDARD)
|
||||
* Creates flat files like: bmad-bmm-correct-course.md
|
||||
*
|
||||
* Note: Workflows do NOT have .agent.md suffix - only agents do.
|
||||
* Note: Workflows do NOT have bmad-agent- prefix - only agents do.
|
||||
*
|
||||
* @param {string} baseCommandsDir - Base commands directory for the IDE
|
||||
* @param {Array} artifacts - Workflow artifacts
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
description = "Activates the {{name}} agent from the BMad Method."
|
||||
prompt = """
|
||||
CRITICAL: You are now the BMad '{{name}}' agent.
|
||||
|
||||
PRE-FLIGHT CHECKLIST:
|
||||
1. [ ] IMMEDIATE ACTION: Load and parse {project-root}/{{bmadFolderName}}/{{module}}/config.yaml - store ALL config values in memory for use throughout the session.
|
||||
2. [ ] IMMEDIATE ACTION: Read and internalize the full agent definition at {project-root}/{{bmadFolderName}}/{{path}}.
|
||||
3. [ ] CONFIRM: The user's name from config is {user_name}.
|
||||
|
||||
Only after all checks are complete, greet the user by name and display the menu.
|
||||
Acknowledge this checklist is complete in your first response.
|
||||
|
||||
AGENT DEFINITION: {project-root}/{{bmadFolderName}}/{{path}}
|
||||
"""
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
description = """{{description}}"""
|
||||
prompt = """
|
||||
Execute the BMAD '{{name}}' workflow.
|
||||
|
||||
CRITICAL: This is a structured YAML workflow. Follow these steps precisely:
|
||||
|
||||
1. LOAD the workflow definition from {project-root}/{{bmadFolderName}}/{{workflow_path}}
|
||||
2. PARSE the YAML structure to understand:
|
||||
- Workflow phases and steps
|
||||
- Required inputs and outputs
|
||||
- Dependencies between steps
|
||||
3. EXECUTE each step in order
|
||||
4. VALIDATE outputs before proceeding to next step
|
||||
|
||||
WORKFLOW FILE: {project-root}/{{bmadFolderName}}/{{workflow_path}}
|
||||
"""
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
description = """{{description}}"""
|
||||
prompt = """
|
||||
Execute the BMAD '{{name}}' workflow.
|
||||
|
||||
CRITICAL: You must load and follow the workflow definition exactly.
|
||||
|
||||
WORKFLOW INSTRUCTIONS:
|
||||
1. LOAD the workflow file from {project-root}/{{bmadFolderName}}/{{workflow_path}}
|
||||
2. READ its entire contents
|
||||
3. FOLLOW every step precisely as specified
|
||||
4. DO NOT skip or modify any steps
|
||||
|
||||
WORKFLOW FILE: {project-root}/{{bmadFolderName}}/{{workflow_path}}
|
||||
"""
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
description = "Activates the {{title}} agent from the BMad Method."
|
||||
prompt = """
|
||||
CRITICAL: You are now the BMad '{{title}}' agent.
|
||||
|
||||
PRE-FLIGHT CHECKLIST:
|
||||
1. [ ] IMMEDIATE ACTION: Load and parse @{_bmad}/{{module}}/config.yaml - store ALL config values in memory for use throughout the session.
|
||||
2. [ ] IMMEDIATE ACTION: Read and internalize the full agent definition at @{_bmad}/{{module}}/agents/{{name}}.md.
|
||||
3. [ ] CONFIRM: The user's name from config is {user_name}.
|
||||
|
||||
Only after all checks are complete, greet the user by name and display the menu.
|
||||
Acknowledge this checklist is complete in your first response.
|
||||
|
||||
AGENT DEFINITION: @{_bmad}/{{module}}/agents/{{name}}.md
|
||||
"""
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
description = "Executes the {{taskName}} task from the BMad Method."
|
||||
prompt = """
|
||||
Execute the following BMad Method task workflow:
|
||||
|
||||
PRE-FLIGHT CHECKLIST:
|
||||
1. [ ] IMMEDIATE ACTION: Load and parse @{_bmad}/{{module}}/config.yaml.
|
||||
2. [ ] IMMEDIATE ACTION: Read and load the task definition at @{_bmad}/{{module}}/tasks/{{filename}}.
|
||||
|
||||
Follow all instructions and complete the task as defined.
|
||||
|
||||
TASK DEFINITION: @{_bmad}/{{module}}/tasks/{{filename}}
|
||||
"""
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
|
||||
|
||||
<agent-activation CRITICAL="TRUE">
|
||||
1. LOAD the FULL agent file from {project-root}/_bmad/{{path}}
|
||||
2. READ its entire contents - this contains the complete agent persona, menu, and instructions
|
||||
3. FOLLOW every step in the <activation> section precisely
|
||||
4. DISPLAY the welcome/greeting as instructed
|
||||
5. PRESENT the numbered menu
|
||||
6. WAIT for user input before proceeding
|
||||
</agent-activation>
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
name = "{{name}}"
|
||||
description = "{{description}}"
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
// Zod schema definition for module.yaml files
|
||||
const { z } = require('zod');
|
||||
|
||||
// Pattern for module code: kebab-case, 2-20 characters, starts with letter
|
||||
const MODULE_CODE_PATTERN = /^[a-z][a-z0-9-]{1,19}$/;
|
||||
|
||||
// Public API ---------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Validate a module YAML payload against the schema.
|
||||
*
|
||||
* @param {string} filePath Path to the module file (used to detect core vs non-core modules).
|
||||
* @param {unknown} moduleYaml Parsed YAML content.
|
||||
* @returns {import('zod').SafeParseReturnType<unknown, unknown>} SafeParse result.
|
||||
*/
|
||||
function validateModuleFile(filePath, moduleYaml) {
|
||||
const isCoreModule = typeof filePath === 'string' && filePath.replaceAll('\\', '/').includes('src/core/');
|
||||
const schema = moduleSchema({ isCoreModule });
|
||||
return schema.safeParse(moduleYaml);
|
||||
}
|
||||
|
||||
module.exports = { validateModuleFile };
|
||||
|
||||
// Internal helpers ---------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Build the Zod schema for validating a module.yaml file.
|
||||
* @param {{isCoreModule?: boolean}} options - Options for schema validation.
|
||||
* @returns {import('zod').ZodSchema} Configured Zod schema instance.
|
||||
*/
|
||||
function moduleSchema(options) {
|
||||
const { isCoreModule = false } = options ?? {};
|
||||
return z
|
||||
.object({
|
||||
// Required fields
|
||||
code: z.string().regex(MODULE_CODE_PATTERN, {
|
||||
message: 'module.code must be kebab-case, 2-20 characters, starting with a letter',
|
||||
}),
|
||||
name: createNonEmptyString('module.name'),
|
||||
header: createNonEmptyString('module.header'),
|
||||
subheader: createNonEmptyString('module.subheader'),
|
||||
// default_selected is optional for core module, required for non-core modules
|
||||
default_selected: z.boolean().optional(),
|
||||
|
||||
// Optional fields
|
||||
type: createNonEmptyString('module.type').optional(),
|
||||
global: z.boolean().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
.superRefine((value, ctx) => {
|
||||
// Enforce default_selected for non-core modules
|
||||
if (!isCoreModule && !('default_selected' in value)) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['default_selected'],
|
||||
message: 'module.default_selected is required for non-core modules',
|
||||
});
|
||||
}
|
||||
|
||||
// Validate any additional keys as variable definitions
|
||||
const reservedKeys = new Set(['code', 'name', 'header', 'subheader', 'default_selected', 'type', 'global']);
|
||||
|
||||
for (const key of Object.keys(value)) {
|
||||
if (reservedKeys.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const variableValue = value[key];
|
||||
|
||||
// Skip if null/undefined
|
||||
if (variableValue === null || variableValue === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate variable definition
|
||||
const variableResult = validateVariableDefinition(key, variableValue);
|
||||
if (!variableResult.valid) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: [key],
|
||||
message: variableResult.error,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a variable definition object.
|
||||
* @param {string} variableName The name of the variable.
|
||||
* @param {unknown} variableValue The variable definition value.
|
||||
* @returns {{ valid: boolean, error?: string }}
|
||||
*/
|
||||
function validateVariableDefinition(variableName, variableValue) {
|
||||
// If it's not an object, it's invalid
|
||||
if (typeof variableValue !== 'object' || variableValue === null) {
|
||||
return { valid: false, error: `${variableName} must be an object with variable definition properties` };
|
||||
}
|
||||
|
||||
const hasInherit = 'inherit' in variableValue;
|
||||
const hasPrompt = 'prompt' in variableValue;
|
||||
|
||||
// Enforce mutual exclusivity: inherit and prompt cannot coexist
|
||||
if (hasInherit && hasPrompt) {
|
||||
return { valid: false, error: `${variableName} must not define both 'inherit' and 'prompt'` };
|
||||
}
|
||||
|
||||
// Check for inherit alias - if present, it's the only required field
|
||||
if (hasInherit) {
|
||||
if (typeof variableValue.inherit !== 'string' || variableValue.inherit.trim().length === 0) {
|
||||
return { valid: false, error: `${variableName}.inherit must be a non-empty string` };
|
||||
}
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
// Otherwise, prompt is required
|
||||
if (!hasPrompt) {
|
||||
return { valid: false, error: `${variableName} must have a 'prompt' or 'inherit' field` };
|
||||
}
|
||||
|
||||
// Validate prompt: string or array of strings
|
||||
const prompt = variableValue.prompt;
|
||||
if (typeof prompt === 'string') {
|
||||
if (prompt.trim().length === 0) {
|
||||
return { valid: false, error: `${variableName}.prompt must be a non-empty string` };
|
||||
}
|
||||
} else if (Array.isArray(prompt)) {
|
||||
if (prompt.length === 0) {
|
||||
return { valid: false, error: `${variableName}.prompt array must not be empty` };
|
||||
}
|
||||
for (const [index, promptItem] of prompt.entries()) {
|
||||
if (typeof promptItem !== 'string' || promptItem.trim().length === 0) {
|
||||
return { valid: false, error: `${variableName}.prompt[${index}] must be a non-empty string` };
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return { valid: false, error: `${variableName}.prompt must be a string or array of strings` };
|
||||
}
|
||||
|
||||
// Enforce mutual exclusivity: single-select and multi-select cannot coexist
|
||||
const hasSingle = 'single-select' in variableValue;
|
||||
const hasMulti = 'multi-select' in variableValue;
|
||||
if (hasSingle && hasMulti) {
|
||||
return { valid: false, error: `${variableName} must not define both 'single-select' and 'multi-select'` };
|
||||
}
|
||||
|
||||
// Validate optional single-select
|
||||
if (hasSingle) {
|
||||
const selectResult = validateSelectOptions(variableName, 'single-select', variableValue['single-select']);
|
||||
if (!selectResult.valid) {
|
||||
return selectResult;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate optional multi-select
|
||||
if (hasMulti) {
|
||||
const selectResult = validateSelectOptions(variableName, 'multi-select', variableValue['multi-select']);
|
||||
if (!selectResult.valid) {
|
||||
return selectResult;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate optional required field
|
||||
if ('required' in variableValue && typeof variableValue.required !== 'boolean') {
|
||||
return { valid: false, error: `${variableName}.required must be a boolean` };
|
||||
}
|
||||
|
||||
// Validate optional result field
|
||||
if ('result' in variableValue && (typeof variableValue.result !== 'string' || variableValue.result.trim().length === 0)) {
|
||||
return { valid: false, error: `${variableName}.result must be a non-empty string` };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate single-select or multi-select options array.
|
||||
* @param {string} variableName The variable name for error messages.
|
||||
* @param {string} selectType Either 'single-select' or 'multi-select'.
|
||||
* @param {unknown} options The options array to validate.
|
||||
* @returns {{ valid: boolean, error?: string }}
|
||||
*/
|
||||
function validateSelectOptions(variableName, selectType, options) {
|
||||
if (!Array.isArray(options)) {
|
||||
return { valid: false, error: `${variableName}.${selectType} must be an array` };
|
||||
}
|
||||
|
||||
if (options.length === 0) {
|
||||
return { valid: false, error: `${variableName}.${selectType} must not be empty` };
|
||||
}
|
||||
|
||||
for (const [index, option] of options.entries()) {
|
||||
if (typeof option !== 'object' || option === null) {
|
||||
return { valid: false, error: `${variableName}.${selectType}[${index}] must be an object` };
|
||||
}
|
||||
if (!('value' in option) || typeof option.value !== 'string') {
|
||||
return { valid: false, error: `${variableName}.${selectType}[${index}].value must be a string` };
|
||||
}
|
||||
if (!('label' in option) || typeof option.label !== 'string') {
|
||||
return { valid: false, error: `${variableName}.${selectType}[${index}].label must be a string` };
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
// Primitive validators -----------------------------------------------------
|
||||
|
||||
function createNonEmptyString(label) {
|
||||
return z.string().refine((value) => value.trim().length > 0, {
|
||||
message: `${label} must be a non-empty string`,
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
/**
|
||||
* Module Schema Validator CLI
|
||||
*
|
||||
* Scans all module.yaml files in src/core/ and src/modules/
|
||||
* and validates them against the Zod schema.
|
||||
*
|
||||
* Usage: node tools/validate-module-schema.js [project_root]
|
||||
* Exit codes: 0 = success, 1 = validation failures
|
||||
*
|
||||
* Optional argument:
|
||||
* project_root - Directory to scan (defaults to BMAD repo root)
|
||||
*/
|
||||
|
||||
const { glob } = require('glob');
|
||||
const yaml = require('yaml');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { validateModuleFile } = require('./schema/module.js');
|
||||
|
||||
/**
|
||||
* Main validation routine
|
||||
* @param {string} [customProjectRoot] - Optional project root to scan (for testing)
|
||||
*/
|
||||
async function main(customProjectRoot) {
|
||||
console.log('🔍 Scanning for module files...\n');
|
||||
|
||||
// Determine project root: use custom path if provided, otherwise default to repo root
|
||||
const project_root = customProjectRoot || path.join(__dirname, '..');
|
||||
|
||||
// Find all module files: core/module.yaml and bmm/module.yaml (and any other top-level modules)
|
||||
const moduleFiles = await glob('src/{core,bmm}/module.yaml', {
|
||||
cwd: project_root,
|
||||
absolute: true,
|
||||
});
|
||||
|
||||
if (moduleFiles.length === 0) {
|
||||
console.log('❌ No module files found. This likely indicates a configuration error.');
|
||||
console.log(' Expected to find module.yaml files in src/core/ and src/modules/*/');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Found ${moduleFiles.length} module file(s)\n`);
|
||||
|
||||
const errors = [];
|
||||
|
||||
// Validate each file
|
||||
for (const filePath of moduleFiles) {
|
||||
const relativePath = path.relative(project_root, filePath).replaceAll('\\', '/');
|
||||
|
||||
try {
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
const moduleData = yaml.parse(fileContent);
|
||||
|
||||
// Ensure path starts with src/ for core module detection
|
||||
const srcRelativePath = relativePath.startsWith('src/') ? relativePath : `src/${relativePath}`;
|
||||
|
||||
const result = validateModuleFile(srcRelativePath, moduleData);
|
||||
|
||||
if (result.success) {
|
||||
console.log(`✅ ${relativePath}`);
|
||||
} else {
|
||||
errors.push({
|
||||
file: relativePath,
|
||||
issues: result.error.issues,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push({
|
||||
file: relativePath,
|
||||
issues: [
|
||||
{
|
||||
code: 'parse_error',
|
||||
message: `Failed to parse YAML: ${error.message}`,
|
||||
path: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Report errors
|
||||
if (errors.length > 0) {
|
||||
console.log('\n❌ Validation failed for the following files:\n');
|
||||
|
||||
for (const { file, issues } of errors) {
|
||||
console.log(`\n📄 ${file}`);
|
||||
for (const issue of issues) {
|
||||
const pathString = issue.path.length > 0 ? issue.path.join('.') : '(root)';
|
||||
console.log(` Path: ${pathString}`);
|
||||
console.log(` Error: ${issue.message}`);
|
||||
if (issue.code) {
|
||||
console.log(` Code: ${issue.code}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n\n💥 ${errors.length} file(s) failed validation`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\n✨ All ${moduleFiles.length} module file(s) passed validation!\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Run with optional command-line argument for project root
|
||||
const customProjectRoot = process.argv[2];
|
||||
main(customProjectRoot).catch((error) => {
|
||||
console.error('Fatal error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
Loading…
Reference in New Issue