Merge branch 'main' into phase1-md-workflows-clean
This commit is contained in:
commit
bff2808986
|
|
@ -147,6 +147,15 @@ Keep messages under 72 characters. Each commit = one logical change.
|
|||
- Everything is natural language (markdown) — no code in core framework
|
||||
- Use BMad modules for domain-specific features
|
||||
- Validate YAML schemas: `npm run validate:schemas`
|
||||
- Validate file references: `npm run validate:refs`
|
||||
|
||||
### File-Pattern-to-Validator Mapping
|
||||
|
||||
| File Pattern | Validator | Extraction Function |
|
||||
| ------------ | --------- | ------------------- |
|
||||
| `*.yaml`, `*.yml` | `validate-file-refs.js` | `extractYamlRefs` |
|
||||
| `*.md`, `*.xml` | `validate-file-refs.js` | `extractMarkdownRefs` |
|
||||
| `*.csv` | `validate-file-refs.js` | `extractCsvRefs` |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ Pick which AI tools you use:
|
|||
- Claude Code
|
||||
- Cursor
|
||||
- Windsurf
|
||||
- Kiro
|
||||
- Others
|
||||
|
||||
Each tool has its own way of integrating commands. The installer creates tiny prompt files to activate workflows and agents — it just puts them where your tool expects to find them.
|
||||
|
|
@ -63,7 +64,8 @@ your-project/
|
|||
│ ├── core/ # Required core module
|
||||
│ └── ...
|
||||
├── _bmad-output/ # Generated artifacts
|
||||
└── .claude/ # Claude Code commands (if using Claude Code)
|
||||
├── .claude/ # Claude Code commands (if using Claude Code)
|
||||
└── .kiro/ # Kiro steering files (if using Kiro)
|
||||
```
|
||||
|
||||
## Verify Installation
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ BMad works with any AI coding assistant that supports custom system prompts or p
|
|||
- **[Claude Code](https://code.claude.com)** — Anthropic's CLI tool (recommended)
|
||||
- **[Cursor](https://cursor.sh)** — AI-first code editor
|
||||
- **[Windsurf](https://codeium.com/windsurf)** — Codeium's AI IDE
|
||||
- **[Kiro](https://kiro.dev)** — Amazon's AI-powered IDE
|
||||
- **[Roo Code](https://roocode.com)** — VS Code extension
|
||||
|
||||
You should be comfortable with basic software development concepts like version control, project structure, and agile workflows. No prior experience with BMad-style agent systems is required—that's what these docs are for.
|
||||
|
|
|
|||
|
|
@ -15,11 +15,22 @@ That means the authoritative list lives **in your project**, not in a static doc
|
|||
|
||||
## Where Commands Are Generated
|
||||
|
||||
The installer writes command files into your project (example paths for Claude Code):
|
||||
The installer writes command files into your project. The location and format depend on your AI tool:
|
||||
|
||||
| AI Tool | Location | File Reference Syntax |
|
||||
| --- | --- | --- |
|
||||
| Claude Code | `.claude/commands/` | `@path` references |
|
||||
| Kiro | `.kiro/steering/` | `#[[file:path]]` references with `inclusion: manual` frontmatter |
|
||||
| Cursor | `.cursor/commands/` | `@path` references |
|
||||
| Windsurf | `.windsurf/workflows/` | `@{project-root}/path` references |
|
||||
|
||||
Example paths for Claude Code:
|
||||
|
||||
- `.claude/commands/bmad/<module>/agents/`
|
||||
- `.claude/commands/bmad/<module>/workflows/`
|
||||
|
||||
All tools invoke the same underlying `_bmad/` workflows and agents — only the launcher format differs.
|
||||
|
||||
These folders are the **canonical, project-specific command list**.
|
||||
|
||||
## Common Commands
|
||||
|
|
|
|||
|
|
@ -41,9 +41,10 @@
|
|||
"lint:md": "markdownlint-cli2 \"**/*.md\"",
|
||||
"prepare": "command -v husky >/dev/null 2>&1 && husky || exit 0",
|
||||
"rebundle": "node tools/cli/bundlers/bundle-web.js rebundle",
|
||||
"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:refs && npm run test:install && npm run validate:schemas && 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:refs": "node test/test-file-refs-csv.js",
|
||||
"test:schemas": "node test/test-agent-schema.js",
|
||||
"validate:refs": "node tools/validate-file-refs.js",
|
||||
"validate:schemas": "node tools/validate-agent-schema.js"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
module,phase,name,workflow-file,description
|
||||
bmm,anytime,Document,,Analyze project
|
||||
bmm,1-analysis,Brainstorm,,Brainstorm ideas
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
module,phase,name,workflow-file,description
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
name,code,description,agent
|
||||
brainstorm,BSP,"Generate ideas",analyst
|
||||
party,PM,"Multi-agent",facilitator
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
module,phase,name,workflow-file,description
|
||||
bmm,anytime,Template Var,{output_folder}/something.md,Has unresolvable template var
|
||||
bmm,anytime,Normal Ref,_bmad/core/tasks/help.md,Normal resolvable ref
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
module,phase,name,code,sequence,workflow-file,command,required,agent,options,description,output-location,outputs,
|
||||
bmm,anytime,Document Project,DP,,_bmad/bmm/workflows/document-project/workflow.yaml,bmad-bmm-document-project,false,analyst,Create Mode,"Analyze project",project-knowledge,*,
|
||||
bmm,1-analysis,Brainstorm Project,BP,10,_bmad/core/workflows/brainstorming/workflow.md,bmad-brainstorming,false,analyst,data=template.md,"Brainstorming",planning_artifacts,"session",
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
module,phase,name,code,sequence,workflow-file,command,required,agent,options,description,output-location,outputs
|
||||
core,anytime,Brainstorming,BSP,,_bmad/core/workflows/brainstorming/workflow.md,bmad-brainstorming,false,analyst,,"Generate ideas",{output_folder}/brainstorming.md,
|
||||
core,anytime,Party Mode,PM,,_bmad/core/workflows/party-mode/workflow.md,bmad-party-mode,false,facilitator,,"Multi-agent discussion",,
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
name,workflow-file,description
|
||||
test,_bmad/core/tasks/help.md,A test entry
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
/**
|
||||
* CSV File Reference Extraction Test Runner
|
||||
*
|
||||
* Tests extractCsvRefs() from validate-file-refs.js against fixtures.
|
||||
* Verifies correct extraction of workflow-file references from CSV files.
|
||||
*
|
||||
* Usage: node test/test-file-refs-csv.js
|
||||
* Exit codes: 0 = all tests pass, 1 = test failures
|
||||
*/
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { extractCsvRefs } = require('../tools/validate-file-refs.js');
|
||||
|
||||
// ANSI color codes
|
||||
const colors = {
|
||||
reset: '\u001B[0m',
|
||||
green: '\u001B[32m',
|
||||
red: '\u001B[31m',
|
||||
cyan: '\u001B[36m',
|
||||
dim: '\u001B[2m',
|
||||
};
|
||||
|
||||
const FIXTURES = path.join(__dirname, 'fixtures/file-refs-csv');
|
||||
|
||||
let totalTests = 0;
|
||||
let passedTests = 0;
|
||||
const failures = [];
|
||||
|
||||
function test(name, fn) {
|
||||
totalTests++;
|
||||
try {
|
||||
fn();
|
||||
passedTests++;
|
||||
console.log(` ${colors.green}\u2713${colors.reset} ${name}`);
|
||||
} catch (error) {
|
||||
console.log(` ${colors.red}\u2717${colors.reset} ${name} ${colors.red}${error.message}${colors.reset}`);
|
||||
failures.push({ name, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
function loadFixture(relativePath) {
|
||||
const fullPath = path.join(FIXTURES, relativePath);
|
||||
const content = fs.readFileSync(fullPath, 'utf-8');
|
||||
return { fullPath, content };
|
||||
}
|
||||
|
||||
// --- Valid fixtures ---
|
||||
|
||||
console.log(`\n${colors.cyan}CSV File Reference Extraction Tests${colors.reset}\n`);
|
||||
console.log(`${colors.cyan}Valid fixtures${colors.reset}`);
|
||||
|
||||
test('bmm-style.csv: extracts workflow-file refs with trailing commas', () => {
|
||||
const { fullPath, content } = loadFixture('valid/bmm-style.csv');
|
||||
const refs = extractCsvRefs(fullPath, content);
|
||||
assert(refs.length === 2, `Expected 2 refs, got ${refs.length}`);
|
||||
assert(refs[0].raw === '_bmad/bmm/workflows/document-project/workflow.yaml', `Wrong raw[0]: ${refs[0].raw}`);
|
||||
assert(refs[1].raw === '_bmad/core/workflows/brainstorming/workflow.md', `Wrong raw[1]: ${refs[1].raw}`);
|
||||
assert(refs[0].type === 'project-root', `Wrong type: ${refs[0].type}`);
|
||||
assert(refs[0].line === 2, `Wrong line for row 0: ${refs[0].line}`);
|
||||
assert(refs[1].line === 3, `Wrong line for row 1: ${refs[1].line}`);
|
||||
assert(refs[0].file === fullPath, 'Wrong file path');
|
||||
});
|
||||
|
||||
test('core-style.csv: extracts refs from core module-help format', () => {
|
||||
const { fullPath, content } = loadFixture('valid/core-style.csv');
|
||||
const refs = extractCsvRefs(fullPath, content);
|
||||
assert(refs.length === 2, `Expected 2 refs, got ${refs.length}`);
|
||||
assert(refs[0].raw === '_bmad/core/workflows/brainstorming/workflow.md', `Wrong raw[0]: ${refs[0].raw}`);
|
||||
assert(refs[1].raw === '_bmad/core/workflows/party-mode/workflow.md', `Wrong raw[1]: ${refs[1].raw}`);
|
||||
});
|
||||
|
||||
test('minimal.csv: extracts refs from minimal 3-column CSV', () => {
|
||||
const { fullPath, content } = loadFixture('valid/minimal.csv');
|
||||
const refs = extractCsvRefs(fullPath, content);
|
||||
assert(refs.length === 1, `Expected 1 ref, got ${refs.length}`);
|
||||
assert(refs[0].raw === '_bmad/core/tasks/help.md', `Wrong raw: ${refs[0].raw}`);
|
||||
assert(refs[0].line === 2, `Wrong line: ${refs[0].line}`);
|
||||
});
|
||||
|
||||
// --- Invalid fixtures ---
|
||||
|
||||
console.log(`\n${colors.cyan}Invalid fixtures (expect 0 refs)${colors.reset}`);
|
||||
|
||||
test('no-workflow-column.csv: returns 0 refs when workflow-file column missing', () => {
|
||||
const { fullPath, content } = loadFixture('invalid/no-workflow-column.csv');
|
||||
const refs = extractCsvRefs(fullPath, content);
|
||||
assert(refs.length === 0, `Expected 0 refs, got ${refs.length}`);
|
||||
});
|
||||
|
||||
test('empty-data.csv: returns 0 refs when CSV has header only', () => {
|
||||
const { fullPath, content } = loadFixture('invalid/empty-data.csv');
|
||||
const refs = extractCsvRefs(fullPath, content);
|
||||
assert(refs.length === 0, `Expected 0 refs, got ${refs.length}`);
|
||||
});
|
||||
|
||||
test('all-empty-workflow.csv: returns 0 refs when all workflow-file cells empty', () => {
|
||||
const { fullPath, content } = loadFixture('invalid/all-empty-workflow.csv');
|
||||
const refs = extractCsvRefs(fullPath, content);
|
||||
assert(refs.length === 0, `Expected 0 refs, got ${refs.length}`);
|
||||
});
|
||||
|
||||
test('unresolvable-vars.csv: filters out template variables, keeps normal refs', () => {
|
||||
const { fullPath, content } = loadFixture('invalid/unresolvable-vars.csv');
|
||||
const refs = extractCsvRefs(fullPath, content);
|
||||
assert(refs.length === 1, `Expected 1 ref, got ${refs.length}`);
|
||||
assert(refs[0].raw === '_bmad/core/tasks/help.md', `Wrong raw: ${refs[0].raw}`);
|
||||
});
|
||||
|
||||
// --- Summary ---
|
||||
|
||||
console.log(`\n${colors.cyan}${'═'.repeat(55)}${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}${'═'.repeat(55)}${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}\u2717${colors.reset} ${failure.name}`);
|
||||
console.log(` ${failure.message}\n`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`${colors.green}All tests passed!${colors.reset}\n`);
|
||||
process.exit(0);
|
||||
|
|
@ -175,7 +175,7 @@ class Installer {
|
|||
}
|
||||
|
||||
// Check if this IDE handler has a collectConfiguration method
|
||||
// (custom installers like Codex, Kilo, Kiro-cli may have this)
|
||||
// (custom installers like Codex, Kilo may have this)
|
||||
if (typeof handler.collectConfiguration === 'function') {
|
||||
await prompts.log.info(`Configuring ${ide}...`);
|
||||
ideConfigurations[ide] = await handler.collectConfiguration({
|
||||
|
|
|
|||
|
|
@ -1,326 +0,0 @@
|
|||
const path = require('node:path');
|
||||
const { BaseIdeSetup } = require('./_base-ide');
|
||||
const fs = require('fs-extra');
|
||||
const prompts = require('../../../lib/prompts');
|
||||
const yaml = require('yaml');
|
||||
|
||||
/**
|
||||
* Kiro CLI setup handler for BMad Method
|
||||
*/
|
||||
class KiroCliSetup extends BaseIdeSetup {
|
||||
constructor() {
|
||||
super('kiro-cli', 'Kiro CLI', false);
|
||||
this.configDir = '.kiro';
|
||||
this.agentsDir = 'agents';
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup old BMAD installation before reinstalling
|
||||
* @param {string} projectDir - Project directory
|
||||
*/
|
||||
async cleanup(projectDir, options = {}) {
|
||||
const bmadAgentsDir = path.join(projectDir, this.configDir, this.agentsDir);
|
||||
|
||||
if (await fs.pathExists(bmadAgentsDir)) {
|
||||
// Remove existing BMad agents
|
||||
const files = await fs.readdir(bmadAgentsDir);
|
||||
for (const file of files) {
|
||||
if (file.startsWith('bmad')) {
|
||||
await fs.remove(path.join(bmadAgentsDir, file));
|
||||
}
|
||||
}
|
||||
if (!options.silent) await prompts.log.message(` Cleaned old BMAD agents from ${this.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup Kiro CLI configuration with BMad agents
|
||||
* @param {string} projectDir - Project directory
|
||||
* @param {string} bmadDir - BMAD installation directory
|
||||
* @param {Object} options - Setup options
|
||||
*/
|
||||
async setup(projectDir, bmadDir, options = {}) {
|
||||
if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`);
|
||||
|
||||
await this.cleanup(projectDir, options);
|
||||
|
||||
const kiroDir = path.join(projectDir, this.configDir);
|
||||
const agentsDir = path.join(kiroDir, this.agentsDir);
|
||||
|
||||
await this.ensureDir(agentsDir);
|
||||
|
||||
// Create BMad agents from source YAML files
|
||||
await this.createBmadAgentsFromSource(agentsDir, projectDir);
|
||||
|
||||
if (!options.silent) await prompts.log.success(`${this.name} configured with BMad agents`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create BMad agent definitions from source YAML files
|
||||
* @param {string} agentsDir - Agents directory
|
||||
* @param {string} projectDir - Project directory
|
||||
*/
|
||||
async createBmadAgentsFromSource(agentsDir, projectDir) {
|
||||
const sourceDir = path.join(__dirname, '../../../../../src/modules');
|
||||
|
||||
// Find all agent YAML files
|
||||
const agentFiles = await this.findAgentFiles(sourceDir);
|
||||
|
||||
for (const agentFile of agentFiles) {
|
||||
try {
|
||||
await this.processAgentFile(agentFile, agentsDir, projectDir);
|
||||
} catch (error) {
|
||||
await prompts.log.warn(`Failed to process ${agentFile}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all agent YAML files in modules and core
|
||||
* @param {string} sourceDir - Source modules directory
|
||||
* @returns {Array} Array of agent file paths
|
||||
*/
|
||||
async findAgentFiles(sourceDir) {
|
||||
const agentFiles = [];
|
||||
|
||||
// Check core agents
|
||||
const coreAgentsDir = path.join(__dirname, '../../../../../src/core/agents');
|
||||
if (await fs.pathExists(coreAgentsDir)) {
|
||||
const files = await fs.readdir(coreAgentsDir);
|
||||
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.agent.yaml')) {
|
||||
agentFiles.push(path.join(coreAgentsDir, file));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check module agents
|
||||
if (!(await fs.pathExists(sourceDir))) {
|
||||
return agentFiles;
|
||||
}
|
||||
|
||||
const modules = await fs.readdir(sourceDir);
|
||||
|
||||
for (const module of modules) {
|
||||
const moduleAgentsDir = path.join(sourceDir, module, 'agents');
|
||||
|
||||
if (await fs.pathExists(moduleAgentsDir)) {
|
||||
const files = await fs.readdir(moduleAgentsDir);
|
||||
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.agent.yaml')) {
|
||||
agentFiles.push(path.join(moduleAgentsDir, file));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return agentFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate BMad Core compliance
|
||||
* @param {Object} agentData - Agent YAML data
|
||||
* @returns {boolean} True if compliant
|
||||
*/
|
||||
validateBmadCompliance(agentData) {
|
||||
const requiredFields = ['agent.metadata.id', 'agent.persona.role', 'agent.persona.principles'];
|
||||
|
||||
for (const field of requiredFields) {
|
||||
const keys = field.split('.');
|
||||
let current = agentData;
|
||||
|
||||
for (const key of keys) {
|
||||
if (!current || !current[key]) {
|
||||
return false;
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process individual agent YAML file
|
||||
* @param {string} agentFile - Path to agent YAML file
|
||||
* @param {string} agentsDir - Target agents directory
|
||||
* @param {string} projectDir - Project directory
|
||||
*/
|
||||
async processAgentFile(agentFile, agentsDir, projectDir) {
|
||||
const yamlContent = await fs.readFile(agentFile, 'utf8');
|
||||
const agentData = yaml.parse(yamlContent);
|
||||
|
||||
if (!this.validateBmadCompliance(agentData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract module from file path
|
||||
const normalizedPath = path.normalize(agentFile);
|
||||
const pathParts = normalizedPath.split(path.sep);
|
||||
const basename = path.basename(agentFile, '.agent.yaml');
|
||||
|
||||
// Find the module name from path
|
||||
let moduleName = 'unknown';
|
||||
if (pathParts.includes('src')) {
|
||||
const srcIndex = pathParts.indexOf('src');
|
||||
if (srcIndex + 3 < pathParts.length) {
|
||||
const folderAfterSrc = pathParts[srcIndex + 1];
|
||||
if (folderAfterSrc === 'core') {
|
||||
moduleName = 'core';
|
||||
} else if (folderAfterSrc === 'bmm') {
|
||||
moduleName = 'bmm';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract the agent name from the ID path in YAML if available
|
||||
let agentBaseName = basename;
|
||||
if (agentData.agent && agentData.agent.metadata && agentData.agent.metadata.id) {
|
||||
const idPath = agentData.agent.metadata.id;
|
||||
agentBaseName = path.basename(idPath, '.md');
|
||||
}
|
||||
|
||||
const agentName = `bmad-${moduleName}-${agentBaseName}`;
|
||||
const sanitizedAgentName = this.sanitizeAgentName(agentName);
|
||||
|
||||
// Create JSON definition
|
||||
await this.createAgentDefinitionFromYaml(agentsDir, sanitizedAgentName, agentData);
|
||||
|
||||
// Create prompt file
|
||||
await this.createAgentPromptFromYaml(agentsDir, sanitizedAgentName, agentData, projectDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize agent name for file naming
|
||||
* @param {string} name - Agent name
|
||||
* @returns {string} Sanitized name
|
||||
*/
|
||||
sanitizeAgentName(name) {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replaceAll(/\s+/g, '-')
|
||||
.replaceAll(/[^a-z0-9-]/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create agent JSON definition from YAML data
|
||||
* @param {string} agentsDir - Agents directory
|
||||
* @param {string} agentName - Agent name (role-based)
|
||||
* @param {Object} agentData - Agent YAML data
|
||||
*/
|
||||
async createAgentDefinitionFromYaml(agentsDir, agentName, agentData) {
|
||||
const personName = agentData.agent.metadata.name;
|
||||
const role = agentData.agent.persona.role;
|
||||
|
||||
const agentConfig = {
|
||||
name: agentName,
|
||||
description: `${personName} - ${role}`,
|
||||
prompt: `file://./${agentName}-prompt.md`,
|
||||
tools: ['*'],
|
||||
mcpServers: {},
|
||||
useLegacyMcpJson: true,
|
||||
resources: [],
|
||||
};
|
||||
|
||||
const agentPath = path.join(agentsDir, `${agentName}.json`);
|
||||
await fs.writeJson(agentPath, agentConfig, { spaces: 2 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Create agent prompt from YAML data
|
||||
* @param {string} agentsDir - Agents directory
|
||||
* @param {string} agentName - Agent name (role-based)
|
||||
* @param {Object} agentData - Agent YAML data
|
||||
* @param {string} projectDir - Project directory
|
||||
*/
|
||||
async createAgentPromptFromYaml(agentsDir, agentName, agentData, projectDir) {
|
||||
const promptPath = path.join(agentsDir, `${agentName}-prompt.md`);
|
||||
|
||||
// Generate prompt from YAML data
|
||||
const prompt = this.generatePromptFromYaml(agentData);
|
||||
await fs.writeFile(promptPath, prompt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate prompt content from YAML data
|
||||
* @param {Object} agentData - Agent YAML data
|
||||
* @returns {string} Generated prompt
|
||||
*/
|
||||
generatePromptFromYaml(agentData) {
|
||||
const agent = agentData.agent;
|
||||
const name = agent.metadata.name;
|
||||
const icon = agent.metadata.icon || '🤖';
|
||||
const role = agent.persona.role;
|
||||
const identity = agent.persona.identity;
|
||||
const style = agent.persona.communication_style;
|
||||
const principles = agent.persona.principles;
|
||||
|
||||
let prompt = `# ${name} ${icon}\n\n`;
|
||||
prompt += `## Role\n${role}\n\n`;
|
||||
|
||||
if (identity) {
|
||||
prompt += `## Identity\n${identity}\n\n`;
|
||||
}
|
||||
|
||||
if (style) {
|
||||
prompt += `## Communication Style\n${style}\n\n`;
|
||||
}
|
||||
|
||||
if (principles) {
|
||||
prompt += `## Principles\n`;
|
||||
if (typeof principles === 'string') {
|
||||
// Handle multi-line string principles
|
||||
prompt += principles + '\n\n';
|
||||
} else if (Array.isArray(principles)) {
|
||||
// Handle array principles
|
||||
for (const principle of principles) {
|
||||
prompt += `- ${principle}\n`;
|
||||
}
|
||||
prompt += '\n';
|
||||
}
|
||||
}
|
||||
|
||||
// Add menu items if available
|
||||
if (agent.menu && agent.menu.length > 0) {
|
||||
prompt += `## Available Workflows\n`;
|
||||
for (let i = 0; i < agent.menu.length; i++) {
|
||||
const item = agent.menu[i];
|
||||
prompt += `${i + 1}. **${item.trigger}**: ${item.description}\n`;
|
||||
}
|
||||
prompt += '\n';
|
||||
}
|
||||
|
||||
prompt += `## Instructions\nYou are ${name}, part of the BMad Method. Follow your role and principles while assisting users with their development needs.\n`;
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Kiro CLI is available
|
||||
* @returns {Promise<boolean>} True if available
|
||||
*/
|
||||
async isAvailable() {
|
||||
try {
|
||||
const { execSync } = require('node:child_process');
|
||||
execSync('kiro-cli --version', { stdio: 'ignore' });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get installation instructions
|
||||
* @returns {string} Installation instructions
|
||||
*/
|
||||
getInstallInstructions() {
|
||||
return `Install Kiro CLI:
|
||||
curl -fsSL https://github.com/aws/kiro-cli/releases/latest/download/install.sh | bash
|
||||
|
||||
Or visit: https://github.com/aws/kiro-cli`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { KiroCliSetup };
|
||||
|
|
@ -8,7 +8,7 @@ const prompts = require('../../../lib/prompts');
|
|||
* Dynamically discovers and loads IDE handlers
|
||||
*
|
||||
* Loading strategy:
|
||||
* 1. Custom installer files (codex.js, kilo.js, kiro-cli.js) - for platforms with unique installation logic
|
||||
* 1. Custom installer files (codex.js, kilo.js) - for platforms with unique installation logic
|
||||
* 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns
|
||||
*/
|
||||
class IdeManager {
|
||||
|
|
@ -44,7 +44,7 @@ class IdeManager {
|
|||
|
||||
/**
|
||||
* Dynamically load all IDE handlers
|
||||
* 1. Load custom installer files first (codex.js, kilo.js, kiro-cli.js)
|
||||
* 1. Load custom installer files first (codex.js, kilo.js)
|
||||
* 2. Load config-driven handlers from platform-codes.yaml
|
||||
*/
|
||||
async loadHandlers() {
|
||||
|
|
@ -61,7 +61,7 @@ class IdeManager {
|
|||
*/
|
||||
async loadCustomInstallerFiles() {
|
||||
const ideDir = __dirname;
|
||||
const customFiles = ['codex.js', 'kilo.js', 'kiro-cli.js'];
|
||||
const customFiles = ['codex.js', 'kilo.js'];
|
||||
|
||||
for (const file of customFiles) {
|
||||
const filePath = path.join(ideDir, file);
|
||||
|
|
|
|||
|
|
@ -111,12 +111,14 @@ platforms:
|
|||
description: "AI coding platform"
|
||||
# No installer config - uses custom kilo.js (creates .kilocodemodes file)
|
||||
|
||||
kiro-cli:
|
||||
name: "Kiro CLI"
|
||||
kiro:
|
||||
name: "Kiro"
|
||||
preferred: false
|
||||
category: cli
|
||||
description: "Kiro command-line interface"
|
||||
# No installer config - uses custom kiro-cli.js (YAML→JSON conversion)
|
||||
category: ide
|
||||
description: "Amazon's AI-powered IDE"
|
||||
installer:
|
||||
target_dir: .kiro/steering
|
||||
template_type: kiro
|
||||
|
||||
opencode:
|
||||
name: "OpenCode"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
inclusion: manual
|
||||
---
|
||||
|
||||
# {{name}}
|
||||
|
||||
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 #[[file:{{bmadFolderName}}/{{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>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
inclusion: manual
|
||||
---
|
||||
|
||||
# {{name}}
|
||||
|
||||
Read the entire task file at: #[[file:{{bmadFolderName}}/{{path}}]]
|
||||
|
||||
Follow all instructions in the task file exactly as written.
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
inclusion: manual
|
||||
---
|
||||
|
||||
# {{name}}
|
||||
|
||||
Read the entire tool file at: #[[file:{{bmadFolderName}}/{{path}}]]
|
||||
|
||||
Follow all instructions in the tool file exactly as written.
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
inclusion: manual
|
||||
---
|
||||
|
||||
# {{name}}
|
||||
|
||||
IT IS CRITICAL THAT YOU FOLLOW THESE STEPS - while staying in character as the current agent persona you may have loaded:
|
||||
|
||||
<steps CRITICAL="TRUE">
|
||||
1. Always LOAD the FULL #[[file:{{bmadFolderName}}/core/tasks/workflow.xml]]
|
||||
2. READ its entire contents - this is the CORE OS for EXECUTING the specific workflow-config #[[file:{{bmadFolderName}}/{{path}}]]
|
||||
3. Pass the yaml path {{bmadFolderName}}/{{path}} as 'workflow-config' parameter to the workflow.xml instructions
|
||||
4. Follow workflow.xml instructions EXACTLY as written to process and follow the specific workflow config and its instructions
|
||||
5. Save outputs after EACH section when generating any documents from templates
|
||||
</steps>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
inclusion: manual
|
||||
---
|
||||
|
||||
# {{name}}
|
||||
|
||||
IT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL #[[file:{{bmadFolderName}}/{{path}}]], READ its entire contents and follow its directions exactly!
|
||||
|
|
@ -67,11 +67,11 @@ platforms:
|
|||
category: ide
|
||||
description: "Atlassian's Rovo development environment"
|
||||
|
||||
kiro-cli:
|
||||
name: "Kiro CLI"
|
||||
kiro:
|
||||
name: "Kiro"
|
||||
preferred: false
|
||||
category: cli
|
||||
description: "Kiro command-line interface"
|
||||
category: ide
|
||||
description: "Amazon's AI-powered IDE"
|
||||
|
||||
github-copilot:
|
||||
name: "GitHub Copilot"
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const yaml = require('yaml');
|
||||
const { parse: parseCsv } = require('csv-parse/sync');
|
||||
|
||||
const PROJECT_ROOT = path.resolve(__dirname, '..');
|
||||
const SRC_DIR = path.join(PROJECT_ROOT, 'src');
|
||||
|
|
@ -38,7 +39,7 @@ const STRICT = process.argv.includes('--strict');
|
|||
// --- Constants ---
|
||||
|
||||
// File extensions to scan
|
||||
const SCAN_EXTENSIONS = new Set(['.yaml', '.yml', '.md', '.xml']);
|
||||
const SCAN_EXTENSIONS = new Set(['.yaml', '.yml', '.md', '.xml', '.csv']);
|
||||
|
||||
// Skip directories
|
||||
const SKIP_DIRS = new Set(['node_modules', '_module-installer', '.git']);
|
||||
|
|
@ -292,6 +293,46 @@ function extractMarkdownRefs(filePath, content) {
|
|||
return refs;
|
||||
}
|
||||
|
||||
function extractCsvRefs(filePath, content) {
|
||||
const refs = [];
|
||||
|
||||
let records;
|
||||
try {
|
||||
records = parseCsv(content, {
|
||||
columns: true,
|
||||
skip_empty_lines: true,
|
||||
relax_column_count: true,
|
||||
});
|
||||
} catch (error) {
|
||||
// No CSV schema validator exists yet (planned as Layer 2c) — surface parse errors visibly.
|
||||
// YAML equivalent (line ~198) defers to validate-agent-schema.js; CSV has no such fallback.
|
||||
const rel = path.relative(PROJECT_ROOT, filePath);
|
||||
console.error(` [CSV-PARSE-ERROR] ${rel}: ${error.message}`);
|
||||
if (process.env.GITHUB_ACTIONS) {
|
||||
console.log(`::warning file=${rel},line=1::${escapeAnnotation(`CSV parse error: ${error.message}`)}`);
|
||||
}
|
||||
return refs;
|
||||
}
|
||||
|
||||
// Only process if workflow-file column exists
|
||||
const firstRecord = records[0];
|
||||
if (!firstRecord || !('workflow-file' in firstRecord)) {
|
||||
return refs;
|
||||
}
|
||||
|
||||
for (const [i, record] of records.entries()) {
|
||||
const raw = record['workflow-file'];
|
||||
if (!raw || raw.trim() === '') continue;
|
||||
if (!isResolvable(raw)) continue;
|
||||
|
||||
// Line = header (1) + data row index (0-based) + 1
|
||||
const line = i + 2;
|
||||
refs.push({ file: filePath, raw, type: 'project-root', line });
|
||||
}
|
||||
|
||||
return refs;
|
||||
}
|
||||
|
||||
// --- Reference Resolution ---
|
||||
|
||||
function resolveRef(ref) {
|
||||
|
|
@ -351,130 +392,163 @@ function checkAbsolutePathLeaks(filePath, content) {
|
|||
return leaks;
|
||||
}
|
||||
|
||||
// --- Exports (for testing) ---
|
||||
module.exports = { extractCsvRefs };
|
||||
|
||||
// --- Main ---
|
||||
|
||||
console.log(`\nValidating file references in: ${SRC_DIR}`);
|
||||
console.log(`Mode: ${STRICT ? 'STRICT (exit 1 on issues)' : 'WARNING (exit 0)'}${VERBOSE ? ' + VERBOSE' : ''}\n`);
|
||||
if (require.main === module) {
|
||||
console.log(`\nValidating file references in: ${SRC_DIR}`);
|
||||
console.log(`Mode: ${STRICT ? 'STRICT (exit 1 on issues)' : 'WARNING (exit 0)'}${VERBOSE ? ' + VERBOSE' : ''}\n`);
|
||||
|
||||
const files = getSourceFiles(SRC_DIR);
|
||||
console.log(`Found ${files.length} source files\n`);
|
||||
const files = getSourceFiles(SRC_DIR);
|
||||
console.log(`Found ${files.length} source files\n`);
|
||||
|
||||
let totalRefs = 0;
|
||||
let brokenRefs = 0;
|
||||
let totalLeaks = 0;
|
||||
let filesWithIssues = 0;
|
||||
const allIssues = []; // Collect for $GITHUB_STEP_SUMMARY
|
||||
let totalRefs = 0;
|
||||
let brokenRefs = 0;
|
||||
let totalLeaks = 0;
|
||||
let filesWithIssues = 0;
|
||||
const allIssues = []; // Collect for $GITHUB_STEP_SUMMARY
|
||||
|
||||
for (const filePath of files) {
|
||||
const relativePath = path.relative(PROJECT_ROOT, filePath);
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const ext = path.extname(filePath);
|
||||
for (const filePath of files) {
|
||||
const relativePath = path.relative(PROJECT_ROOT, filePath);
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const ext = path.extname(filePath);
|
||||
|
||||
// Extract references
|
||||
let refs;
|
||||
if (ext === '.yaml' || ext === '.yml') {
|
||||
refs = extractYamlRefs(filePath, content);
|
||||
} else {
|
||||
refs = extractMarkdownRefs(filePath, content);
|
||||
}
|
||||
// Extract references
|
||||
let refs;
|
||||
if (ext === '.yaml' || ext === '.yml') {
|
||||
refs = extractYamlRefs(filePath, content);
|
||||
} else if (ext === '.csv') {
|
||||
refs = extractCsvRefs(filePath, content);
|
||||
} else {
|
||||
refs = extractMarkdownRefs(filePath, content);
|
||||
}
|
||||
|
||||
// Resolve and check
|
||||
const broken = [];
|
||||
// Resolve and classify all refs before printing anything.
|
||||
// This avoids the confusing pattern of printing headers at two different
|
||||
// times depending on verbosity — collect first, then print once.
|
||||
const broken = [];
|
||||
const ok = [];
|
||||
|
||||
if (VERBOSE && refs.length > 0) {
|
||||
console.log(`\n${relativePath}`);
|
||||
}
|
||||
for (const ref of refs) {
|
||||
totalRefs++;
|
||||
const resolved = resolveRef(ref);
|
||||
|
||||
for (const ref of refs) {
|
||||
totalRefs++;
|
||||
const resolved = resolveRef(ref);
|
||||
|
||||
if (resolved && !fs.existsSync(resolved)) {
|
||||
// For paths without extensions, also check if it's a directory
|
||||
const hasExt = path.extname(resolved) !== '';
|
||||
if (!hasExt) {
|
||||
// Could be a directory reference — skip if not clearly a file
|
||||
if (resolved && !fs.existsSync(resolved)) {
|
||||
// Extensionless paths may be directory references or partial templates.
|
||||
// If the path has no extension, check whether it exists as a directory.
|
||||
// Flag it if nothing exists at all — likely a real broken reference.
|
||||
const hasExt = path.extname(resolved) !== '';
|
||||
if (!hasExt) {
|
||||
if (fs.existsSync(resolved)) {
|
||||
ok.push({ ref, tag: 'OK-DIR' });
|
||||
} else {
|
||||
// No extension and nothing exists — not a file, not a directory.
|
||||
// Flag as UNRESOLVED (distinct from BROKEN which means "file with extension not found").
|
||||
broken.push({ ref, resolved: path.relative(PROJECT_ROOT, resolved), kind: 'unresolved' });
|
||||
brokenRefs++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
broken.push({ ref, resolved: path.relative(PROJECT_ROOT, resolved), kind: 'broken' });
|
||||
brokenRefs++;
|
||||
continue;
|
||||
}
|
||||
broken.push({ ref, resolved: path.relative(PROJECT_ROOT, resolved) });
|
||||
brokenRefs++;
|
||||
continue;
|
||||
|
||||
if (resolved) {
|
||||
ok.push({ ref, tag: 'OK' });
|
||||
}
|
||||
}
|
||||
|
||||
if (VERBOSE && resolved) {
|
||||
console.log(` [OK] ${ref.raw}`);
|
||||
}
|
||||
}
|
||||
// Check absolute path leaks
|
||||
const leaks = checkAbsolutePathLeaks(filePath, content);
|
||||
totalLeaks += leaks.length;
|
||||
|
||||
// Check absolute path leaks
|
||||
const leaks = checkAbsolutePathLeaks(filePath, content);
|
||||
totalLeaks += leaks.length;
|
||||
// Print results — file header appears once, in one place
|
||||
const hasFileIssues = broken.length > 0 || leaks.length > 0;
|
||||
|
||||
// Report issues for this file
|
||||
if (broken.length > 0 || leaks.length > 0) {
|
||||
filesWithIssues++;
|
||||
if (!VERBOSE) {
|
||||
if (hasFileIssues) {
|
||||
filesWithIssues++;
|
||||
console.log(`\n${relativePath}`);
|
||||
}
|
||||
|
||||
for (const { ref, resolved } of broken) {
|
||||
const location = ref.line ? `line ${ref.line}` : ref.key ? `key: ${ref.key}` : '';
|
||||
console.log(` [BROKEN] ${ref.raw}${location ? ` (${location})` : ''}`);
|
||||
console.log(` Target not found: ${resolved}`);
|
||||
allIssues.push({ file: relativePath, line: ref.line || 1, ref: ref.raw, issue: 'broken ref' });
|
||||
if (process.env.GITHUB_ACTIONS) {
|
||||
const line = ref.line || 1;
|
||||
console.log(`::warning file=${relativePath},line=${line}::${escapeAnnotation(`Broken reference: ${ref.raw} → ${resolved}`)}`);
|
||||
if (VERBOSE) {
|
||||
for (const { ref, tag, note } of ok) {
|
||||
const suffix = note ? ` (${note})` : '';
|
||||
console.log(` [${tag}] ${ref.raw}${suffix}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const leak of leaks) {
|
||||
console.log(` [ABS-PATH] Line ${leak.line}: ${leak.content}`);
|
||||
allIssues.push({ file: relativePath, line: leak.line, ref: leak.content, issue: 'abs-path' });
|
||||
if (process.env.GITHUB_ACTIONS) {
|
||||
console.log(`::warning file=${relativePath},line=${leak.line}::${escapeAnnotation(`Absolute path leak: ${leak.content}`)}`);
|
||||
for (const { ref, resolved, kind } of broken) {
|
||||
const location = ref.line ? `line ${ref.line}` : ref.key ? `key: ${ref.key}` : '';
|
||||
const tag = kind === 'unresolved' ? 'UNRESOLVED' : 'BROKEN';
|
||||
const detail = kind === 'unresolved' ? 'Not found as file or directory' : 'Target not found';
|
||||
const issueType = kind === 'unresolved' ? 'unresolved path' : 'broken ref';
|
||||
console.log(` [${tag}] ${ref.raw}${location ? ` (${location})` : ''}`);
|
||||
console.log(` ${detail}: ${resolved}`);
|
||||
allIssues.push({ file: relativePath, line: ref.line || 1, ref: ref.raw, issue: issueType });
|
||||
if (process.env.GITHUB_ACTIONS) {
|
||||
const line = ref.line || 1;
|
||||
console.log(
|
||||
`::warning file=${relativePath},line=${line}::${escapeAnnotation(`${tag === 'UNRESOLVED' ? 'Unresolved path' : 'Broken reference'}: ${ref.raw} → ${resolved}`)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const leak of leaks) {
|
||||
console.log(` [ABS-PATH] Line ${leak.line}: ${leak.content}`);
|
||||
allIssues.push({ file: relativePath, line: leak.line, ref: leak.content, issue: 'abs-path' });
|
||||
if (process.env.GITHUB_ACTIONS) {
|
||||
console.log(`::warning file=${relativePath},line=${leak.line}::${escapeAnnotation(`Absolute path leak: ${leak.content}`)}`);
|
||||
}
|
||||
}
|
||||
} else if (VERBOSE && refs.length > 0) {
|
||||
console.log(`\n${relativePath}`);
|
||||
for (const { ref, tag, note } of ok) {
|
||||
const suffix = note ? ` (${note})` : '';
|
||||
console.log(` [${tag}] ${ref.raw}${suffix}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log(`\n${'─'.repeat(60)}`);
|
||||
console.log(`\nSummary:`);
|
||||
console.log(` Files scanned: ${files.length}`);
|
||||
console.log(` References checked: ${totalRefs}`);
|
||||
console.log(` Broken references: ${brokenRefs}`);
|
||||
console.log(` Absolute path leaks: ${totalLeaks}`);
|
||||
// Summary
|
||||
console.log(`\n${'─'.repeat(60)}`);
|
||||
console.log(`\nSummary:`);
|
||||
console.log(` Files scanned: ${files.length}`);
|
||||
console.log(` References checked: ${totalRefs}`);
|
||||
console.log(` Broken references: ${brokenRefs}`);
|
||||
console.log(` Absolute path leaks: ${totalLeaks}`);
|
||||
|
||||
const hasIssues = brokenRefs > 0 || totalLeaks > 0;
|
||||
const hasIssues = brokenRefs > 0 || totalLeaks > 0;
|
||||
|
||||
if (hasIssues) {
|
||||
console.log(`\n ${filesWithIssues} file(s) with issues`);
|
||||
if (hasIssues) {
|
||||
console.log(`\n ${filesWithIssues} file(s) with issues`);
|
||||
|
||||
if (STRICT) {
|
||||
console.log(`\n [STRICT MODE] Exiting with failure.`);
|
||||
if (STRICT) {
|
||||
console.log(`\n [STRICT MODE] Exiting with failure.`);
|
||||
} else {
|
||||
console.log(`\n Run with --strict to treat warnings as errors.`);
|
||||
}
|
||||
} else {
|
||||
console.log(`\n Run with --strict to treat warnings as errors.`);
|
||||
console.log(`\n All file references valid!`);
|
||||
}
|
||||
} else {
|
||||
console.log(`\n All file references valid!`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log('');
|
||||
|
||||
// Write GitHub Actions step summary
|
||||
if (process.env.GITHUB_STEP_SUMMARY) {
|
||||
let summary = '## File Reference Validation\n\n';
|
||||
if (allIssues.length > 0) {
|
||||
summary += '| File | Line | Reference | Issue |\n';
|
||||
summary += '|------|------|-----------|-------|\n';
|
||||
for (const issue of allIssues) {
|
||||
summary += `| ${escapeTableCell(issue.file)} | ${issue.line} | ${escapeTableCell(issue.ref)} | ${issue.issue} |\n`;
|
||||
// Write GitHub Actions step summary
|
||||
if (process.env.GITHUB_STEP_SUMMARY) {
|
||||
let summary = '## File Reference Validation\n\n';
|
||||
if (allIssues.length > 0) {
|
||||
summary += '| File | Line | Reference | Issue |\n';
|
||||
summary += '|------|------|-----------|-------|\n';
|
||||
for (const issue of allIssues) {
|
||||
summary += `| ${escapeTableCell(issue.file)} | ${issue.line} | ${escapeTableCell(issue.ref)} | ${issue.issue} |\n`;
|
||||
}
|
||||
summary += '\n';
|
||||
}
|
||||
summary += '\n';
|
||||
summary += `**${files.length} files scanned, ${totalRefs} references checked, ${brokenRefs + totalLeaks} issues found**\n`;
|
||||
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary);
|
||||
}
|
||||
summary += `**${files.length} files scanned, ${totalRefs} references checked, ${brokenRefs + totalLeaks} issues found**\n`;
|
||||
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary);
|
||||
}
|
||||
|
||||
process.exit(hasIssues && STRICT ? 1 : 0);
|
||||
process.exit(hasIssues && STRICT ? 1 : 0);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue