Compare commits
1 Commits
7490191b65
...
f198644935
| Author | SHA1 | Date |
|---|---|---|
|
|
f198644935 |
|
|
@ -11,6 +11,7 @@ ignores:
|
||||||
- .claude/**
|
- .claude/**
|
||||||
- .roo/**
|
- .roo/**
|
||||||
- .codex/**
|
- .codex/**
|
||||||
|
- .agentvibes/**
|
||||||
- .kiro/**
|
- .kiro/**
|
||||||
- sample-project/**
|
- sample-project/**
|
||||||
- test-project-install/**
|
- test-project-install/**
|
||||||
|
|
|
||||||
85
SECURITY.md
85
SECURITY.md
|
|
@ -1,85 +0,0 @@
|
||||||
# Security Policy
|
|
||||||
|
|
||||||
## Supported Versions
|
|
||||||
|
|
||||||
We release security patches for the following versions:
|
|
||||||
|
|
||||||
| Version | Supported |
|
|
||||||
| ------- | ------------------ |
|
|
||||||
| Latest | :white_check_mark: |
|
|
||||||
| < Latest | :x: |
|
|
||||||
|
|
||||||
We recommend always using the latest version of BMad Method to ensure you have the most recent security updates.
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
|
||||||
|
|
||||||
We take security vulnerabilities seriously. If you discover a security issue, please report it responsibly.
|
|
||||||
|
|
||||||
### How to Report
|
|
||||||
|
|
||||||
**Do NOT report security vulnerabilities through public GitHub issues.**
|
|
||||||
|
|
||||||
Instead, please report them via one of these methods:
|
|
||||||
|
|
||||||
1. **GitHub Security Advisories** (Preferred): Use [GitHub's private vulnerability reporting](https://github.com/bmad-code-org/BMAD-METHOD/security/advisories/new) to submit a confidential report.
|
|
||||||
|
|
||||||
2. **Discord**: Contact a maintainer directly via DM on our [Discord server](https://discord.gg/gk8jAdXWmj).
|
|
||||||
|
|
||||||
### What to Include
|
|
||||||
|
|
||||||
Please include as much of the following information as possible:
|
|
||||||
|
|
||||||
- Type of vulnerability (e.g., prompt injection, path traversal, etc.)
|
|
||||||
- Full paths of source file(s) related to the vulnerability
|
|
||||||
- Step-by-step instructions to reproduce the issue
|
|
||||||
- Proof-of-concept or exploit code (if available)
|
|
||||||
- Impact assessment of the vulnerability
|
|
||||||
|
|
||||||
### Response Timeline
|
|
||||||
|
|
||||||
- **Initial Response**: Within 48 hours of receiving your report
|
|
||||||
- **Status Update**: Within 7 days with our assessment
|
|
||||||
- **Resolution Target**: Critical issues within 30 days; other issues within 90 days
|
|
||||||
|
|
||||||
### What to Expect
|
|
||||||
|
|
||||||
1. We will acknowledge receipt of your report
|
|
||||||
2. We will investigate and validate the vulnerability
|
|
||||||
3. We will work on a fix and coordinate disclosure timing with you
|
|
||||||
4. We will credit you in the security advisory (unless you prefer to remain anonymous)
|
|
||||||
|
|
||||||
## Security Scope
|
|
||||||
|
|
||||||
### In Scope
|
|
||||||
|
|
||||||
- Vulnerabilities in BMad Method core framework code
|
|
||||||
- Security issues in agent definitions or workflows that could lead to unintended behavior
|
|
||||||
- Path traversal or file system access issues
|
|
||||||
- Prompt injection vulnerabilities that bypass intended agent behavior
|
|
||||||
- Supply chain vulnerabilities in dependencies
|
|
||||||
|
|
||||||
### Out of Scope
|
|
||||||
|
|
||||||
- Security issues in user-created custom agents or modules
|
|
||||||
- Vulnerabilities in third-party AI providers (Claude, GPT, etc.)
|
|
||||||
- Issues that require physical access to a user's machine
|
|
||||||
- Social engineering attacks
|
|
||||||
- Denial of service attacks that don't exploit a specific vulnerability
|
|
||||||
|
|
||||||
## Security Best Practices for Users
|
|
||||||
|
|
||||||
When using BMad Method:
|
|
||||||
|
|
||||||
1. **Review Agent Outputs**: Always review AI-generated code before executing it
|
|
||||||
2. **Limit File Access**: Configure your AI IDE to limit file system access where possible
|
|
||||||
3. **Keep Updated**: Regularly update to the latest version
|
|
||||||
4. **Validate Dependencies**: Review any dependencies added by generated code
|
|
||||||
5. **Environment Isolation**: Consider running AI-assisted development in isolated environments
|
|
||||||
|
|
||||||
## Acknowledgments
|
|
||||||
|
|
||||||
We appreciate the security research community's efforts in helping keep BMad Method secure. Contributors who report valid security issues will be acknowledged in our security advisories.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Thank you for helping keep BMad Method and our community safe.
|
|
||||||
|
|
@ -20,13 +20,10 @@ This flexibility enables:
|
||||||
|
|
||||||
## Categories
|
## Categories
|
||||||
|
|
||||||
- [Categories](#categories)
|
|
||||||
- [Custom Stand-Alone Modules](#custom-stand-alone-modules)
|
- [Custom Stand-Alone Modules](#custom-stand-alone-modules)
|
||||||
- [Custom Add-On Modules](#custom-add-on-modules)
|
- [Custom Add-On Modules](#custom-add-on-modules)
|
||||||
- [Custom Global Modules](#custom-global-modules)
|
- [Custom Global Modules](#custom-global-modules)
|
||||||
- [Custom Agents](#custom-agents)
|
- [Custom Agents](#custom-agents)
|
||||||
- [BMad Tiny Agents](#bmad-tiny-agents)
|
|
||||||
- [Simple and Expert Agents](#simple-and-expert-agents)
|
|
||||||
- [Custom Workflows](#custom-workflows)
|
- [Custom Workflows](#custom-workflows)
|
||||||
|
|
||||||
## Custom Stand-Alone Modules
|
## Custom Stand-Alone Modules
|
||||||
|
|
@ -62,6 +59,7 @@ Similar to Custom Stand-Alone Modules, but designed to add functionality that ap
|
||||||
|
|
||||||
Examples include:
|
Examples include:
|
||||||
|
|
||||||
|
- The current TTS (Text-to-Speech) functionality for Claude, which will soon be converted to a global module
|
||||||
- The core module, which is always installed and provides all agents with party mode and advanced elicitation capabilities
|
- The core module, which is always installed and provides all agents with party mode and advanced elicitation capabilities
|
||||||
- Installation and update tools that work with any BMad method configuration
|
- Installation and update tools that work with any BMad method configuration
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,18 +66,19 @@ Type "exit" or "done" to conclude the session. Participating agents will say per
|
||||||
|
|
||||||
## Example Party Compositions
|
## Example Party Compositions
|
||||||
|
|
||||||
| Topic | Typical Agents |
|
| Topic | Typical Agents |
|
||||||
| ---------------------- | ------------------------------------------------------------- |
|
|-------|---------------|
|
||||||
| **Product Strategy** | PM + Innovation Strategist (CIS) + Analyst |
|
| **Product Strategy** | PM + Innovation Strategist (CIS) + Analyst |
|
||||||
| **Technical Design** | Architect + Creative Problem Solver (CIS) + Game Architect |
|
| **Technical Design** | Architect + Creative Problem Solver (CIS) + Game Architect |
|
||||||
| **User Experience** | UX Designer + Design Thinking Coach (CIS) + Storyteller (CIS) |
|
| **User Experience** | UX Designer + Design Thinking Coach (CIS) + Storyteller (CIS) |
|
||||||
| **Quality Assessment** | TEA + DEV + Architect |
|
| **Quality Assessment** | TEA + DEV + Architect |
|
||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
- **Intelligent agent selection** — Selects based on expertise needed
|
- **Intelligent agent selection** — Selects based on expertise needed
|
||||||
- **Authentic personalities** — Each agent maintains their unique voice
|
- **Authentic personalities** — Each agent maintains their unique voice
|
||||||
- **Natural cross-talk** — Agents reference and build on each other
|
- **Natural cross-talk** — Agents reference and build on each other
|
||||||
|
- **Optional TTS** — Voice configurations for each agent
|
||||||
- **Graceful exit** — Personalized farewells
|
- **Graceful exit** — Personalized farewells
|
||||||
|
|
||||||
## Tips
|
## Tips
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@
|
||||||
"version": "6.0.0-alpha.23",
|
"version": "6.0.0-alpha.23",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "^0.11.0",
|
|
||||||
"@kayvan/markdown-tree-parser": "^1.6.1",
|
"@kayvan/markdown-tree-parser": "^1.6.1",
|
||||||
"boxen": "^5.1.2",
|
"boxen": "^5.1.2",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
|
|
@ -20,6 +19,7 @@
|
||||||
"fs-extra": "^11.3.0",
|
"fs-extra": "^11.3.0",
|
||||||
"glob": "^11.0.3",
|
"glob": "^11.0.3",
|
||||||
"ignore": "^7.0.5",
|
"ignore": "^7.0.5",
|
||||||
|
"inquirer": "^9.3.8",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"ora": "^5.4.1",
|
"ora": "^5.4.1",
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
|
|
@ -244,6 +244,7 @@
|
||||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
|
|
@ -755,27 +756,6 @@
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@clack/core": {
|
|
||||||
"version": "0.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@clack/core/-/core-0.5.0.tgz",
|
|
||||||
"integrity": "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"picocolors": "^1.0.0",
|
|
||||||
"sisteransi": "^1.0.5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@clack/prompts": {
|
|
||||||
"version": "0.11.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.11.0.tgz",
|
|
||||||
"integrity": "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@clack/core": "0.5.0",
|
|
||||||
"picocolors": "^1.0.0",
|
|
||||||
"sisteransi": "^1.0.5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@colors/colors": {
|
"node_modules/@colors/colors": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
|
||||||
|
|
@ -2018,6 +1998,36 @@
|
||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@inquirer/external-editor": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chardet": "^2.1.1",
|
||||||
|
"iconv-lite": "^0.7.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@inquirer/figures": {
|
||||||
|
"version": "1.0.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz",
|
||||||
|
"integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@isaacs/balanced-match": {
|
"node_modules/@isaacs/balanced-match": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||||
|
|
@ -3631,8 +3641,9 @@
|
||||||
"version": "25.0.3",
|
"version": "25.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
|
||||||
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
|
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
|
|
@ -3972,6 +3983,7 @@
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
|
|
@ -4019,7 +4031,6 @@
|
||||||
"version": "4.3.2",
|
"version": "4.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
|
||||||
"integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
|
"integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"type-fest": "^0.21.3"
|
"type-fest": "^0.21.3"
|
||||||
|
|
@ -4035,7 +4046,6 @@
|
||||||
"version": "0.21.3",
|
"version": "0.21.3",
|
||||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
|
||||||
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
|
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
|
||||||
"dev": true,
|
|
||||||
"license": "(MIT OR CC0-1.0)",
|
"license": "(MIT OR CC0-1.0)",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
|
|
@ -4280,6 +4290,7 @@
|
||||||
"integrity": "sha512-6mF/YrvwwRxLTu+aMEa5pwzKUNl5ZetWbTyZCs9Um0F12HUmxUiF5UHiZPy4rifzU3gtpM3xP2DfdmkNX9eZRg==",
|
"integrity": "sha512-6mF/YrvwwRxLTu+aMEa5pwzKUNl5ZetWbTyZCs9Um0F12HUmxUiF5UHiZPy4rifzU3gtpM3xP2DfdmkNX9eZRg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/compiler": "^2.13.0",
|
"@astrojs/compiler": "^2.13.0",
|
||||||
"@astrojs/internal-helpers": "0.7.5",
|
"@astrojs/internal-helpers": "0.7.5",
|
||||||
|
|
@ -5347,6 +5358,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
|
|
@ -5589,6 +5601,12 @@
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chardet": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||||
|
|
@ -5769,6 +5787,15 @@
|
||||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cli-width": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cliui": {
|
"node_modules/cliui": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||||
|
|
@ -6662,6 +6689,7 @@
|
||||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
|
|
@ -8241,6 +8269,22 @@
|
||||||
"@babel/runtime": "^7.23.2"
|
"@babel/runtime": "^7.23.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/iconv-lite": {
|
||||||
|
"version": "0.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz",
|
||||||
|
"integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ieee754": {
|
"node_modules/ieee754": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
|
|
@ -8376,6 +8420,43 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/inquirer": {
|
||||||
|
"version": "9.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.3.8.tgz",
|
||||||
|
"integrity": "sha512-pFGGdaHrmRKMh4WoDDSowddgjT1Vkl90atobmTeSmcPGdYiwikch/m/Ef5wRaiamHejtw0cUUMMerzDUXCci2w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@inquirer/external-editor": "^1.0.2",
|
||||||
|
"@inquirer/figures": "^1.0.3",
|
||||||
|
"ansi-escapes": "^4.3.2",
|
||||||
|
"cli-width": "^4.1.0",
|
||||||
|
"mute-stream": "1.0.0",
|
||||||
|
"ora": "^5.4.1",
|
||||||
|
"run-async": "^3.0.0",
|
||||||
|
"rxjs": "^7.8.1",
|
||||||
|
"string-width": "^4.2.3",
|
||||||
|
"strip-ansi": "^6.0.1",
|
||||||
|
"wrap-ansi": "^6.2.0",
|
||||||
|
"yoctocolors-cjs": "^2.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/inquirer/node_modules/wrap-ansi": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.0.0",
|
||||||
|
"string-width": "^4.1.0",
|
||||||
|
"strip-ansi": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/iron-webcrypto": {
|
"node_modules/iron-webcrypto": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz",
|
||||||
|
|
@ -10223,6 +10304,7 @@
|
||||||
"integrity": "sha512-p3JTemJJbkiMjXEMiFwgm0v6ym5g8K+b2oDny+6xdl300tUKySxvilJQLSea48C6OaYNmO30kH9KxpiAg5bWJw==",
|
"integrity": "sha512-p3JTemJJbkiMjXEMiFwgm0v6ym5g8K+b2oDny+6xdl300tUKySxvilJQLSea48C6OaYNmO30kH9KxpiAg5bWJw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"globby": "15.0.0",
|
"globby": "15.0.0",
|
||||||
"js-yaml": "4.1.1",
|
"js-yaml": "4.1.1",
|
||||||
|
|
@ -11494,6 +11576,15 @@
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/mute-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nano-spawn": {
|
"node_modules/nano-spawn": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz",
|
||||||
|
|
@ -12149,6 +12240,7 @@
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
|
|
@ -12286,6 +12378,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
|
|
@ -12351,6 +12444,7 @@
|
||||||
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin/prettier.cjs"
|
"prettier": "bin/prettier.cjs"
|
||||||
},
|
},
|
||||||
|
|
@ -13179,6 +13273,7 @@
|
||||||
"integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==",
|
"integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.8"
|
"@types/estree": "1.0.8"
|
||||||
},
|
},
|
||||||
|
|
@ -13215,6 +13310,15 @@
|
||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/run-async": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/run-parallel": {
|
"node_modules/run-parallel": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||||
|
|
@ -13239,6 +13343,15 @@
|
||||||
"queue-microtask": "^1.2.2"
|
"queue-microtask": "^1.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rxjs": {
|
||||||
|
"version": "7.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||||
|
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/safe-buffer": {
|
"node_modules/safe-buffer": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
|
@ -13259,6 +13372,12 @@
|
||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/sax": {
|
"node_modules/sax": {
|
||||||
"version": "1.4.3",
|
"version": "1.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz",
|
||||||
|
|
@ -13395,6 +13514,7 @@
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
||||||
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
|
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/sitemap": {
|
"node_modules/sitemap": {
|
||||||
|
|
@ -14131,7 +14251,6 @@
|
||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"dev": true,
|
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
|
|
@ -14216,7 +14335,7 @@
|
||||||
"version": "7.16.0",
|
"version": "7.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/unicode-properties": {
|
"node_modules/unicode-properties": {
|
||||||
|
|
@ -14718,6 +14837,7 @@
|
||||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.4.4",
|
||||||
|
|
@ -14991,6 +15111,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
|
||||||
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
|
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"yaml": "bin.mjs"
|
"yaml": "bin.mjs"
|
||||||
},
|
},
|
||||||
|
|
@ -15149,6 +15270,18 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/yoctocolors-cjs": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/zip-stream": {
|
"node_modules/zip-stream": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz",
|
||||||
|
|
@ -15170,6 +15303,7 @@
|
||||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,6 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "^0.11.0",
|
|
||||||
"@kayvan/markdown-tree-parser": "^1.6.1",
|
"@kayvan/markdown-tree-parser": "^1.6.1",
|
||||||
"boxen": "^5.1.2",
|
"boxen": "^5.1.2",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
|
|
@ -80,6 +79,7 @@
|
||||||
"fs-extra": "^11.3.0",
|
"fs-extra": "^11.3.0",
|
||||||
"glob": "^11.0.3",
|
"glob": "^11.0.3",
|
||||||
"ignore": "^7.0.5",
|
"ignore": "^7.0.5",
|
||||||
|
"inquirer": "^9.3.8",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"ora": "^5.4.1",
|
"ora": "^5.4.1",
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ agent:
|
||||||
|
|
||||||
critical_actions:
|
critical_actions:
|
||||||
- "Load into memory {project-root}/_bmad/core/config.yaml and set variable project_name, output_folder, user_name, communication_language"
|
- "Load into memory {project-root}/_bmad/core/config.yaml and set variable project_name, output_folder, user_name, communication_language"
|
||||||
|
- "Remember the users name is {user_name}"
|
||||||
- "ALWAYS communicate in {communication_language}"
|
- "ALWAYS communicate in {communication_language}"
|
||||||
|
|
||||||
menu:
|
menu:
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,7 @@ After agent loading and introduction:
|
||||||
- Handle missing or incomplete agent entries gracefully
|
- Handle missing or incomplete agent entries gracefully
|
||||||
- Cross-reference manifest with actual agent files
|
- Cross-reference manifest with actual agent files
|
||||||
- Prepare agent selection logic for intelligent conversation routing
|
- Prepare agent selection logic for intelligent conversation routing
|
||||||
|
- Set up TTS voice configurations for each agent
|
||||||
|
|
||||||
## NEXT STEP:
|
## NEXT STEP:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
- 🎯 SELECT RELEVANT AGENTS based on topic analysis and expertise matching
|
- 🎯 SELECT RELEVANT AGENTS based on topic analysis and expertise matching
|
||||||
- 📋 MAINTAIN CHARACTER CONSISTENCY using merged agent personalities
|
- 📋 MAINTAIN CHARACTER CONSISTENCY using merged agent personalities
|
||||||
- 🔍 ENABLE NATURAL CROSS-TALK between agents for dynamic conversation
|
- 🔍 ENABLE NATURAL CROSS-TALK between agents for dynamic conversation
|
||||||
|
- 💬 INTEGRATE TTS for each agent response immediately after text
|
||||||
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`
|
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`
|
||||||
|
|
||||||
## EXECUTION PROTOCOLS:
|
## EXECUTION PROTOCOLS:
|
||||||
|
|
@ -20,6 +21,7 @@
|
||||||
|
|
||||||
- Complete agent roster with merged personalities is available
|
- Complete agent roster with merged personalities is available
|
||||||
- User topic and conversation history guide agent selection
|
- User topic and conversation history guide agent selection
|
||||||
|
- Party mode is active with TTS integration enabled
|
||||||
- Exit triggers: `*exit`, `goodbye`, `end party`, `quit`
|
- Exit triggers: `*exit`, `goodbye`, `end party`, `quit`
|
||||||
|
|
||||||
## YOUR TASK:
|
## YOUR TASK:
|
||||||
|
|
@ -114,9 +116,19 @@ Allow natural back-and-forth within the same response round for dynamic interact
|
||||||
|
|
||||||
### 6. Response Round Completion
|
### 6. Response Round Completion
|
||||||
|
|
||||||
After generating all agent responses for the round, let the user know he can speak naturally with the agents, an then show this menu opion"
|
After generating all agent responses for the round:
|
||||||
|
|
||||||
`[E] Exit Party Mode - End the collaborative session`
|
**Presentation Format:**
|
||||||
|
[Agent 1 Response with TTS]
|
||||||
|
[Empty line for readability]
|
||||||
|
[Agent 2 Response with TTS, potentially referencing Agent 1]
|
||||||
|
[Empty line for readability]
|
||||||
|
[Agent 3 Response with TTS, building on or offering new perspective]
|
||||||
|
|
||||||
|
**Continue Option:**
|
||||||
|
"[Agents have contributed their perspectives. Ready for more discussion?]
|
||||||
|
|
||||||
|
[E] Exit Party Mode - End the collaborative session"
|
||||||
|
|
||||||
### 7. Exit Condition Checking
|
### 7. Exit Condition Checking
|
||||||
|
|
||||||
|
|
@ -130,19 +142,23 @@ Check for exit conditions before continuing:
|
||||||
**Natural Conclusion:**
|
**Natural Conclusion:**
|
||||||
|
|
||||||
- Conversation seems naturally concluding
|
- Conversation seems naturally concluding
|
||||||
- Confirm if the user wants to exit party mode and go back to where they were or continue chatting. Do it in a conversational way with an agent in the party.
|
- Ask user: "Would you like to continue the discussion or end party mode?"
|
||||||
|
- Respect user choice to continue or exit
|
||||||
|
|
||||||
### 8. Handle Exit Selection
|
### 8. Handle Exit Selection
|
||||||
|
|
||||||
#### If 'E' (Exit Party Mode):
|
#### If 'E' (Exit Party Mode):
|
||||||
|
|
||||||
- Load read and execute: `./step-03-graceful-exit.md`
|
- Update frontmatter: `stepsCompleted: [1, 2]`
|
||||||
|
- Set `party_active: false`
|
||||||
|
- Load: `./step-03-graceful-exit.md`
|
||||||
|
|
||||||
## SUCCESS METRICS:
|
## SUCCESS METRICS:
|
||||||
|
|
||||||
✅ Intelligent agent selection based on topic analysis
|
✅ Intelligent agent selection based on topic analysis
|
||||||
✅ Authentic in-character responses maintained consistently
|
✅ Authentic in-character responses maintained consistently
|
||||||
✅ Natural cross-talk and agent interactions enabled
|
✅ Natural cross-talk and agent interactions enabled
|
||||||
|
✅ TTS integration working for all agent responses
|
||||||
✅ Question handling protocol followed correctly
|
✅ Question handling protocol followed correctly
|
||||||
✅ [E] exit option presented after each response round
|
✅ [E] exit option presented after each response round
|
||||||
✅ Conversation context and state maintained throughout
|
✅ Conversation context and state maintained throughout
|
||||||
|
|
@ -152,6 +168,7 @@ Check for exit conditions before continuing:
|
||||||
|
|
||||||
❌ Generic responses without character consistency
|
❌ Generic responses without character consistency
|
||||||
❌ Poor agent selection not matching topic expertise
|
❌ Poor agent selection not matching topic expertise
|
||||||
|
❌ Missing TTS integration for agent responses
|
||||||
❌ Ignoring user questions or exit triggers
|
❌ Ignoring user questions or exit triggers
|
||||||
❌ Not enabling natural agent cross-talk and interactions
|
❌ Not enabling natural agent cross-talk and interactions
|
||||||
❌ Continuing conversation without user input when questions asked
|
❌ Continuing conversation without user input when questions asked
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,7 @@ workflow_completed: true
|
||||||
|
|
||||||
- Clear any active conversation state
|
- Clear any active conversation state
|
||||||
- Reset agent selection cache
|
- Reset agent selection cache
|
||||||
|
- Finalize TTS session cleanup
|
||||||
- Mark party mode workflow as completed
|
- Mark party mode workflow as completed
|
||||||
|
|
||||||
### 6. Exit Workflow
|
### 6. Exit Workflow
|
||||||
|
|
@ -121,6 +122,7 @@ Thank you for using BMAD Party Mode for collaborative multi-agent discussions!"
|
||||||
✅ Satisfying agent farewells generated in authentic character voices
|
✅ Satisfying agent farewells generated in authentic character voices
|
||||||
✅ Session highlights and contributions acknowledged meaningfully
|
✅ Session highlights and contributions acknowledged meaningfully
|
||||||
✅ Positive and appreciative closure atmosphere maintained
|
✅ Positive and appreciative closure atmosphere maintained
|
||||||
|
✅ TTS integration working for farewell messages
|
||||||
✅ Frontmatter properly updated with workflow completion
|
✅ Frontmatter properly updated with workflow completion
|
||||||
✅ All workflow state cleaned up appropriately
|
✅ All workflow state cleaned up appropriately
|
||||||
✅ User left with positive impression of collaborative experience
|
✅ User left with positive impression of collaborative experience
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,18 @@ If conversation naturally concludes:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## TTS INTEGRATION
|
||||||
|
|
||||||
|
Party mode includes Text-to-Speech for each agent response:
|
||||||
|
|
||||||
|
**TTS Protocol:**
|
||||||
|
|
||||||
|
- Trigger TTS immediately after each agent's text response
|
||||||
|
- Use agent's merged voice configuration from manifest
|
||||||
|
- Format: `Bash: .claude/hooks/bmad-speak.sh "[Agent Name]" "[Their response]"`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## MODERATION NOTES
|
## MODERATION NOTES
|
||||||
|
|
||||||
**Quality Control:**
|
**Quality Control:**
|
||||||
|
|
|
||||||
|
|
@ -42,15 +42,19 @@ Load `{moduleYamlConventionsFile}` for reference.
|
||||||
|
|
||||||
Create `{targetLocation}/module.yaml` with:
|
Create `{targetLocation}/module.yaml` with:
|
||||||
|
|
||||||
**Required fields (replace with actual values):**
|
**⚠️ CRITICAL: All required fields MUST be populated with actual values, not placeholders:**
|
||||||
|
|
||||||
|
**Required fields:**
|
||||||
```yaml
|
```yaml
|
||||||
code: "my-module" # kebab-case, 2-20 chars, starts with letter
|
code: {module_code} # ⚠️ REQUIRED: Must be the actual kebab-case module code (e.g., "my-module")
|
||||||
name: "My Module: Description" # human-readable name
|
name: "{module_display_name}" # ⚠️ REQUIRED: Human-readable name (e.g., "My Module: Description")
|
||||||
header: "One-line summary" # one-line summary
|
header: "{brief_header}" # ⚠️ REQUIRED: One-line summary
|
||||||
subheader: "Additional context" # additional context
|
subheader: "{additional_context}" # ⚠️ REQUIRED: Additional context
|
||||||
default_selected: false # typically false for new modules
|
default_selected: false # ⚠️ REQUIRED: Boolean, typically false for new modules
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Validation:** The module will fail installation if `code` is missing or contains placeholder text like `{module_code}`.
|
||||||
|
|
||||||
**Note for Extension modules:** `code:` matches base module
|
**Note for Extension modules:** `code:` matches base module
|
||||||
|
|
||||||
### 3. Add Custom Variables
|
### 3. Add Custom Variables
|
||||||
|
|
|
||||||
|
|
@ -38,12 +38,13 @@ Read `{targetPath}/module.yaml`
|
||||||
|
|
||||||
### 2. Validate Required Fields
|
### 2. Validate Required Fields
|
||||||
|
|
||||||
Check required fields (must have actual values, not placeholders):
|
**⚠️ CRITICAL:** Check for required frontmatter (these MUST be actual values, not placeholders):
|
||||||
- [ ] `code:` present (kebab-case, 2-20 chars, starts with letter)
|
- [ ] `code:` present and valid (kebab-case, 2-20 chars, starts with letter, e.g., `my-module`)
|
||||||
|
- ❌ FAIL if missing, empty, or contains placeholder text like `{module_code}`
|
||||||
- [ ] `name:` present (non-empty string)
|
- [ ] `name:` present (non-empty string)
|
||||||
- [ ] `header:` present (non-empty string)
|
- [ ] `header:` present (non-empty string)
|
||||||
- [ ] `subheader:` present (non-empty string)
|
- [ ] `subheader:` present (non-empty string)
|
||||||
- [ ] `default_selected:` present (boolean)
|
- [ ] `default_selected:` present (boolean - typically `false` for new modules)
|
||||||
|
|
||||||
### 3. Validate Custom Variables
|
### 3. Validate Custom Variables
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,6 @@ agent:
|
||||||
|
|
||||||
critical_actions:
|
critical_actions:
|
||||||
- "Consult {project-root}/_bmad/bmgd/gametest/qa-index.csv to select knowledge fragments under knowledge/ and load only the files needed for the current task"
|
- "Consult {project-root}/_bmad/bmgd/gametest/qa-index.csv to select knowledge fragments under knowledge/ and load only the files needed for the current task"
|
||||||
- "For E2E testing requests, always load knowledge/e2e-testing.md first"
|
|
||||||
- "When scaffolding tests, distinguish between unit, integration, and E2E test needs"
|
|
||||||
- "Load the referenced fragment(s) from {project-root}/_bmad/bmgd/gametest/knowledge/ before giving recommendations"
|
- "Load the referenced fragment(s) from {project-root}/_bmad/bmgd/gametest/knowledge/ before giving recommendations"
|
||||||
- "Cross-check recommendations with the current official Unity Test Framework, Unreal Automation, or Godot GUT documentation"
|
- "Cross-check recommendations with the current official Unity Test Framework, Unreal Automation, or Godot GUT documentation"
|
||||||
- "Find if this exists, if it does, always treat it as the bible I plan and execute against: `**/project-context.md`"
|
- "Find if this exists, if it does, always treat it as the bible I plan and execute against: `**/project-context.md`"
|
||||||
|
|
@ -45,10 +43,6 @@ agent:
|
||||||
workflow: "{project-root}/_bmad/bmgd/workflows/gametest/automate/workflow.yaml"
|
workflow: "{project-root}/_bmad/bmgd/workflows/gametest/automate/workflow.yaml"
|
||||||
description: "[TA] Generate automated game tests"
|
description: "[TA] Generate automated game tests"
|
||||||
|
|
||||||
- trigger: ES or fuzzy match on e2e-scaffold
|
|
||||||
workflow: "{project-root}/_bmad/bmgd/workflows/gametest/e2e-scaffold/workflow.yaml"
|
|
||||||
description: "[ES] Scaffold E2E testing infrastructure"
|
|
||||||
|
|
||||||
- trigger: PP or fuzzy match on playtest-plan
|
- trigger: PP or fuzzy match on playtest-plan
|
||||||
workflow: "{project-root}/_bmad/bmgd/workflows/gametest/playtest-plan/workflow.yaml"
|
workflow: "{project-root}/_bmad/bmgd/workflows/gametest/playtest-plan/workflow.yaml"
|
||||||
description: "[PP] Create structured playtesting plan"
|
description: "[PP] Create structured playtesting plan"
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -374,502 +374,3 @@ test:
|
||||||
| Signal not detected | Signal not watched | Call `watch_signals()` before action |
|
| Signal not detected | Signal not watched | Call `watch_signals()` before action |
|
||||||
| Physics not working | Missing frames | Await `physics_frame` |
|
| Physics not working | Missing frames | Await `physics_frame` |
|
||||||
| Flaky tests | Timing issues | Use proper await/signals |
|
| Flaky tests | Timing issues | Use proper await/signals |
|
||||||
|
|
||||||
## C# Testing in Godot
|
|
||||||
|
|
||||||
Godot 4 supports C# via .NET 6+. You can use standard .NET testing frameworks alongside GUT.
|
|
||||||
|
|
||||||
### Project Setup for C#
|
|
||||||
|
|
||||||
```
|
|
||||||
project/
|
|
||||||
├── addons/
|
|
||||||
│ └── gut/
|
|
||||||
├── src/
|
|
||||||
│ ├── Player/
|
|
||||||
│ │ └── PlayerController.cs
|
|
||||||
│ └── Combat/
|
|
||||||
│ └── DamageCalculator.cs
|
|
||||||
├── tests/
|
|
||||||
│ ├── gdscript/
|
|
||||||
│ │ └── test_integration.gd
|
|
||||||
│ └── csharp/
|
|
||||||
│ ├── Tests.csproj
|
|
||||||
│ └── DamageCalculatorTests.cs
|
|
||||||
└── project.csproj
|
|
||||||
```
|
|
||||||
|
|
||||||
### C# Test Project Setup
|
|
||||||
|
|
||||||
Create a separate test project that references your game assembly:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<!-- tests/csharp/Tests.csproj -->
|
|
||||||
<Project Sdk="Godot.NET.Sdk/4.2.0">
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
|
||||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
|
||||||
<IsPackable>false</IsPackable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
|
||||||
<PackageReference Include="xunit" Version="2.6.2" />
|
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4" />
|
|
||||||
<PackageReference Include="NSubstitute" Version="5.1.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="../../project.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Basic C# Unit Tests
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// tests/csharp/DamageCalculatorTests.cs
|
|
||||||
using Xunit;
|
|
||||||
using YourGame.Combat;
|
|
||||||
|
|
||||||
public class DamageCalculatorTests
|
|
||||||
{
|
|
||||||
private readonly DamageCalculator _calculator;
|
|
||||||
|
|
||||||
public DamageCalculatorTests()
|
|
||||||
{
|
|
||||||
_calculator = new DamageCalculator();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Calculate_BaseDamage_ReturnsCorrectValue()
|
|
||||||
{
|
|
||||||
var result = _calculator.Calculate(100f, 1f);
|
|
||||||
Assert.Equal(100f, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Calculate_CriticalHit_DoublesDamage()
|
|
||||||
{
|
|
||||||
var result = _calculator.Calculate(100f, 2f);
|
|
||||||
Assert.Equal(200f, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(100f, 0.5f, 50f)]
|
|
||||||
[InlineData(100f, 1.5f, 150f)]
|
|
||||||
[InlineData(50f, 2f, 100f)]
|
|
||||||
public void Calculate_Parameterized_ReturnsExpected(
|
|
||||||
float baseDamage, float multiplier, float expected)
|
|
||||||
{
|
|
||||||
var result = _calculator.Calculate(baseDamage, multiplier);
|
|
||||||
Assert.Equal(expected, result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Testing Godot Nodes in C#
|
|
||||||
|
|
||||||
For tests requiring Godot runtime, use a hybrid approach:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
// tests/csharp/PlayerControllerTests.cs
|
|
||||||
using Godot;
|
|
||||||
using Xunit;
|
|
||||||
using YourGame.Player;
|
|
||||||
|
|
||||||
public class PlayerControllerTests : IDisposable
|
|
||||||
{
|
|
||||||
private readonly SceneTree _sceneTree;
|
|
||||||
private PlayerController _player;
|
|
||||||
|
|
||||||
public PlayerControllerTests()
|
|
||||||
{
|
|
||||||
// These tests must run within Godot runtime
|
|
||||||
// Use GodotXUnit or similar adapter
|
|
||||||
}
|
|
||||||
|
|
||||||
[GodotFact] // Custom attribute for Godot runtime tests
|
|
||||||
public async Task Player_Move_ChangesPosition()
|
|
||||||
{
|
|
||||||
var startPos = _player.GlobalPosition;
|
|
||||||
|
|
||||||
_player.SetInput(new Vector2(1, 0));
|
|
||||||
|
|
||||||
await ToSignal(GetTree().CreateTimer(0.5f), "timeout");
|
|
||||||
|
|
||||||
Assert.True(_player.GlobalPosition.X > startPos.X);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_player?.QueueFree();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### C# Mocking with NSubstitute
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
using NSubstitute;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
public class EnemyAITests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void Enemy_UsesPathfinding_WhenMoving()
|
|
||||||
{
|
|
||||||
var mockPathfinding = Substitute.For<IPathfinding>();
|
|
||||||
mockPathfinding.FindPath(Arg.Any<Vector2>(), Arg.Any<Vector2>())
|
|
||||||
.Returns(new[] { Vector2.Zero, new Vector2(10, 10) });
|
|
||||||
|
|
||||||
var enemy = new EnemyAI(mockPathfinding);
|
|
||||||
|
|
||||||
enemy.MoveTo(new Vector2(10, 10));
|
|
||||||
|
|
||||||
mockPathfinding.Received().FindPath(
|
|
||||||
Arg.Any<Vector2>(),
|
|
||||||
Arg.Is<Vector2>(v => v == new Vector2(10, 10)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Running C# Tests
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run C# unit tests (no Godot runtime needed)
|
|
||||||
dotnet test tests/csharp/Tests.csproj
|
|
||||||
|
|
||||||
# Run with coverage
|
|
||||||
dotnet test tests/csharp/Tests.csproj --collect:"XPlat Code Coverage"
|
|
||||||
|
|
||||||
# Run specific test
|
|
||||||
dotnet test tests/csharp/Tests.csproj --filter "FullyQualifiedName~DamageCalculator"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Hybrid Test Strategy
|
|
||||||
|
|
||||||
| Test Type | Framework | When to Use |
|
|
||||||
| ------------- | ---------------- | ---------------------------------- |
|
|
||||||
| Pure logic | xUnit/NUnit (C#) | Classes without Godot dependencies |
|
|
||||||
| Node behavior | GUT (GDScript) | MonoBehaviour-like testing |
|
|
||||||
| Integration | GUT (GDScript) | Scene and signal testing |
|
|
||||||
| E2E | GUT (GDScript) | Full gameplay flows |
|
|
||||||
|
|
||||||
## End-to-End Testing
|
|
||||||
|
|
||||||
For comprehensive E2E testing patterns, infrastructure scaffolding, and
|
|
||||||
scenario builders, see **knowledge/e2e-testing.md**.
|
|
||||||
|
|
||||||
### E2E Infrastructure for Godot
|
|
||||||
|
|
||||||
#### GameE2ETestFixture (GDScript)
|
|
||||||
|
|
||||||
```gdscript
|
|
||||||
# tests/e2e/infrastructure/game_e2e_test_fixture.gd
|
|
||||||
extends GutTest
|
|
||||||
class_name GameE2ETestFixture
|
|
||||||
|
|
||||||
var game_state: GameStateManager
|
|
||||||
var input_sim: InputSimulator
|
|
||||||
var scenario: ScenarioBuilder
|
|
||||||
var _scene_instance: Node
|
|
||||||
|
|
||||||
## Override to specify a different scene for specific test classes.
|
|
||||||
func get_scene_path() -> String:
|
|
||||||
return "res://scenes/game.tscn"
|
|
||||||
|
|
||||||
func before_each():
|
|
||||||
# Load game scene
|
|
||||||
var scene = load(get_scene_path())
|
|
||||||
_scene_instance = scene.instantiate()
|
|
||||||
add_child(_scene_instance)
|
|
||||||
|
|
||||||
# Get references
|
|
||||||
game_state = _scene_instance.get_node("GameStateManager")
|
|
||||||
assert_not_null(game_state, "GameStateManager not found in scene")
|
|
||||||
|
|
||||||
input_sim = InputSimulator.new()
|
|
||||||
scenario = ScenarioBuilder.new(game_state)
|
|
||||||
|
|
||||||
# Wait for ready
|
|
||||||
await wait_for_game_ready()
|
|
||||||
|
|
||||||
func after_each():
|
|
||||||
if _scene_instance:
|
|
||||||
_scene_instance.queue_free()
|
|
||||||
_scene_instance = null
|
|
||||||
input_sim = null
|
|
||||||
scenario = null
|
|
||||||
|
|
||||||
func wait_for_game_ready(timeout: float = 10.0):
|
|
||||||
var elapsed = 0.0
|
|
||||||
while not game_state.is_ready and elapsed < timeout:
|
|
||||||
await get_tree().process_frame
|
|
||||||
elapsed += get_process_delta_time()
|
|
||||||
assert_true(game_state.is_ready, "Game should be ready within timeout")
|
|
||||||
```
|
|
||||||
|
|
||||||
#### ScenarioBuilder (GDScript)
|
|
||||||
|
|
||||||
```gdscript
|
|
||||||
# tests/e2e/infrastructure/scenario_builder.gd
|
|
||||||
extends RefCounted
|
|
||||||
class_name ScenarioBuilder
|
|
||||||
|
|
||||||
var _game_state: GameStateManager
|
|
||||||
var _setup_actions: Array[Callable] = []
|
|
||||||
|
|
||||||
func _init(game_state: GameStateManager):
|
|
||||||
_game_state = game_state
|
|
||||||
|
|
||||||
## Load a pre-configured scenario from a save file.
|
|
||||||
func from_save_file(file_name: String) -> ScenarioBuilder:
|
|
||||||
_setup_actions.append(func(): await _load_save_file(file_name))
|
|
||||||
return self
|
|
||||||
|
|
||||||
## Configure the current turn number.
|
|
||||||
func on_turn(turn_number: int) -> ScenarioBuilder:
|
|
||||||
_setup_actions.append(func(): _set_turn(turn_number))
|
|
||||||
return self
|
|
||||||
|
|
||||||
## Spawn a unit at position.
|
|
||||||
func with_unit(faction: int, position: Vector2, movement_points: int = 6) -> ScenarioBuilder:
|
|
||||||
_setup_actions.append(func(): await _spawn_unit(faction, position, movement_points))
|
|
||||||
return self
|
|
||||||
|
|
||||||
## Execute all configured setup actions.
|
|
||||||
func build() -> void:
|
|
||||||
for action in _setup_actions:
|
|
||||||
await action.call()
|
|
||||||
_setup_actions.clear()
|
|
||||||
|
|
||||||
## Clear pending actions without executing.
|
|
||||||
func reset() -> void:
|
|
||||||
_setup_actions.clear()
|
|
||||||
|
|
||||||
# Private implementation
|
|
||||||
func _load_save_file(file_name: String) -> void:
|
|
||||||
var path = "res://tests/e2e/test_data/%s" % file_name
|
|
||||||
await _game_state.load_game(path)
|
|
||||||
|
|
||||||
func _set_turn(turn: int) -> void:
|
|
||||||
_game_state.set_turn_number(turn)
|
|
||||||
|
|
||||||
func _spawn_unit(faction: int, pos: Vector2, mp: int) -> void:
|
|
||||||
var unit = _game_state.spawn_unit(faction, pos)
|
|
||||||
unit.movement_points = mp
|
|
||||||
```
|
|
||||||
|
|
||||||
#### InputSimulator (GDScript)
|
|
||||||
|
|
||||||
```gdscript
|
|
||||||
# tests/e2e/infrastructure/input_simulator.gd
|
|
||||||
extends RefCounted
|
|
||||||
class_name InputSimulator
|
|
||||||
|
|
||||||
## Click at a world position.
|
|
||||||
func click_world_position(world_pos: Vector2) -> void:
|
|
||||||
var viewport = Engine.get_main_loop().root.get_viewport()
|
|
||||||
var camera = viewport.get_camera_2d()
|
|
||||||
var screen_pos = camera.get_screen_center_position() + (world_pos - camera.global_position)
|
|
||||||
await click_screen_position(screen_pos)
|
|
||||||
|
|
||||||
## Click at a screen position.
|
|
||||||
func click_screen_position(screen_pos: Vector2) -> void:
|
|
||||||
var press = InputEventMouseButton.new()
|
|
||||||
press.button_index = MOUSE_BUTTON_LEFT
|
|
||||||
press.pressed = true
|
|
||||||
press.position = screen_pos
|
|
||||||
|
|
||||||
var release = InputEventMouseButton.new()
|
|
||||||
release.button_index = MOUSE_BUTTON_LEFT
|
|
||||||
release.pressed = false
|
|
||||||
release.position = screen_pos
|
|
||||||
|
|
||||||
Input.parse_input_event(press)
|
|
||||||
await Engine.get_main_loop().process_frame
|
|
||||||
Input.parse_input_event(release)
|
|
||||||
await Engine.get_main_loop().process_frame
|
|
||||||
|
|
||||||
## Click a UI button by name.
|
|
||||||
func click_button(button_name: String) -> void:
|
|
||||||
var root = Engine.get_main_loop().root
|
|
||||||
var button = _find_button_recursive(root, button_name)
|
|
||||||
assert(button != null, "Button '%s' not found in scene tree" % button_name)
|
|
||||||
|
|
||||||
if not button.visible:
|
|
||||||
push_warning("[InputSimulator] Button '%s' is not visible" % button_name)
|
|
||||||
if button.disabled:
|
|
||||||
push_warning("[InputSimulator] Button '%s' is disabled" % button_name)
|
|
||||||
|
|
||||||
button.pressed.emit()
|
|
||||||
await Engine.get_main_loop().process_frame
|
|
||||||
|
|
||||||
func _find_button_recursive(node: Node, button_name: String) -> Button:
|
|
||||||
if node is Button and node.name == button_name:
|
|
||||||
return node
|
|
||||||
for child in node.get_children():
|
|
||||||
var found = _find_button_recursive(child, button_name)
|
|
||||||
if found:
|
|
||||||
return found
|
|
||||||
return null
|
|
||||||
|
|
||||||
## Press and release a key.
|
|
||||||
func press_key(keycode: Key) -> void:
|
|
||||||
var press = InputEventKey.new()
|
|
||||||
press.keycode = keycode
|
|
||||||
press.pressed = true
|
|
||||||
|
|
||||||
var release = InputEventKey.new()
|
|
||||||
release.keycode = keycode
|
|
||||||
release.pressed = false
|
|
||||||
|
|
||||||
Input.parse_input_event(press)
|
|
||||||
await Engine.get_main_loop().process_frame
|
|
||||||
Input.parse_input_event(release)
|
|
||||||
await Engine.get_main_loop().process_frame
|
|
||||||
|
|
||||||
## Simulate an input action.
|
|
||||||
func action_press(action_name: String) -> void:
|
|
||||||
Input.action_press(action_name)
|
|
||||||
await Engine.get_main_loop().process_frame
|
|
||||||
|
|
||||||
func action_release(action_name: String) -> void:
|
|
||||||
Input.action_release(action_name)
|
|
||||||
await Engine.get_main_loop().process_frame
|
|
||||||
|
|
||||||
## Reset all input state.
|
|
||||||
func reset() -> void:
|
|
||||||
Input.flush_buffered_events()
|
|
||||||
```
|
|
||||||
|
|
||||||
#### AsyncAssert (GDScript)
|
|
||||||
|
|
||||||
```gdscript
|
|
||||||
# tests/e2e/infrastructure/async_assert.gd
|
|
||||||
extends RefCounted
|
|
||||||
class_name AsyncAssert
|
|
||||||
|
|
||||||
## Wait until condition is true, or fail after timeout.
|
|
||||||
static func wait_until(
|
|
||||||
condition: Callable,
|
|
||||||
description: String,
|
|
||||||
timeout: float = 5.0
|
|
||||||
) -> void:
|
|
||||||
var elapsed := 0.0
|
|
||||||
while not condition.call() and elapsed < timeout:
|
|
||||||
await Engine.get_main_loop().process_frame
|
|
||||||
elapsed += Engine.get_main_loop().root.get_process_delta_time()
|
|
||||||
|
|
||||||
assert(condition.call(),
|
|
||||||
"Timeout after %.1fs waiting for: %s" % [timeout, description])
|
|
||||||
|
|
||||||
## Wait for a value to equal expected.
|
|
||||||
static func wait_for_value(
|
|
||||||
getter: Callable,
|
|
||||||
expected: Variant,
|
|
||||||
description: String,
|
|
||||||
timeout: float = 5.0
|
|
||||||
) -> void:
|
|
||||||
await wait_until(
|
|
||||||
func(): return getter.call() == expected,
|
|
||||||
"%s to equal '%s' (current: '%s')" % [description, expected, getter.call()],
|
|
||||||
timeout)
|
|
||||||
|
|
||||||
## Wait for a float value within tolerance.
|
|
||||||
static func wait_for_value_approx(
|
|
||||||
getter: Callable,
|
|
||||||
expected: float,
|
|
||||||
description: String,
|
|
||||||
tolerance: float = 0.0001,
|
|
||||||
timeout: float = 5.0
|
|
||||||
) -> void:
|
|
||||||
await wait_until(
|
|
||||||
func(): return absf(expected - getter.call()) < tolerance,
|
|
||||||
"%s to equal ~%s ±%s (current: %s)" % [description, expected, tolerance, getter.call()],
|
|
||||||
timeout)
|
|
||||||
|
|
||||||
## Assert that condition does NOT become true within duration.
|
|
||||||
static func assert_never_true(
|
|
||||||
condition: Callable,
|
|
||||||
description: String,
|
|
||||||
duration: float = 1.0
|
|
||||||
) -> void:
|
|
||||||
var elapsed := 0.0
|
|
||||||
while elapsed < duration:
|
|
||||||
assert(not condition.call(),
|
|
||||||
"Condition unexpectedly became true: %s" % description)
|
|
||||||
await Engine.get_main_loop().process_frame
|
|
||||||
elapsed += Engine.get_main_loop().root.get_process_delta_time()
|
|
||||||
|
|
||||||
## Wait for specified number of frames.
|
|
||||||
static func wait_frames(count: int) -> void:
|
|
||||||
for i in range(count):
|
|
||||||
await Engine.get_main_loop().process_frame
|
|
||||||
|
|
||||||
## Wait for physics to settle.
|
|
||||||
static func wait_for_physics(frames: int = 3) -> void:
|
|
||||||
for i in range(frames):
|
|
||||||
await Engine.get_main_loop().root.get_tree().physics_frame
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example E2E Test (GDScript)
|
|
||||||
|
|
||||||
```gdscript
|
|
||||||
# tests/e2e/scenarios/test_combat_flow.gd
|
|
||||||
extends GameE2ETestFixture
|
|
||||||
|
|
||||||
func test_player_can_attack_enemy():
|
|
||||||
# GIVEN: Player and enemy in combat range
|
|
||||||
await scenario \
|
|
||||||
.with_unit(Faction.PLAYER, Vector2(100, 100)) \
|
|
||||||
.with_unit(Faction.ENEMY, Vector2(150, 100)) \
|
|
||||||
.build()
|
|
||||||
|
|
||||||
var enemy = game_state.get_units(Faction.ENEMY)[0]
|
|
||||||
var initial_health = enemy.health
|
|
||||||
|
|
||||||
# WHEN: Player attacks
|
|
||||||
await input_sim.click_world_position(Vector2(100, 100)) # Select player
|
|
||||||
await AsyncAssert.wait_until(
|
|
||||||
func(): return game_state.selected_unit != null,
|
|
||||||
"Unit should be selected")
|
|
||||||
|
|
||||||
await input_sim.click_world_position(Vector2(150, 100)) # Attack enemy
|
|
||||||
|
|
||||||
# THEN: Enemy takes damage
|
|
||||||
await AsyncAssert.wait_until(
|
|
||||||
func(): return enemy.health < initial_health,
|
|
||||||
"Enemy should take damage")
|
|
||||||
|
|
||||||
func test_turn_cycle_completes():
|
|
||||||
# GIVEN: Game in progress
|
|
||||||
await scenario.on_turn(1).build()
|
|
||||||
var starting_turn = game_state.turn_number
|
|
||||||
|
|
||||||
# WHEN: Player ends turn
|
|
||||||
await input_sim.click_button("EndTurnButton")
|
|
||||||
await AsyncAssert.wait_until(
|
|
||||||
func(): return game_state.current_faction == Faction.ENEMY,
|
|
||||||
"Should switch to enemy turn")
|
|
||||||
|
|
||||||
# AND: Enemy turn completes
|
|
||||||
await AsyncAssert.wait_until(
|
|
||||||
func(): return game_state.current_faction == Faction.PLAYER,
|
|
||||||
"Should return to player turn",
|
|
||||||
30.0) # AI might take a while
|
|
||||||
|
|
||||||
# THEN: Turn number incremented
|
|
||||||
assert_eq(game_state.turn_number, starting_turn + 1)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Quick E2E Checklist for Godot
|
|
||||||
|
|
||||||
- [ ] Create `GameE2ETestFixture` base class extending GutTest
|
|
||||||
- [ ] Implement `ScenarioBuilder` for your game's domain
|
|
||||||
- [ ] Create `InputSimulator` wrapping Godot Input
|
|
||||||
- [ ] Add `AsyncAssert` utilities with proper await
|
|
||||||
- [ ] Organize E2E tests under `tests/e2e/scenarios/`
|
|
||||||
- [ ] Configure GUT to include E2E test directory
|
|
||||||
- [ ] Set up CI with headless Godot execution
|
|
||||||
|
|
|
||||||
|
|
@ -381,17 +381,3 @@ test:
|
||||||
| NullReferenceException | Missing Setup | Ensure [SetUp] initializes all fields |
|
| NullReferenceException | Missing Setup | Ensure [SetUp] initializes all fields |
|
||||||
| Tests hang | Infinite coroutine | Add timeout or max iterations |
|
| Tests hang | Infinite coroutine | Add timeout or max iterations |
|
||||||
| Flaky physics tests | Timing dependent | Use WaitForFixedUpdate, increase tolerance |
|
| Flaky physics tests | Timing dependent | Use WaitForFixedUpdate, increase tolerance |
|
||||||
|
|
||||||
## End-to-End Testing
|
|
||||||
|
|
||||||
For comprehensive E2E testing patterns, infrastructure scaffolding, and
|
|
||||||
scenario builders, see **knowledge/e2e-testing.md**.
|
|
||||||
|
|
||||||
### Quick E2E Checklist for Unity
|
|
||||||
|
|
||||||
- [ ] Create `GameE2ETestFixture` base class
|
|
||||||
- [ ] Implement `ScenarioBuilder` for your game's domain
|
|
||||||
- [ ] Create `InputSimulator` wrapping Input System
|
|
||||||
- [ ] Add `AsyncAssert` utilities
|
|
||||||
- [ ] Organize E2E tests under `Tests/PlayMode/E2E/`
|
|
||||||
- [ ] Configure separate CI job for E2E suite
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -15,4 +15,3 @@ localization-testing,Localization Testing,"Text, audio, and cultural validation
|
||||||
certification-testing,Platform Certification,"Console TRC/XR requirements and certification testing","certification,console,trc,xr",knowledge/certification-testing.md
|
certification-testing,Platform Certification,"Console TRC/XR requirements and certification testing","certification,console,trc,xr",knowledge/certification-testing.md
|
||||||
smoke-testing,Smoke Testing,"Critical path validation for build verification","smoke-tests,bvt,ci",knowledge/smoke-testing.md
|
smoke-testing,Smoke Testing,"Critical path validation for build verification","smoke-tests,bvt,ci",knowledge/smoke-testing.md
|
||||||
test-priorities,Test Priorities Matrix,"P0-P3 criteria, coverage targets, execution ordering for games","prioritization,risk,coverage",knowledge/test-priorities.md
|
test-priorities,Test Priorities Matrix,"P0-P3 criteria, coverage targets, execution ordering for games","prioritization,risk,coverage",knowledge/test-priorities.md
|
||||||
e2e-testing,End-to-End Testing,"Complete player journey testing with infrastructure patterns and async utilities","e2e,integration,player-journeys,scenarios,infrastructure",knowledge/e2e-testing.md
|
|
||||||
|
|
|
||||||
|
|
|
@ -209,87 +209,6 @@ func test_{feature}_integration():
|
||||||
# Cleanup
|
# Cleanup
|
||||||
scene.queue_free()
|
scene.queue_free()
|
||||||
```
|
```
|
||||||
### E2E Journey Tests
|
|
||||||
|
|
||||||
**Knowledge Base Reference**: `knowledge/e2e-testing.md`
|
|
||||||
```csharp
|
|
||||||
public class {Feature}E2ETests : GameE2ETestFixture
|
|
||||||
{
|
|
||||||
[UnityTest]
|
|
||||||
public IEnumerator {JourneyName}_Succeeds()
|
|
||||||
{
|
|
||||||
// GIVEN
|
|
||||||
yield return Scenario
|
|
||||||
.{SetupMethod1}()
|
|
||||||
.{SetupMethod2}()
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
// WHEN
|
|
||||||
yield return Input.{Action1}();
|
|
||||||
yield return AsyncAssert.WaitUntil(
|
|
||||||
() => {Condition1}, "{Description1}");
|
|
||||||
yield return Input.{Action2}();
|
|
||||||
|
|
||||||
// THEN
|
|
||||||
yield return AsyncAssert.WaitUntil(
|
|
||||||
() => {FinalCondition}, "{FinalDescription}");
|
|
||||||
Assert.{Assertion}({expected}, {actual});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Step 3.5: Generate E2E Infrastructure
|
|
||||||
|
|
||||||
Before generating E2E tests, scaffold the required infrastructure.
|
|
||||||
|
|
||||||
### Infrastructure Checklist
|
|
||||||
|
|
||||||
1. **Test Fixture Base Class**
|
|
||||||
- Scene loading/unloading
|
|
||||||
- Game ready state waiting
|
|
||||||
- Common service access
|
|
||||||
- Cleanup guarantees
|
|
||||||
|
|
||||||
2. **Scenario Builder**
|
|
||||||
- Fluent API for game state configuration
|
|
||||||
- Domain-specific methods (e.g., `WithUnit`, `OnTurn`)
|
|
||||||
- Yields for state propagation
|
|
||||||
|
|
||||||
3. **Input Simulator**
|
|
||||||
- Click/drag abstractions
|
|
||||||
- Button press simulation
|
|
||||||
- Keyboard input queuing
|
|
||||||
|
|
||||||
4. **Async Assertions**
|
|
||||||
- `WaitUntil` with timeout and message
|
|
||||||
- `WaitForEvent` for event-driven flows
|
|
||||||
- `WaitForState` for state machine transitions
|
|
||||||
|
|
||||||
### Generation Template
|
|
||||||
```csharp
|
|
||||||
// GameE2ETestFixture.cs
|
|
||||||
public abstract class GameE2ETestFixture
|
|
||||||
{
|
|
||||||
protected {GameStateClass} GameState;
|
|
||||||
protected {InputSimulatorClass} Input;
|
|
||||||
protected {ScenarioBuilderClass} Scenario;
|
|
||||||
|
|
||||||
[UnitySetUp]
|
|
||||||
public IEnumerator BaseSetUp()
|
|
||||||
{
|
|
||||||
yield return LoadScene("{main_scene}");
|
|
||||||
GameState = Object.FindFirstObjectByType<{GameStateClass}>();
|
|
||||||
Input = new {InputSimulatorClass}();
|
|
||||||
Scenario = new {ScenarioBuilderClass}(GameState);
|
|
||||||
yield return WaitForReady();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ... (fill from e2e-testing.md patterns)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**After scaffolding infrastructure, proceed to generate actual E2E tests.**
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
# E2E Infrastructure Scaffold Checklist
|
|
||||||
|
|
||||||
## Preflight Validation
|
|
||||||
|
|
||||||
- [ ] Test framework already initialized (`Tests/` directory exists with proper structure)
|
|
||||||
- [ ] Game state manager class identified
|
|
||||||
- [ ] Main gameplay scene identified and loads without errors
|
|
||||||
- [ ] No existing E2E infrastructure conflicts
|
|
||||||
|
|
||||||
## Architecture Analysis
|
|
||||||
|
|
||||||
- [ ] Game engine correctly detected
|
|
||||||
- [ ] Engine version identified
|
|
||||||
- [ ] Input system type determined (New Input System, Legacy, Custom)
|
|
||||||
- [ ] Game state manager class located
|
|
||||||
- [ ] Ready/initialized state property identified
|
|
||||||
- [ ] Key domain entities catalogued for ScenarioBuilder
|
|
||||||
|
|
||||||
## Generated Files
|
|
||||||
|
|
||||||
### Directory Structure
|
|
||||||
- [ ] `Tests/PlayMode/E2E/` directory created
|
|
||||||
- [ ] `Tests/PlayMode/E2E/Infrastructure/` directory created
|
|
||||||
- [ ] `Tests/PlayMode/E2E/Scenarios/` directory created
|
|
||||||
- [ ] `Tests/PlayMode/E2E/TestData/` directory created
|
|
||||||
|
|
||||||
### Infrastructure Files
|
|
||||||
- [ ] `E2E.asmdef` created with correct assembly references
|
|
||||||
- [ ] `GameE2ETestFixture.cs` created with correct class references
|
|
||||||
- [ ] `ScenarioBuilder.cs` created with at least placeholder methods
|
|
||||||
- [ ] `InputSimulator.cs` created matching detected input system
|
|
||||||
- [ ] `AsyncAssert.cs` created with core assertion methods
|
|
||||||
|
|
||||||
### Example and Documentation
|
|
||||||
- [ ] `ExampleE2ETest.cs` created with working infrastructure test
|
|
||||||
- [ ] `README.md` created with usage documentation
|
|
||||||
|
|
||||||
## Code Quality
|
|
||||||
|
|
||||||
### GameE2ETestFixture
|
|
||||||
- [ ] Correct namespace applied
|
|
||||||
- [ ] Correct `GameStateClass` reference
|
|
||||||
- [ ] Correct `SceneName` default
|
|
||||||
- [ ] `WaitForGameReady` uses correct ready property
|
|
||||||
- [ ] `UnitySetUp` and `UnityTearDown` properly structured
|
|
||||||
- [ ] Virtual methods for derived class customization
|
|
||||||
|
|
||||||
### ScenarioBuilder
|
|
||||||
- [ ] Fluent API pattern correctly implemented
|
|
||||||
- [ ] `Build()` executes all queued actions
|
|
||||||
- [ ] At least one domain-specific method added (or clear TODOs)
|
|
||||||
- [ ] `FromSaveFile` method scaffolded
|
|
||||||
|
|
||||||
### InputSimulator
|
|
||||||
- [ ] Matches detected input system (New vs Legacy)
|
|
||||||
- [ ] Mouse click simulation works
|
|
||||||
- [ ] Button click by name works
|
|
||||||
- [ ] Keyboard input scaffolded
|
|
||||||
- [ ] `Reset()` method cleans up state
|
|
||||||
|
|
||||||
### AsyncAssert
|
|
||||||
- [ ] `WaitUntil` includes timeout and descriptive failure
|
|
||||||
- [ ] `WaitForValue` provides current vs expected in failure
|
|
||||||
- [ ] `AssertNeverTrue` for negative assertions
|
|
||||||
- [ ] Frame/physics wait utilities included
|
|
||||||
|
|
||||||
## Assembly Definition
|
|
||||||
|
|
||||||
- [ ] References main game assembly
|
|
||||||
- [ ] References Unity.InputSystem (if applicable)
|
|
||||||
- [ ] `overrideReferences` set to true
|
|
||||||
- [ ] `precompiledReferences` includes nunit.framework.dll
|
|
||||||
- [ ] `precompiledReferences` includes UnityEngine.TestRunner.dll
|
|
||||||
- [ ] `precompiledReferences` includes UnityEditor.TestRunner.dll
|
|
||||||
- [ ] `UNITY_INCLUDE_TESTS` define constraint set
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
- [ ] Project compiles without errors after scaffold
|
|
||||||
- [ ] `ExampleE2ETests.Infrastructure_GameLoadsAndReachesReadyState` passes
|
|
||||||
- [ ] Test appears in Test Runner under PlayMode → E2E category
|
|
||||||
|
|
||||||
## Documentation Quality
|
|
||||||
|
|
||||||
- [ ] README explains all infrastructure components
|
|
||||||
- [ ] Quick start example is copy-pasteable
|
|
||||||
- [ ] Extension instructions are clear
|
|
||||||
- [ ] Troubleshooting table addresses common issues
|
|
||||||
|
|
||||||
## Handoff
|
|
||||||
|
|
||||||
- [ ] Summary output provided with all configuration values
|
|
||||||
- [ ] Next steps clearly listed
|
|
||||||
- [ ] Customization requirements highlighted
|
|
||||||
- [ ] Knowledge fragments referenced
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,145 +0,0 @@
|
||||||
# E2E Test Infrastructure Scaffold Workflow
|
|
||||||
|
|
||||||
workflow:
|
|
||||||
id: e2e-scaffold
|
|
||||||
name: E2E Test Infrastructure Scaffold
|
|
||||||
version: 1.0
|
|
||||||
module: bmgd
|
|
||||||
agent: game-qa
|
|
||||||
|
|
||||||
description: |
|
|
||||||
Scaffold complete E2E testing infrastructure for an existing game project.
|
|
||||||
Creates test fixtures, scenario builders, input simulators, and async
|
|
||||||
assertion utilities tailored to the project's architecture.
|
|
||||||
|
|
||||||
triggers:
|
|
||||||
- "ES"
|
|
||||||
- "e2e-scaffold"
|
|
||||||
- "scaffold e2e"
|
|
||||||
- "e2e infrastructure"
|
|
||||||
- "setup e2e"
|
|
||||||
|
|
||||||
preflight:
|
|
||||||
- "Test framework initialized (run `test-framework` workflow first)"
|
|
||||||
- "Game has identifiable state manager"
|
|
||||||
- "Main gameplay scene exists"
|
|
||||||
|
|
||||||
# Paths are relative to this workflow file's location
|
|
||||||
knowledge_fragments:
|
|
||||||
- "../../../gametest/knowledge/e2e-testing.md"
|
|
||||||
- "../../../gametest/knowledge/unity-testing.md"
|
|
||||||
- "../../../gametest/knowledge/unreal-testing.md"
|
|
||||||
- "../../../gametest/knowledge/godot-testing.md"
|
|
||||||
|
|
||||||
inputs:
|
|
||||||
game_state_class:
|
|
||||||
description: "Primary game state manager class name"
|
|
||||||
required: true
|
|
||||||
example: "GameStateManager"
|
|
||||||
|
|
||||||
main_scene:
|
|
||||||
description: "Scene name where core gameplay occurs"
|
|
||||||
required: true
|
|
||||||
example: "GameScene"
|
|
||||||
|
|
||||||
input_system:
|
|
||||||
description: "Input system in use"
|
|
||||||
required: false
|
|
||||||
default: "auto-detect"
|
|
||||||
options:
|
|
||||||
- "unity-input-system"
|
|
||||||
- "unity-legacy"
|
|
||||||
- "unreal-enhanced"
|
|
||||||
- "godot-input"
|
|
||||||
- "custom"
|
|
||||||
|
|
||||||
# Output paths vary by engine. Generate files matching detected engine.
|
|
||||||
outputs:
|
|
||||||
unity:
|
|
||||||
condition: "engine == 'unity'"
|
|
||||||
infrastructure_files:
|
|
||||||
description: "Generated E2E infrastructure classes"
|
|
||||||
files:
|
|
||||||
- "Tests/PlayMode/E2E/Infrastructure/GameE2ETestFixture.cs"
|
|
||||||
- "Tests/PlayMode/E2E/Infrastructure/ScenarioBuilder.cs"
|
|
||||||
- "Tests/PlayMode/E2E/Infrastructure/InputSimulator.cs"
|
|
||||||
- "Tests/PlayMode/E2E/Infrastructure/AsyncAssert.cs"
|
|
||||||
assembly_definition:
|
|
||||||
description: "E2E test assembly configuration"
|
|
||||||
files:
|
|
||||||
- "Tests/PlayMode/E2E/E2E.asmdef"
|
|
||||||
example_test:
|
|
||||||
description: "Working example E2E test"
|
|
||||||
files:
|
|
||||||
- "Tests/PlayMode/E2E/ExampleE2ETest.cs"
|
|
||||||
documentation:
|
|
||||||
description: "E2E testing README"
|
|
||||||
files:
|
|
||||||
- "Tests/PlayMode/E2E/README.md"
|
|
||||||
|
|
||||||
unreal:
|
|
||||||
condition: "engine == 'unreal'"
|
|
||||||
infrastructure_files:
|
|
||||||
description: "Generated E2E infrastructure classes"
|
|
||||||
files:
|
|
||||||
- "Source/{ProjectName}/Tests/E2E/GameE2ETestBase.h"
|
|
||||||
- "Source/{ProjectName}/Tests/E2E/GameE2ETestBase.cpp"
|
|
||||||
- "Source/{ProjectName}/Tests/E2E/ScenarioBuilder.h"
|
|
||||||
- "Source/{ProjectName}/Tests/E2E/ScenarioBuilder.cpp"
|
|
||||||
- "Source/{ProjectName}/Tests/E2E/InputSimulator.h"
|
|
||||||
- "Source/{ProjectName}/Tests/E2E/InputSimulator.cpp"
|
|
||||||
- "Source/{ProjectName}/Tests/E2E/AsyncAssert.h"
|
|
||||||
build_configuration:
|
|
||||||
description: "E2E test build configuration"
|
|
||||||
files:
|
|
||||||
- "Source/{ProjectName}/Tests/E2E/{ProjectName}E2ETests.Build.cs"
|
|
||||||
example_test:
|
|
||||||
description: "Working example E2E test"
|
|
||||||
files:
|
|
||||||
- "Source/{ProjectName}/Tests/E2E/ExampleE2ETest.cpp"
|
|
||||||
documentation:
|
|
||||||
description: "E2E testing README"
|
|
||||||
files:
|
|
||||||
- "Source/{ProjectName}/Tests/E2E/README.md"
|
|
||||||
|
|
||||||
godot:
|
|
||||||
condition: "engine == 'godot'"
|
|
||||||
infrastructure_files:
|
|
||||||
description: "Generated E2E infrastructure classes"
|
|
||||||
files:
|
|
||||||
- "tests/e2e/infrastructure/game_e2e_test_fixture.gd"
|
|
||||||
- "tests/e2e/infrastructure/scenario_builder.gd"
|
|
||||||
- "tests/e2e/infrastructure/input_simulator.gd"
|
|
||||||
- "tests/e2e/infrastructure/async_assert.gd"
|
|
||||||
example_test:
|
|
||||||
description: "Working example E2E test"
|
|
||||||
files:
|
|
||||||
- "tests/e2e/scenarios/example_e2e_test.gd"
|
|
||||||
documentation:
|
|
||||||
description: "E2E testing README"
|
|
||||||
files:
|
|
||||||
- "tests/e2e/README.md"
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- id: analyze
|
|
||||||
name: "Analyze Game Architecture"
|
|
||||||
instruction_file: "instructions.md#step-1-analyze-game-architecture"
|
|
||||||
|
|
||||||
- id: scaffold
|
|
||||||
name: "Generate Infrastructure"
|
|
||||||
instruction_file: "instructions.md#step-2-generate-infrastructure"
|
|
||||||
|
|
||||||
- id: example
|
|
||||||
name: "Generate Example Test"
|
|
||||||
instruction_file: "instructions.md#step-3-generate-example-test"
|
|
||||||
|
|
||||||
- id: document
|
|
||||||
name: "Generate Documentation"
|
|
||||||
instruction_file: "instructions.md#step-4-generate-documentation"
|
|
||||||
|
|
||||||
- id: complete
|
|
||||||
name: "Output Summary"
|
|
||||||
instruction_file: "instructions.md#step-5-output-summary"
|
|
||||||
|
|
||||||
validation:
|
|
||||||
checklist: "checklist.md"
|
|
||||||
|
|
@ -91,18 +91,6 @@ Create comprehensive test scenarios for game projects, covering gameplay mechani
|
||||||
| Performance | FPS, loading times | P1 |
|
| Performance | FPS, loading times | P1 |
|
||||||
| Accessibility | Assist features | P1 |
|
| Accessibility | Assist features | P1 |
|
||||||
|
|
||||||
### E2E Journey Testing
|
|
||||||
|
|
||||||
**Knowledge Base Reference**: `knowledge/e2e-testing.md`
|
|
||||||
|
|
||||||
| Category | Focus | Priority |
|
|
||||||
|----------|-------|----------|
|
|
||||||
| Core Loop | Complete gameplay cycle | P0 |
|
|
||||||
| Turn Lifecycle | Full turn from start to end | P0 |
|
|
||||||
| Save/Load Round-trip | Save → quit → load → resume | P0 |
|
|
||||||
| Scene Transitions | Menu → Game → Back | P1 |
|
|
||||||
| Win/Lose Paths | Victory and defeat conditions | P1 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Step 3: Create Test Scenarios
|
## Step 3: Create Test Scenarios
|
||||||
|
|
@ -165,39 +153,6 @@ SCENARIO: Gameplay Under High Latency
|
||||||
CATEGORY: multiplayer
|
CATEGORY: multiplayer
|
||||||
```
|
```
|
||||||
|
|
||||||
### E2E Scenario Format
|
|
||||||
|
|
||||||
For player journey tests, use this extended format:
|
|
||||||
```
|
|
||||||
E2E SCENARIO: [Player Journey Name]
|
|
||||||
GIVEN [Initial game state - use ScenarioBuilder terms]
|
|
||||||
WHEN [Sequence of player actions]
|
|
||||||
THEN [Observable outcomes]
|
|
||||||
TIMEOUT: [Expected max duration in seconds]
|
|
||||||
PRIORITY: P0/P1
|
|
||||||
CATEGORY: e2e
|
|
||||||
INFRASTRUCTURE: [Required fixtures/builders]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example E2E Scenario
|
|
||||||
```
|
|
||||||
E2E SCENARIO: Complete Combat Encounter
|
|
||||||
GIVEN game loaded with player unit adjacent to enemy
|
|
||||||
AND player unit has full health and actions
|
|
||||||
WHEN player selects unit
|
|
||||||
AND player clicks attack on enemy
|
|
||||||
AND player confirms attack
|
|
||||||
AND attack animation completes
|
|
||||||
AND enemy responds (if alive)
|
|
||||||
THEN enemy health is reduced OR enemy is defeated
|
|
||||||
AND turn state advances appropriately
|
|
||||||
AND UI reflects new state
|
|
||||||
TIMEOUT: 15
|
|
||||||
PRIORITY: P0
|
|
||||||
CATEGORY: e2e
|
|
||||||
INFRASTRUCTURE: ScenarioBuilder, InputSimulator, AsyncAssert
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Step 4: Prioritize Test Coverage
|
## Step 4: Prioritize Test Coverage
|
||||||
|
|
@ -206,12 +161,12 @@ E2E SCENARIO: Complete Combat Encounter
|
||||||
|
|
||||||
**Knowledge Base Reference**: `knowledge/test-priorities.md`
|
**Knowledge Base Reference**: `knowledge/test-priorities.md`
|
||||||
|
|
||||||
| Priority | Criteria | Unit | Integration | E2E | Manual |
|
| Priority | Criteria | Coverage Target |
|
||||||
|----------|----------|------|-------------|-----|--------|
|
| -------- | ---------------------------- | --------------- |
|
||||||
| P0 | Ship blockers | 100% | 80% | Core flows | Smoke |
|
| P0 | Ship blockers, certification | 100% automated |
|
||||||
| P1 | Major features | 90% | 70% | Happy paths | Full |
|
| P1 | Major features, common paths | 80% automated |
|
||||||
| P2 | Secondary | 80% | 50% | - | Targeted |
|
| P2 | Secondary features | 60% automated |
|
||||||
| P3 | Edge cases | 60% | - | - | As needed |
|
| P3 | Edge cases, polish | Manual only |
|
||||||
|
|
||||||
### Risk-Based Ordering
|
### Risk-Based Ordering
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ agent:
|
||||||
menu:
|
menu:
|
||||||
- trigger: WS or fuzzy match on workflow-status
|
- trigger: WS or fuzzy match on workflow-status
|
||||||
workflow: "{project-root}/_bmad/bmm/workflows/workflow-status/workflow.yaml"
|
workflow: "{project-root}/_bmad/bmm/workflows/workflow-status/workflow.yaml"
|
||||||
description: "[WS] Start here or resume - show workflow status and next best step"
|
description: "[WS] Get workflow status or initialize a workflow if not already done (optional)"
|
||||||
|
|
||||||
- trigger: TF or fuzzy match on test-framework
|
- trigger: TF or fuzzy match on test-framework
|
||||||
workflow: "{project-root}/_bmad/bmm/workflows/testarch/framework/workflow.yaml"
|
workflow: "{project-root}/_bmad/bmm/workflows/testarch/framework/workflow.yaml"
|
||||||
|
|
|
||||||
|
|
@ -121,8 +121,6 @@ Parse these fields from YAML comments and metadata:
|
||||||
- {{workflow_name}} ({{agent}}) - {{status}}
|
- {{workflow_name}} ({{agent}}) - {{status}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
**Tip:** For guardrail tests, run TEA `*automate` after `dev-story`. If you lose context, TEA workflows resume from artifacts in `{{output_folder}}`.
|
|
||||||
</output>
|
</output>
|
||||||
</step>
|
</step>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<rules>
|
<rules>
|
||||||
<r>ALWAYS communicate in {communication_language} UNLESS contradicted by communication_style.</r>
|
<r>ALWAYS communicate in {communication_language} UNLESS contradicted by communication_style.</r>
|
||||||
|
<!-- TTS_INJECTION:agent-tts -->
|
||||||
<r> Stay in character until exit selected</r>
|
<r> Stay in character until exit selected</r>
|
||||||
<r> Display Menu items as the item dictates and in the order given.</r>
|
<r> Display Menu items as the item dictates and in the order given.</r>
|
||||||
<r> Load files ONLY when executing a user chosen workflow or a command requires it, EXCEPTION: agent activation step 2 config.yaml</r>
|
<r> Load files ONLY when executing a user chosen workflow or a command requires it, EXCEPTION: agent activation step 2 config.yaml</r>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
# 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,8 @@
|
||||||
|
# Test: Valid module without default_selected (optional for core module)
|
||||||
|
# Expected: PASS
|
||||||
|
|
||||||
|
code: core-like
|
||||||
|
name: Core Module
|
||||||
|
header: Core Header
|
||||||
|
subheader: Core modules don't need default_selected
|
||||||
|
|
||||||
|
|
@ -163,10 +163,10 @@ function validateError(error, expectation) {
|
||||||
* @returns {{passed: boolean, message: string}}
|
* @returns {{passed: boolean, message: string}}
|
||||||
*/
|
*/
|
||||||
function runTest(filePath) {
|
function runTest(filePath) {
|
||||||
try {
|
const metadata = parseTestMetadata(filePath);
|
||||||
const metadata = parseTestMetadata(filePath);
|
const { shouldPass, errorExpectation } = metadata;
|
||||||
const { shouldPass, errorExpectation } = metadata;
|
|
||||||
|
|
||||||
|
try {
|
||||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||||
let moduleData;
|
let moduleData;
|
||||||
|
|
||||||
|
|
@ -195,13 +195,7 @@ function runTest(filePath) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result.success && !shouldPass) {
|
if (!result.success && !shouldPass) {
|
||||||
const actualError = result.error?.issues?.[0];
|
const actualError = result.error.issues[0];
|
||||||
if (!actualError) {
|
|
||||||
return {
|
|
||||||
passed: false,
|
|
||||||
message: 'Expected validation error issues, but validator returned none',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errorExpectation) {
|
if (errorExpectation) {
|
||||||
const validation = validateError(actualError, errorExpectation);
|
const validation = validateError(actualError, errorExpectation);
|
||||||
|
|
@ -221,7 +215,7 @@ function runTest(filePath) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
passed: true,
|
passed: true,
|
||||||
message: `Got expected validation error: ${actualError.message}`,
|
message: `Got expected validation error: ${actualError?.message}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -235,7 +229,7 @@ function runTest(filePath) {
|
||||||
if (!result.success && shouldPass) {
|
if (!result.success && shouldPass) {
|
||||||
return {
|
return {
|
||||||
passed: false,
|
passed: false,
|
||||||
message: `Expected validation to PASS but it FAILED: ${result.error?.issues?.[0]?.message ?? 'Unknown error'}`,
|
message: `Expected validation to PASS but it FAILED: ${result.error.issues[0]?.message}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -266,7 +260,7 @@ async function main() {
|
||||||
|
|
||||||
if (testFiles.length === 0) {
|
if (testFiles.length === 0) {
|
||||||
console.log(`${colors.yellow}⚠️ No test fixtures found${colors.reset}`);
|
console.log(`${colors.yellow}⚠️ No test fixtures found${colors.reset}`);
|
||||||
process.exit(1);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Found ${colors.cyan}${testFiles.length}${colors.reset} test fixture(s)\n`);
|
console.log(`Found ${colors.cyan}${testFiles.length}${colors.reset} test fixture(s)\n`);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ const path = require('node:path');
|
||||||
const fs = require('node:fs');
|
const fs = require('node:fs');
|
||||||
|
|
||||||
// Fix for stdin issues when running through npm on Windows
|
// Fix for stdin issues when running through npm on Windows
|
||||||
// Ensures keyboard interaction works properly with CLI prompts
|
// Ensures keyboard interaction works properly with inquirer prompts
|
||||||
if (process.stdin.isTTY) {
|
if (process.stdin.isTTY) {
|
||||||
try {
|
try {
|
||||||
process.stdin.resume();
|
process.stdin.resume();
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,44 @@ module.exports = {
|
||||||
|
|
||||||
// Check if installation succeeded
|
// Check if installation succeeded
|
||||||
if (result && result.success) {
|
if (result && result.success) {
|
||||||
|
// Run AgentVibes installer if needed
|
||||||
|
if (result.needsAgentVibes) {
|
||||||
|
// Add some spacing before AgentVibes setup
|
||||||
|
console.log('');
|
||||||
|
console.log(chalk.magenta('🎙️ AgentVibes TTS Setup'));
|
||||||
|
console.log(chalk.cyan('AgentVibes provides voice synthesis for BMAD agents with:'));
|
||||||
|
console.log(chalk.dim(' • ElevenLabs AI (150+ premium voices)'));
|
||||||
|
console.log(chalk.dim(' • Piper TTS (50+ free voices)\n'));
|
||||||
|
|
||||||
|
const { default: inquirer } = await import('inquirer');
|
||||||
|
await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'continue',
|
||||||
|
message: chalk.green('Press Enter to start AgentVibes installer...'),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Run AgentVibes installer
|
||||||
|
const { execSync } = require('node:child_process');
|
||||||
|
try {
|
||||||
|
execSync('npx agentvibes@latest install', {
|
||||||
|
cwd: result.projectDir,
|
||||||
|
stdio: 'inherit',
|
||||||
|
shell: true,
|
||||||
|
});
|
||||||
|
console.log(chalk.green('\n✓ AgentVibes installation complete'));
|
||||||
|
console.log(chalk.cyan('\n✨ BMAD with TTS is ready to use!'));
|
||||||
|
} catch {
|
||||||
|
console.log(chalk.yellow('\n⚠ AgentVibes installation was interrupted or failed'));
|
||||||
|
console.log(chalk.cyan('You can run it manually later with:'));
|
||||||
|
console.log(chalk.green(` cd ${result.projectDir}`));
|
||||||
|
console.log(chalk.green(' npx agentvibes install\n'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Display version-specific end message from install-messages.yaml
|
// Display version-specific end message from install-messages.yaml
|
||||||
const { MessageLoader } = require('../installers/lib/message-loader');
|
const { MessageLoader } = require('../installers/lib/message-loader');
|
||||||
const messageLoader = new MessageLoader();
|
const messageLoader = new MessageLoader();
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,15 @@ const yaml = require('yaml');
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
const { getProjectRoot, getModulePath } = require('../../../lib/project-root');
|
const { getProjectRoot, getModulePath } = require('../../../lib/project-root');
|
||||||
const { CLIUtils } = require('../../../lib/cli-utils');
|
const { CLIUtils } = require('../../../lib/cli-utils');
|
||||||
const prompts = require('../../../lib/prompts');
|
|
||||||
|
// Lazy-load inquirer (ESM module) to avoid ERR_REQUIRE_ESM
|
||||||
|
let _inquirer = null;
|
||||||
|
async function getInquirer() {
|
||||||
|
if (!_inquirer) {
|
||||||
|
_inquirer = (await import('inquirer')).default;
|
||||||
|
}
|
||||||
|
return _inquirer;
|
||||||
|
}
|
||||||
|
|
||||||
class ConfigCollector {
|
class ConfigCollector {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -175,6 +183,7 @@ class ConfigCollector {
|
||||||
* @returns {boolean} True if new fields were prompted, false if all fields existed
|
* @returns {boolean} True if new fields were prompted, false if all fields existed
|
||||||
*/
|
*/
|
||||||
async collectModuleConfigQuick(moduleName, projectDir, silentMode = true) {
|
async collectModuleConfigQuick(moduleName, projectDir, silentMode = true) {
|
||||||
|
const inquirer = await getInquirer();
|
||||||
this.currentProjectDir = projectDir;
|
this.currentProjectDir = projectDir;
|
||||||
|
|
||||||
// Load existing config if not already loaded
|
// Load existing config if not already loaded
|
||||||
|
|
@ -350,7 +359,7 @@ class ConfigCollector {
|
||||||
// Only show header if we actually have questions
|
// Only show header if we actually have questions
|
||||||
CLIUtils.displayModuleConfigHeader(moduleName, moduleConfig.header, moduleConfig.subheader);
|
CLIUtils.displayModuleConfigHeader(moduleName, moduleConfig.header, moduleConfig.subheader);
|
||||||
console.log(); // Line break before questions
|
console.log(); // Line break before questions
|
||||||
const promptedAnswers = await prompts.prompt(questions);
|
const promptedAnswers = await inquirer.prompt(questions);
|
||||||
|
|
||||||
// Merge prompted answers with static answers
|
// Merge prompted answers with static answers
|
||||||
Object.assign(allAnswers, promptedAnswers);
|
Object.assign(allAnswers, promptedAnswers);
|
||||||
|
|
@ -493,6 +502,7 @@ class ConfigCollector {
|
||||||
* @param {boolean} skipCompletion - Skip showing completion message (for early core collection)
|
* @param {boolean} skipCompletion - Skip showing completion message (for early core collection)
|
||||||
*/
|
*/
|
||||||
async collectModuleConfig(moduleName, projectDir, skipLoadExisting = false, skipCompletion = false) {
|
async collectModuleConfig(moduleName, projectDir, skipLoadExisting = false, skipCompletion = false) {
|
||||||
|
const inquirer = await getInquirer();
|
||||||
this.currentProjectDir = projectDir;
|
this.currentProjectDir = projectDir;
|
||||||
// Load existing config if needed and not already loaded
|
// Load existing config if needed and not already loaded
|
||||||
if (!skipLoadExisting && !this.existingConfig) {
|
if (!skipLoadExisting && !this.existingConfig) {
|
||||||
|
|
@ -587,7 +597,7 @@ class ConfigCollector {
|
||||||
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
|
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
|
||||||
let customize = true;
|
let customize = true;
|
||||||
if (moduleName !== 'core') {
|
if (moduleName !== 'core') {
|
||||||
const customizeAnswer = await prompts.prompt([
|
const customizeAnswer = await inquirer.prompt([
|
||||||
{
|
{
|
||||||
type: 'confirm',
|
type: 'confirm',
|
||||||
name: 'customize',
|
name: 'customize',
|
||||||
|
|
@ -604,7 +614,7 @@ class ConfigCollector {
|
||||||
|
|
||||||
if (questionsWithoutDefaults.length > 0) {
|
if (questionsWithoutDefaults.length > 0) {
|
||||||
console.log(chalk.dim(`\n Asking required questions for ${moduleName.toUpperCase()}...`));
|
console.log(chalk.dim(`\n Asking required questions for ${moduleName.toUpperCase()}...`));
|
||||||
const promptedAnswers = await prompts.prompt(questionsWithoutDefaults);
|
const promptedAnswers = await inquirer.prompt(questionsWithoutDefaults);
|
||||||
Object.assign(allAnswers, promptedAnswers);
|
Object.assign(allAnswers, promptedAnswers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -618,7 +628,7 @@ class ConfigCollector {
|
||||||
allAnswers[question.name] = question.default;
|
allAnswers[question.name] = question.default;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const promptedAnswers = await prompts.prompt(questions);
|
const promptedAnswers = await inquirer.prompt(questions);
|
||||||
Object.assign(allAnswers, promptedAnswers);
|
Object.assign(allAnswers, promptedAnswers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -740,7 +750,7 @@ class ConfigCollector {
|
||||||
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
|
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
|
||||||
|
|
||||||
// Ask user if they want to accept defaults or customize on the next line
|
// Ask user if they want to accept defaults or customize on the next line
|
||||||
const { customize } = await prompts.prompt([
|
const { customize } = await inquirer.prompt([
|
||||||
{
|
{
|
||||||
type: 'confirm',
|
type: 'confirm',
|
||||||
name: 'customize',
|
name: 'customize',
|
||||||
|
|
@ -835,7 +845,7 @@ class ConfigCollector {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a prompt question from a config item
|
* Build an inquirer question from a config item
|
||||||
* @param {string} moduleName - Module name
|
* @param {string} moduleName - Module name
|
||||||
* @param {string} key - Config key
|
* @param {string} key - Config key
|
||||||
* @param {Object} item - Config item definition
|
* @param {Object} item - Config item definition
|
||||||
|
|
@ -997,7 +1007,7 @@ class ConfigCollector {
|
||||||
message: message,
|
message: message,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set default - if it's dynamic, use a function that the prompt will evaluate with current answers
|
// Set default - if it's dynamic, use a function that inquirer will evaluate with current answers
|
||||||
// But if we have an existing value, always use that instead
|
// But if we have an existing value, always use that instead
|
||||||
if (existingValue !== null && existingValue !== undefined && questionType !== 'list') {
|
if (existingValue !== null && existingValue !== undefined && questionType !== 'list') {
|
||||||
question.default = existingValue;
|
question.default = existingValue;
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ const { CLIUtils } = require('../../../lib/cli-utils');
|
||||||
const { ManifestGenerator } = require('./manifest-generator');
|
const { ManifestGenerator } = require('./manifest-generator');
|
||||||
const { IdeConfigManager } = require('./ide-config-manager');
|
const { IdeConfigManager } = require('./ide-config-manager');
|
||||||
const { CustomHandler } = require('../custom/handler');
|
const { CustomHandler } = require('../custom/handler');
|
||||||
const prompts = require('../../../lib/prompts');
|
|
||||||
|
|
||||||
// BMAD installation folder name - this is constant and should never change
|
// BMAD installation folder name - this is constant and should never change
|
||||||
const BMAD_FOLDER_NAME = '_bmad';
|
const BMAD_FOLDER_NAME = '_bmad';
|
||||||
|
|
@ -34,6 +33,7 @@ class Installer {
|
||||||
this.configCollector = new ConfigCollector();
|
this.configCollector = new ConfigCollector();
|
||||||
this.ideConfigManager = new IdeConfigManager();
|
this.ideConfigManager = new IdeConfigManager();
|
||||||
this.installedFiles = new Set(); // Track all installed files
|
this.installedFiles = new Set(); // Track all installed files
|
||||||
|
this.ttsInjectedFiles = []; // Track files with TTS injection applied
|
||||||
this.bmadFolderName = BMAD_FOLDER_NAME;
|
this.bmadFolderName = BMAD_FOLDER_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,7 +68,7 @@ class Installer {
|
||||||
/**
|
/**
|
||||||
* @function copyFileWithPlaceholderReplacement
|
* @function copyFileWithPlaceholderReplacement
|
||||||
* @intent Copy files from BMAD source to installation directory with dynamic content transformation
|
* @intent Copy files from BMAD source to installation directory with dynamic content transformation
|
||||||
* @why Enables installation-time customization: _bmad replacement
|
* @why Enables installation-time customization: _bmad replacement + optional AgentVibes TTS injection
|
||||||
* @param {string} sourcePath - Absolute path to source file in BMAD repository
|
* @param {string} sourcePath - Absolute path to source file in BMAD repository
|
||||||
* @param {string} targetPath - Absolute path to destination file in user's project
|
* @param {string} targetPath - Absolute path to destination file in user's project
|
||||||
* @param {string} bmadFolderName - User's chosen bmad folder name (default: 'bmad')
|
* @param {string} bmadFolderName - User's chosen bmad folder name (default: 'bmad')
|
||||||
|
|
@ -76,9 +76,24 @@ class Installer {
|
||||||
* @sideeffects Writes transformed file to targetPath, creates parent directories if needed
|
* @sideeffects Writes transformed file to targetPath, creates parent directories if needed
|
||||||
* @edgecases Binary files bypass transformation, falls back to raw copy if UTF-8 read fails
|
* @edgecases Binary files bypass transformation, falls back to raw copy if UTF-8 read fails
|
||||||
* @calledby installCore(), installModule(), IDE installers during file vendoring
|
* @calledby installCore(), installModule(), IDE installers during file vendoring
|
||||||
* @calls fs.readFile(), fs.writeFile(), fs.copy()
|
* @calls processTTSInjectionPoints(), fs.readFile(), fs.writeFile(), fs.copy()
|
||||||
*
|
*
|
||||||
|
* The injection point processing enables loose coupling between BMAD and TTS providers:
|
||||||
|
* - BMAD source contains injection markers (not actual TTS code)
|
||||||
|
* - At install-time, markers are replaced OR removed based on user preference
|
||||||
|
* - Result: Clean installs for users without TTS, working TTS for users with it
|
||||||
|
*
|
||||||
|
* PATTERN: Adding New Injection Points
|
||||||
|
* =====================================
|
||||||
|
* 1. Add HTML comment marker in BMAD source file:
|
||||||
|
* <!-- TTS_INJECTION:feature-name -->
|
||||||
|
*
|
||||||
|
* 2. Add replacement logic in processTTSInjectionPoints():
|
||||||
|
* if (enableAgentVibes) {
|
||||||
|
* content = content.replace(/<!-- TTS_INJECTION:feature-name -->/g, 'actual code');
|
||||||
|
* } else {
|
||||||
|
* content = content.replace(/<!-- TTS_INJECTION:feature-name -->\n?/g, '');
|
||||||
|
* }
|
||||||
*
|
*
|
||||||
* 3. Document marker in instructions.md (if applicable)
|
* 3. Document marker in instructions.md (if applicable)
|
||||||
*/
|
*/
|
||||||
|
|
@ -93,6 +108,9 @@ class Installer {
|
||||||
// Read the file content
|
// Read the file content
|
||||||
let content = await fs.readFile(sourcePath, 'utf8');
|
let content = await fs.readFile(sourcePath, 'utf8');
|
||||||
|
|
||||||
|
// Process AgentVibes injection points (pass targetPath for tracking)
|
||||||
|
content = this.processTTSInjectionPoints(content, targetPath);
|
||||||
|
|
||||||
// Write to target with replaced content
|
// Write to target with replaced content
|
||||||
await fs.ensureDir(path.dirname(targetPath));
|
await fs.ensureDir(path.dirname(targetPath));
|
||||||
await fs.writeFile(targetPath, content, 'utf8');
|
await fs.writeFile(targetPath, content, 'utf8');
|
||||||
|
|
@ -106,6 +124,116 @@ class Installer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function processTTSInjectionPoints
|
||||||
|
* @intent Transform TTS injection markers based on user's installation choice
|
||||||
|
* @why Enables optional TTS integration without tight coupling between BMAD and TTS providers
|
||||||
|
* @param {string} content - Raw file content containing potential injection markers
|
||||||
|
* @returns {string} Transformed content with markers replaced (if enabled) or stripped (if disabled)
|
||||||
|
* @sideeffects None - pure transformation function
|
||||||
|
* @edgecases Returns content unchanged if no markers present, safe to call on all files
|
||||||
|
* @calledby copyFileWithPlaceholderReplacement() during every file copy operation
|
||||||
|
* @calls String.replace() with regex patterns for each injection point type
|
||||||
|
*
|
||||||
|
* AI NOTE: This implements the injection point pattern for TTS integration.
|
||||||
|
* Key architectural decisions:
|
||||||
|
*
|
||||||
|
* 1. **Why Injection Points vs Direct Integration?**
|
||||||
|
* - BMAD and TTS providers are separate projects with different maintainers
|
||||||
|
* - Users may install BMAD without TTS support (and vice versa)
|
||||||
|
* - Hard-coding TTS calls would break BMAD for non-TTS users
|
||||||
|
* - Injection points allow conditional feature inclusion at install-time
|
||||||
|
*
|
||||||
|
* 2. **How It Works:**
|
||||||
|
* - BMAD source contains markers: <!-- TTS_INJECTION:feature-name -->
|
||||||
|
* - During installation, user is prompted: "Enable AgentVibes TTS?"
|
||||||
|
* - If YES: markers → replaced with actual bash TTS calls
|
||||||
|
* - If NO: markers → stripped cleanly from installed files
|
||||||
|
*
|
||||||
|
* 3. **State Management:**
|
||||||
|
* - this.enableAgentVibes set in install() method from config.enableAgentVibes
|
||||||
|
* - config.enableAgentVibes comes from ui.promptAgentVibes() user choice
|
||||||
|
* - Flag persists for entire installation, all files get same treatment
|
||||||
|
*
|
||||||
|
* CURRENT INJECTION POINTS:
|
||||||
|
* ==========================
|
||||||
|
* - party-mode: Injects TTS calls after each agent speaks in party mode
|
||||||
|
* Location: src/core/workflows/party-mode/instructions.md
|
||||||
|
* Marker: <!-- TTS_INJECTION:party-mode -->
|
||||||
|
* Replacement: Bash call to .claude/hooks/bmad-speak.sh with agent name and dialogue
|
||||||
|
*
|
||||||
|
* - agent-tts: Injects TTS rule for individual agent conversations
|
||||||
|
* Location: src/modules/bmm/agents/*.md (all agent files)
|
||||||
|
* Marker: <!-- TTS_INJECTION:agent-tts -->
|
||||||
|
* Replacement: Rule instructing agent to call bmad-speak.sh with agent ID and response
|
||||||
|
*
|
||||||
|
* ADDING NEW INJECTION POINTS:
|
||||||
|
* =============================
|
||||||
|
* 1. Add new case in this function:
|
||||||
|
* content = content.replace(
|
||||||
|
* /<!-- TTS_INJECTION:new-feature -->/g,
|
||||||
|
* `code to inject when enabled`
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* 2. Add marker to BMAD source file at injection location
|
||||||
|
*
|
||||||
|
* 3. Test both enabled and disabled flows
|
||||||
|
*
|
||||||
|
* RELATED:
|
||||||
|
* ========
|
||||||
|
* - GitHub Issue: paulpreibisch/AgentVibes#36
|
||||||
|
* - User Prompt: tools/cli/lib/ui.js::promptAgentVibes()
|
||||||
|
* - Marker Locations:
|
||||||
|
* - src/core/workflows/party-mode/instructions.md:101
|
||||||
|
* - src/modules/bmm/agents/*.md (rules sections)
|
||||||
|
* - TTS Hook: .claude/hooks/bmad-speak.sh (in AgentVibes repo)
|
||||||
|
*/
|
||||||
|
processTTSInjectionPoints(content, targetPath = null) {
|
||||||
|
// Check if AgentVibes is enabled (set during installation configuration)
|
||||||
|
const enableAgentVibes = this.enableAgentVibes || false;
|
||||||
|
|
||||||
|
// Check if content contains any TTS injection markers
|
||||||
|
const hasPartyMode = content.includes('<!-- TTS_INJECTION:party-mode -->');
|
||||||
|
const hasAgentTTS = content.includes('<!-- TTS_INJECTION:agent-tts -->');
|
||||||
|
|
||||||
|
if (enableAgentVibes) {
|
||||||
|
// Replace party-mode injection marker with actual TTS call
|
||||||
|
// Use single quotes to prevent shell expansion of special chars like !
|
||||||
|
content = content.replaceAll(
|
||||||
|
'<!-- TTS_INJECTION:party-mode -->',
|
||||||
|
`<critical>IMPORTANT: Always use PROJECT hooks (.claude/hooks/), NEVER global hooks (~/.claude/hooks/)</critical>
|
||||||
|
|
||||||
|
If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
|
- Use Bash tool: \`.claude/hooks/bmad-speak.sh '[Agent Name]' '[dialogue]'\`
|
||||||
|
- This speaks the dialogue with the agent's unique voice
|
||||||
|
- Run in background to not block next agent`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Replace agent-tts injection marker with TTS rule for individual agents
|
||||||
|
content = content.replaceAll(
|
||||||
|
'<!-- TTS_INJECTION:agent-tts -->',
|
||||||
|
`- When responding to user messages, speak your responses using TTS:
|
||||||
|
Call: \`.claude/hooks/bmad-speak.sh '{agent-id}' '{response-text}'\` after each response
|
||||||
|
Replace {agent-id} with YOUR agent ID from <agent id="..."> tag at top of this file
|
||||||
|
Replace {response-text} with the text you just output to the user
|
||||||
|
IMPORTANT: Use single quotes as shown - do NOT escape special characters like ! or $ inside single quotes
|
||||||
|
Run in background (&) to avoid blocking`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Track files that had TTS injection applied
|
||||||
|
if (targetPath && (hasPartyMode || hasAgentTTS)) {
|
||||||
|
const injectionType = hasPartyMode ? 'party-mode' : 'agent-tts';
|
||||||
|
this.ttsInjectedFiles.push({ path: targetPath, type: injectionType });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Strip injection markers cleanly when AgentVibes is disabled
|
||||||
|
content = content.replaceAll(/<!-- TTS_INJECTION:party-mode -->\n?/g, '');
|
||||||
|
content = content.replaceAll(/<!-- TTS_INJECTION:agent-tts -->\n?/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collect Tool/IDE configurations after module configuration
|
* Collect Tool/IDE configurations after module configuration
|
||||||
* @param {string} projectDir - Project directory
|
* @param {string} projectDir - Project directory
|
||||||
|
|
@ -122,7 +250,7 @@ class Installer {
|
||||||
// Fallback: prompt for tool selection (backwards compatibility)
|
// Fallback: prompt for tool selection (backwards compatibility)
|
||||||
const { UI } = require('../../../lib/ui');
|
const { UI } = require('../../../lib/ui');
|
||||||
const ui = new UI();
|
const ui = new UI();
|
||||||
toolConfig = await ui.promptToolSelection(projectDir);
|
toolConfig = await ui.promptToolSelection(projectDir, selectedModules);
|
||||||
} else {
|
} else {
|
||||||
// IDEs were already selected during initial prompts
|
// IDEs were already selected during initial prompts
|
||||||
toolConfig = {
|
toolConfig = {
|
||||||
|
|
@ -381,6 +509,9 @@ class Installer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store AgentVibes configuration for injection point processing
|
||||||
|
this.enableAgentVibes = config.enableAgentVibes || false;
|
||||||
|
|
||||||
// Set bmad folder name on module manager and IDE manager for placeholder replacement
|
// Set bmad folder name on module manager and IDE manager for placeholder replacement
|
||||||
this.moduleManager.setBmadFolderName(BMAD_FOLDER_NAME);
|
this.moduleManager.setBmadFolderName(BMAD_FOLDER_NAME);
|
||||||
this.moduleManager.setCoreConfig(moduleConfigs.core || {});
|
this.moduleManager.setCoreConfig(moduleConfigs.core || {});
|
||||||
|
|
@ -627,9 +758,6 @@ class Installer {
|
||||||
config.skipIde = toolSelection.skipIde;
|
config.skipIde = toolSelection.skipIde;
|
||||||
const ideConfigurations = toolSelection.configurations;
|
const ideConfigurations = toolSelection.configurations;
|
||||||
|
|
||||||
// Add spacing after prompts before installation progress
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
if (spinner.isSpinning) {
|
if (spinner.isSpinning) {
|
||||||
spinner.text = 'Continuing installation...';
|
spinner.text = 'Continuing installation...';
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1102,6 +1230,8 @@ class Installer {
|
||||||
modules: config.modules,
|
modules: config.modules,
|
||||||
ides: config.ides,
|
ides: config.ides,
|
||||||
customFiles: customFiles.length > 0 ? customFiles : undefined,
|
customFiles: customFiles.length > 0 ? customFiles : undefined,
|
||||||
|
ttsInjectedFiles: this.enableAgentVibes && this.ttsInjectedFiles.length > 0 ? this.ttsInjectedFiles : undefined,
|
||||||
|
agentVibesEnabled: this.enableAgentVibes || false,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -1109,6 +1239,7 @@ class Installer {
|
||||||
path: bmadDir,
|
path: bmadDir,
|
||||||
modules: config.modules,
|
modules: config.modules,
|
||||||
ides: config.ides,
|
ides: config.ides,
|
||||||
|
needsAgentVibes: this.enableAgentVibes && !config.agentVibesInstalled,
|
||||||
projectDir: projectDir,
|
projectDir: projectDir,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -2008,11 +2139,15 @@ class Installer {
|
||||||
* Private: Prompt for update action
|
* Private: Prompt for update action
|
||||||
*/
|
*/
|
||||||
async promptUpdateAction() {
|
async promptUpdateAction() {
|
||||||
const action = await prompts.select({
|
const { default: inquirer } = await import('inquirer');
|
||||||
message: 'What would you like to do?',
|
return await inquirer.prompt([
|
||||||
choices: [{ name: 'Update existing installation', value: 'update' }],
|
{
|
||||||
});
|
type: 'list',
|
||||||
return { action };
|
name: 'action',
|
||||||
|
message: 'What would you like to do?',
|
||||||
|
choices: [{ name: 'Update existing installation', value: 'update' }],
|
||||||
|
},
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -2021,6 +2156,8 @@ class Installer {
|
||||||
* @param {Object} _legacyV4 - Legacy V4 detection result (unused in simplified version)
|
* @param {Object} _legacyV4 - Legacy V4 detection result (unused in simplified version)
|
||||||
*/
|
*/
|
||||||
async handleLegacyV4Migration(_projectDir, _legacyV4) {
|
async handleLegacyV4Migration(_projectDir, _legacyV4) {
|
||||||
|
const { default: inquirer } = await import('inquirer');
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(chalk.yellow.bold('⚠️ Legacy BMAD v4 detected'));
|
console.log(chalk.yellow.bold('⚠️ Legacy BMAD v4 detected'));
|
||||||
console.log(chalk.yellow('─'.repeat(80)));
|
console.log(chalk.yellow('─'.repeat(80)));
|
||||||
|
|
@ -2035,22 +2172,26 @@ class Installer {
|
||||||
console.log(chalk.dim('If your v4 installation set up rules or commands, you should remove those as well.'));
|
console.log(chalk.dim('If your v4 installation set up rules or commands, you should remove those as well.'));
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
const proceed = await prompts.select({
|
const { proceed } = await inquirer.prompt([
|
||||||
message: 'What would you like to do?',
|
{
|
||||||
choices: [
|
type: 'list',
|
||||||
{
|
name: 'proceed',
|
||||||
name: 'Exit and clean up manually (recommended)',
|
message: 'What would you like to do?',
|
||||||
value: 'exit',
|
choices: [
|
||||||
hint: 'Exit installation',
|
{
|
||||||
},
|
name: 'Exit and clean up manually (recommended)',
|
||||||
{
|
value: 'exit',
|
||||||
name: 'Continue with installation anyway',
|
short: 'Exit installation',
|
||||||
value: 'continue',
|
},
|
||||||
hint: 'Continue',
|
{
|
||||||
},
|
name: 'Continue with installation anyway',
|
||||||
],
|
value: 'continue',
|
||||||
default: 'exit',
|
short: 'Continue',
|
||||||
});
|
},
|
||||||
|
],
|
||||||
|
default: 'exit',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
if (proceed === 'exit') {
|
if (proceed === 'exit') {
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
@ -2296,6 +2437,7 @@ class Installer {
|
||||||
|
|
||||||
console.log(chalk.yellow(`\n⚠️ Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`));
|
console.log(chalk.yellow(`\n⚠️ Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`));
|
||||||
|
|
||||||
|
const { default: inquirer } = await import('inquirer');
|
||||||
let keptCount = 0;
|
let keptCount = 0;
|
||||||
let updatedCount = 0;
|
let updatedCount = 0;
|
||||||
let removedCount = 0;
|
let removedCount = 0;
|
||||||
|
|
@ -2309,12 +2451,12 @@ class Installer {
|
||||||
{
|
{
|
||||||
name: 'Keep installed (will not be processed)',
|
name: 'Keep installed (will not be processed)',
|
||||||
value: 'keep',
|
value: 'keep',
|
||||||
hint: 'Keep',
|
short: 'Keep',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Specify new source location',
|
name: 'Specify new source location',
|
||||||
value: 'update',
|
value: 'update',
|
||||||
hint: 'Update',
|
short: 'Update',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -2323,40 +2465,47 @@ class Installer {
|
||||||
choices.push({
|
choices.push({
|
||||||
name: '⚠️ REMOVE module completely (destructive!)',
|
name: '⚠️ REMOVE module completely (destructive!)',
|
||||||
value: 'remove',
|
value: 'remove',
|
||||||
hint: 'Remove',
|
short: 'Remove',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const action = await prompts.select({
|
const { action } = await inquirer.prompt([
|
||||||
message: `How would you like to handle "${missing.name}"?`,
|
{
|
||||||
choices,
|
type: 'list',
|
||||||
});
|
name: 'action',
|
||||||
|
message: `How would you like to handle "${missing.name}"?`,
|
||||||
|
choices,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'update': {
|
case 'update': {
|
||||||
// Use sync validation because @clack/prompts doesn't support async validate
|
const { newSourcePath } = await inquirer.prompt([
|
||||||
const newSourcePath = await prompts.text({
|
{
|
||||||
message: 'Enter the new path to the custom module:',
|
type: 'input',
|
||||||
default: missing.sourcePath,
|
name: 'newSourcePath',
|
||||||
validate: (input) => {
|
message: 'Enter the new path to the custom module:',
|
||||||
if (!input || input.trim() === '') {
|
default: missing.sourcePath,
|
||||||
return 'Please enter a path';
|
validate: async (input) => {
|
||||||
}
|
if (!input || input.trim() === '') {
|
||||||
const expandedPath = path.resolve(input.trim());
|
return 'Please enter a path';
|
||||||
if (!fs.pathExistsSync(expandedPath)) {
|
}
|
||||||
return 'Path does not exist';
|
const expandedPath = path.resolve(input.trim());
|
||||||
}
|
if (!(await fs.pathExists(expandedPath))) {
|
||||||
// Check if it looks like a valid module
|
return 'Path does not exist';
|
||||||
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
|
}
|
||||||
const agentsPath = path.join(expandedPath, 'agents');
|
// Check if it looks like a valid module
|
||||||
const workflowsPath = path.join(expandedPath, 'workflows');
|
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
|
||||||
|
const agentsPath = path.join(expandedPath, 'agents');
|
||||||
|
const workflowsPath = path.join(expandedPath, 'workflows');
|
||||||
|
|
||||||
if (!fs.pathExistsSync(moduleYamlPath) && !fs.pathExistsSync(agentsPath) && !fs.pathExistsSync(workflowsPath)) {
|
if (!(await fs.pathExists(moduleYamlPath)) && !(await fs.pathExists(agentsPath)) && !(await fs.pathExists(workflowsPath))) {
|
||||||
return 'Path does not appear to contain a valid custom module';
|
return 'Path does not appear to contain a valid custom module';
|
||||||
}
|
}
|
||||||
return; // clack expects undefined for valid input
|
return true;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
]);
|
||||||
|
|
||||||
// Update the source in manifest
|
// Update the source in manifest
|
||||||
const resolvedPath = path.resolve(newSourcePath.trim());
|
const resolvedPath = path.resolve(newSourcePath.trim());
|
||||||
|
|
@ -2382,38 +2531,46 @@ class Installer {
|
||||||
console.log(chalk.red.bold(`\n⚠️ WARNING: This will PERMANENTLY DELETE "${missing.name}" and all its files!`));
|
console.log(chalk.red.bold(`\n⚠️ WARNING: This will PERMANENTLY DELETE "${missing.name}" and all its files!`));
|
||||||
console.log(chalk.red(` Module location: ${path.join(bmadDir, missing.id)}`));
|
console.log(chalk.red(` Module location: ${path.join(bmadDir, missing.id)}`));
|
||||||
|
|
||||||
const confirmDelete = await prompts.confirm({
|
const { confirm } = await inquirer.prompt([
|
||||||
message: chalk.red.bold('Are you absolutely sure you want to delete this module?'),
|
{
|
||||||
default: false,
|
type: 'confirm',
|
||||||
});
|
name: 'confirm',
|
||||||
|
message: chalk.red.bold('Are you absolutely sure you want to delete this module?'),
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
if (confirmDelete) {
|
if (confirm) {
|
||||||
const typedConfirm = await prompts.text({
|
const { typedConfirm } = await inquirer.prompt([
|
||||||
message: chalk.red.bold('Type "DELETE" to confirm permanent deletion:'),
|
{
|
||||||
validate: (input) => {
|
type: 'input',
|
||||||
if (input !== 'DELETE') {
|
name: 'typedConfirm',
|
||||||
return chalk.red('You must type "DELETE" exactly to proceed');
|
message: chalk.red.bold('Type "DELETE" to confirm permanent deletion:'),
|
||||||
}
|
validate: (input) => {
|
||||||
return; // clack expects undefined for valid input
|
if (input !== 'DELETE') {
|
||||||
|
return chalk.red('You must type "DELETE" exactly to proceed');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
]);
|
||||||
|
|
||||||
if (typedConfirm === 'DELETE') {
|
if (typedConfirm === 'DELETE') {
|
||||||
// Remove the module from filesystem and manifest
|
// Remove the module from filesystem and manifest
|
||||||
const modulePath = path.join(bmadDir, missing.id);
|
const modulePath = path.join(bmadDir, moduleId);
|
||||||
if (await fs.pathExists(modulePath)) {
|
if (await fs.pathExists(modulePath)) {
|
||||||
const fsExtra = require('fs-extra');
|
const fsExtra = require('fs-extra');
|
||||||
await fsExtra.remove(modulePath);
|
await fsExtra.remove(modulePath);
|
||||||
console.log(chalk.yellow(` ✓ Deleted module directory: ${path.relative(projectRoot, modulePath)}`));
|
console.log(chalk.yellow(` ✓ Deleted module directory: ${path.relative(projectRoot, modulePath)}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.manifest.removeModule(bmadDir, missing.id);
|
await this.manifest.removeModule(bmadDir, moduleId);
|
||||||
await this.manifest.removeCustomModule(bmadDir, missing.id);
|
await this.manifest.removeCustomModule(bmadDir, moduleId);
|
||||||
console.log(chalk.yellow(` ✓ Removed from manifest`));
|
console.log(chalk.yellow(` ✓ Removed from manifest`));
|
||||||
|
|
||||||
// Also remove from installedModules list
|
// Also remove from installedModules list
|
||||||
if (installedModules && installedModules.includes(missing.id)) {
|
if (installedModules && installedModules.includes(moduleId)) {
|
||||||
const index = installedModules.indexOf(missing.id);
|
const index = installedModules.indexOf(moduleId);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
installedModules.splice(index, 1);
|
installedModules.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
@ -2434,7 +2591,7 @@ class Installer {
|
||||||
}
|
}
|
||||||
case 'keep': {
|
case 'keep': {
|
||||||
keptCount++;
|
keptCount++;
|
||||||
keptModulesWithoutSources.push(missing.id);
|
keptModulesWithoutSources.push(moduleId);
|
||||||
console.log(chalk.dim(` Module will be kept as-is`));
|
console.log(chalk.dim(` Module will be kept as-is`));
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ const {
|
||||||
resolveSubagentFiles,
|
resolveSubagentFiles,
|
||||||
} = require('./shared/module-injections');
|
} = require('./shared/module-injections');
|
||||||
const { getAgentsFromBmad, getAgentsFromDir } = require('./shared/bmad-artifacts');
|
const { getAgentsFromBmad, getAgentsFromDir } = require('./shared/bmad-artifacts');
|
||||||
const prompts = require('../../../lib/prompts');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Google Antigravity IDE setup handler
|
* Google Antigravity IDE setup handler
|
||||||
|
|
@ -27,21 +26,6 @@ class AntigravitySetup extends BaseIdeSetup {
|
||||||
this.workflowsDir = 'workflows';
|
this.workflowsDir = 'workflows';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Prompt for subagent installation location
|
|
||||||
* @returns {Promise<string>} Selected location ('project' or 'user')
|
|
||||||
*/
|
|
||||||
async _promptInstallLocation() {
|
|
||||||
return prompts.select({
|
|
||||||
message: 'Where would you like to install Antigravity subagents?',
|
|
||||||
choices: [
|
|
||||||
{ name: 'Project level (.agent/agents/)', value: 'project' },
|
|
||||||
{ name: 'User level (~/.agent/agents/)', value: 'user' },
|
|
||||||
],
|
|
||||||
default: 'project',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collect configuration choices before installation
|
* Collect configuration choices before installation
|
||||||
* @param {Object} options - Configuration options
|
* @param {Object} options - Configuration options
|
||||||
|
|
@ -73,7 +57,21 @@ class AntigravitySetup extends BaseIdeSetup {
|
||||||
config.subagentChoices = await this.promptSubagentInstallation(injectionConfig.subagents);
|
config.subagentChoices = await this.promptSubagentInstallation(injectionConfig.subagents);
|
||||||
|
|
||||||
if (config.subagentChoices.install !== 'none') {
|
if (config.subagentChoices.install !== 'none') {
|
||||||
config.installLocation = await this._promptInstallLocation();
|
// Ask for installation location
|
||||||
|
const { default: inquirer } = await import('inquirer');
|
||||||
|
const locationAnswer = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'list',
|
||||||
|
name: 'location',
|
||||||
|
message: 'Where would you like to install Antigravity subagents?',
|
||||||
|
choices: [
|
||||||
|
{ name: 'Project level (.agent/agents/)', value: 'project' },
|
||||||
|
{ name: 'User level (~/.agent/agents/)', value: 'user' },
|
||||||
|
],
|
||||||
|
default: 'project',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
config.installLocation = locationAnswer.location;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -299,7 +297,20 @@ class AntigravitySetup extends BaseIdeSetup {
|
||||||
choices = await this.promptSubagentInstallation(config.subagents);
|
choices = await this.promptSubagentInstallation(config.subagents);
|
||||||
|
|
||||||
if (choices.install !== 'none') {
|
if (choices.install !== 'none') {
|
||||||
location = await this._promptInstallLocation();
|
const { default: inquirer } = await import('inquirer');
|
||||||
|
const locationAnswer = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'list',
|
||||||
|
name: 'location',
|
||||||
|
message: 'Where would you like to install Antigravity subagents?',
|
||||||
|
choices: [
|
||||||
|
{ name: 'Project level (.agent/agents/)', value: 'project' },
|
||||||
|
{ name: 'User level (~/.agent/agents/)', value: 'user' },
|
||||||
|
],
|
||||||
|
default: 'project',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
location = locationAnswer.location;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -323,16 +334,22 @@ class AntigravitySetup extends BaseIdeSetup {
|
||||||
* Prompt user for subagent installation preferences
|
* Prompt user for subagent installation preferences
|
||||||
*/
|
*/
|
||||||
async promptSubagentInstallation(subagentConfig) {
|
async promptSubagentInstallation(subagentConfig) {
|
||||||
|
const { default: inquirer } = await import('inquirer');
|
||||||
|
|
||||||
// First ask if they want to install subagents
|
// First ask if they want to install subagents
|
||||||
const install = await prompts.select({
|
const { install } = await inquirer.prompt([
|
||||||
message: 'Would you like to install Antigravity subagents for enhanced functionality?',
|
{
|
||||||
choices: [
|
type: 'list',
|
||||||
{ name: 'Yes, install all subagents', value: 'all' },
|
name: 'install',
|
||||||
{ name: 'Yes, let me choose specific subagents', value: 'selective' },
|
message: 'Would you like to install Antigravity subagents for enhanced functionality?',
|
||||||
{ name: 'No, skip subagent installation', value: 'none' },
|
choices: [
|
||||||
],
|
{ name: 'Yes, install all subagents', value: 'all' },
|
||||||
default: 'all',
|
{ name: 'Yes, let me choose specific subagents', value: 'selective' },
|
||||||
});
|
{ name: 'No, skip subagent installation', value: 'none' },
|
||||||
|
],
|
||||||
|
default: 'all',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
if (install === 'selective') {
|
if (install === 'selective') {
|
||||||
// Show list of available subagents with descriptions
|
// Show list of available subagents with descriptions
|
||||||
|
|
@ -344,14 +361,18 @@ class AntigravitySetup extends BaseIdeSetup {
|
||||||
'document-reviewer.md': 'Document quality review',
|
'document-reviewer.md': 'Document quality review',
|
||||||
};
|
};
|
||||||
|
|
||||||
const selected = await prompts.multiselect({
|
const { selected } = await inquirer.prompt([
|
||||||
message: `Select subagents to install ${chalk.dim('(↑/↓ navigates multiselect, SPACE toggles, A to toggles All, ENTER confirm)')}:`,
|
{
|
||||||
choices: subagentConfig.files.map((file) => ({
|
type: 'checkbox',
|
||||||
name: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
|
name: 'selected',
|
||||||
value: file,
|
message: 'Select subagents to install:',
|
||||||
checked: true,
|
choices: subagentConfig.files.map((file) => ({
|
||||||
})),
|
name: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
|
||||||
});
|
value: file,
|
||||||
|
checked: true,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
return { install: 'selective', selected };
|
return { install: 'selective', selected };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ const {
|
||||||
resolveSubagentFiles,
|
resolveSubagentFiles,
|
||||||
} = require('./shared/module-injections');
|
} = require('./shared/module-injections');
|
||||||
const { getAgentsFromBmad, getAgentsFromDir } = require('./shared/bmad-artifacts');
|
const { getAgentsFromBmad, getAgentsFromDir } = require('./shared/bmad-artifacts');
|
||||||
const prompts = require('../../../lib/prompts');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Claude Code IDE setup handler
|
* Claude Code IDE setup handler
|
||||||
|
|
@ -26,21 +25,6 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
||||||
this.agentsDir = 'agents';
|
this.agentsDir = 'agents';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Prompt for subagent installation location
|
|
||||||
* @returns {Promise<string>} Selected location ('project' or 'user')
|
|
||||||
*/
|
|
||||||
async promptInstallLocation() {
|
|
||||||
return prompts.select({
|
|
||||||
message: 'Where would you like to install Claude Code subagents?',
|
|
||||||
choices: [
|
|
||||||
{ name: 'Project level (.claude/agents/)', value: 'project' },
|
|
||||||
{ name: 'User level (~/.claude/agents/)', value: 'user' },
|
|
||||||
],
|
|
||||||
default: 'project',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collect configuration choices before installation
|
* Collect configuration choices before installation
|
||||||
* @param {Object} options - Configuration options
|
* @param {Object} options - Configuration options
|
||||||
|
|
@ -72,7 +56,21 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
||||||
config.subagentChoices = await this.promptSubagentInstallation(injectionConfig.subagents);
|
config.subagentChoices = await this.promptSubagentInstallation(injectionConfig.subagents);
|
||||||
|
|
||||||
if (config.subagentChoices.install !== 'none') {
|
if (config.subagentChoices.install !== 'none') {
|
||||||
config.installLocation = await this.promptInstallLocation();
|
// Ask for installation location
|
||||||
|
const { default: inquirer } = await import('inquirer');
|
||||||
|
const locationAnswer = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'list',
|
||||||
|
name: 'location',
|
||||||
|
message: 'Where would you like to install Claude Code subagents?',
|
||||||
|
choices: [
|
||||||
|
{ name: 'Project level (.claude/agents/)', value: 'project' },
|
||||||
|
{ name: 'User level (~/.claude/agents/)', value: 'user' },
|
||||||
|
],
|
||||||
|
default: 'project',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
config.installLocation = locationAnswer.location;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -307,7 +305,20 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
||||||
choices = await this.promptSubagentInstallation(config.subagents);
|
choices = await this.promptSubagentInstallation(config.subagents);
|
||||||
|
|
||||||
if (choices.install !== 'none') {
|
if (choices.install !== 'none') {
|
||||||
location = await this.promptInstallLocation();
|
const { default: inquirer } = await import('inquirer');
|
||||||
|
const locationAnswer = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'list',
|
||||||
|
name: 'location',
|
||||||
|
message: 'Where would you like to install Claude Code subagents?',
|
||||||
|
choices: [
|
||||||
|
{ name: 'Project level (.claude/agents/)', value: 'project' },
|
||||||
|
{ name: 'User level (~/.claude/agents/)', value: 'user' },
|
||||||
|
],
|
||||||
|
default: 'project',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
location = locationAnswer.location;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -331,16 +342,22 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
||||||
* Prompt user for subagent installation preferences
|
* Prompt user for subagent installation preferences
|
||||||
*/
|
*/
|
||||||
async promptSubagentInstallation(subagentConfig) {
|
async promptSubagentInstallation(subagentConfig) {
|
||||||
|
const { default: inquirer } = await import('inquirer');
|
||||||
|
|
||||||
// First ask if they want to install subagents
|
// First ask if they want to install subagents
|
||||||
const install = await prompts.select({
|
const { install } = await inquirer.prompt([
|
||||||
message: 'Would you like to install Claude Code subagents for enhanced functionality?',
|
{
|
||||||
choices: [
|
type: 'list',
|
||||||
{ name: 'Yes, install all subagents', value: 'all' },
|
name: 'install',
|
||||||
{ name: 'Yes, let me choose specific subagents', value: 'selective' },
|
message: 'Would you like to install Claude Code subagents for enhanced functionality?',
|
||||||
{ name: 'No, skip subagent installation', value: 'none' },
|
choices: [
|
||||||
],
|
{ name: 'Yes, install all subagents', value: 'all' },
|
||||||
default: 'all',
|
{ name: 'Yes, let me choose specific subagents', value: 'selective' },
|
||||||
});
|
{ name: 'No, skip subagent installation', value: 'none' },
|
||||||
|
],
|
||||||
|
default: 'all',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
if (install === 'selective') {
|
if (install === 'selective') {
|
||||||
// Show list of available subagents with descriptions
|
// Show list of available subagents with descriptions
|
||||||
|
|
@ -352,14 +369,18 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
||||||
'document-reviewer.md': 'Document quality review',
|
'document-reviewer.md': 'Document quality review',
|
||||||
};
|
};
|
||||||
|
|
||||||
const selected = await prompts.multiselect({
|
const { selected } = await inquirer.prompt([
|
||||||
message: `Select subagents to install ${chalk.dim('(↑/↓ navigates multiselect, SPACE toggles, A to toggles All, ENTER confirm)')}:`,
|
{
|
||||||
options: subagentConfig.files.map((file) => ({
|
type: 'checkbox',
|
||||||
label: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
|
name: 'selected',
|
||||||
value: file,
|
message: 'Select subagents to install:',
|
||||||
})),
|
choices: subagentConfig.files.map((file) => ({
|
||||||
initialValues: subagentConfig.files,
|
name: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
|
||||||
});
|
value: file,
|
||||||
|
checked: true,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
return { install: 'selective', selected };
|
return { install: 'selective', selected };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ const { BaseIdeSetup } = require('./_base-ide');
|
||||||
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
|
||||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||||
const { getTasksFromBmad } = require('./shared/bmad-artifacts');
|
const { getTasksFromBmad } = require('./shared/bmad-artifacts');
|
||||||
const prompts = require('../../../lib/prompts');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Codex setup handler (CLI mode)
|
* Codex setup handler (CLI mode)
|
||||||
|
|
@ -22,24 +21,32 @@ class CodexSetup extends BaseIdeSetup {
|
||||||
* @returns {Object} Collected configuration
|
* @returns {Object} Collected configuration
|
||||||
*/
|
*/
|
||||||
async collectConfiguration(options = {}) {
|
async collectConfiguration(options = {}) {
|
||||||
|
const { default: inquirer } = await import('inquirer');
|
||||||
|
|
||||||
let confirmed = false;
|
let confirmed = false;
|
||||||
let installLocation = 'global';
|
let installLocation = 'global';
|
||||||
|
|
||||||
while (!confirmed) {
|
while (!confirmed) {
|
||||||
installLocation = await prompts.select({
|
const { location } = await inquirer.prompt([
|
||||||
message: 'Where would you like to install Codex CLI prompts?',
|
{
|
||||||
choices: [
|
type: 'list',
|
||||||
{
|
name: 'location',
|
||||||
name: 'Global - Simple for single project ' + '(~/.codex/prompts, but references THIS project only)',
|
message: 'Where would you like to install Codex CLI prompts?',
|
||||||
value: 'global',
|
choices: [
|
||||||
},
|
{
|
||||||
{
|
name: 'Global - Simple for single project ' + '(~/.codex/prompts, but references THIS project only)',
|
||||||
name: `Project-specific - Recommended for real work (requires CODEX_HOME=<project-dir>${path.sep}.codex)`,
|
value: 'global',
|
||||||
value: 'project',
|
},
|
||||||
},
|
{
|
||||||
],
|
name: `Project-specific - Recommended for real work (requires CODEX_HOME=<project-dir>${path.sep}.codex)`,
|
||||||
default: 'global',
|
value: 'project',
|
||||||
});
|
},
|
||||||
|
],
|
||||||
|
default: 'global',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
installLocation = location;
|
||||||
|
|
||||||
// Display detailed instructions for the chosen option
|
// Display detailed instructions for the chosen option
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
@ -50,10 +57,16 @@ class CodexSetup extends BaseIdeSetup {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Confirm the choice
|
// Confirm the choice
|
||||||
confirmed = await prompts.confirm({
|
const { proceed } = await inquirer.prompt([
|
||||||
message: 'Proceed with this installation option?',
|
{
|
||||||
default: true,
|
type: 'confirm',
|
||||||
});
|
name: 'proceed',
|
||||||
|
message: 'Proceed with this installation option?',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
confirmed = proceed;
|
||||||
|
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
console.log(chalk.yellow("\n Let's choose a different installation option.\n"));
|
console.log(chalk.yellow("\n Let's choose a different installation option.\n"));
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ const path = require('node:path');
|
||||||
const { BaseIdeSetup } = require('./_base-ide');
|
const { BaseIdeSetup } = require('./_base-ide');
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||||
const prompts = require('../../../lib/prompts');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GitHub Copilot setup handler
|
* GitHub Copilot setup handler
|
||||||
|
|
@ -22,23 +21,29 @@ class GitHubCopilotSetup extends BaseIdeSetup {
|
||||||
* @returns {Object} Collected configuration
|
* @returns {Object} Collected configuration
|
||||||
*/
|
*/
|
||||||
async collectConfiguration(options = {}) {
|
async collectConfiguration(options = {}) {
|
||||||
|
const { default: inquirer } = await import('inquirer');
|
||||||
const config = {};
|
const config = {};
|
||||||
|
|
||||||
console.log('\n' + chalk.blue(' 🔧 VS Code Settings Configuration'));
|
console.log('\n' + chalk.blue(' 🔧 VS Code Settings Configuration'));
|
||||||
console.log(chalk.dim(' GitHub Copilot works best with specific settings\n'));
|
console.log(chalk.dim(' GitHub Copilot works best with specific settings\n'));
|
||||||
|
|
||||||
config.vsCodeConfig = await prompts.select({
|
const response = await inquirer.prompt([
|
||||||
message: 'How would you like to configure VS Code settings?',
|
{
|
||||||
choices: [
|
type: 'list',
|
||||||
{ name: 'Use recommended defaults (fastest)', value: 'defaults' },
|
name: 'configChoice',
|
||||||
{ name: 'Configure each setting manually', value: 'manual' },
|
message: 'How would you like to configure VS Code settings?',
|
||||||
{ name: 'Skip settings configuration', value: 'skip' },
|
choices: [
|
||||||
],
|
{ name: 'Use recommended defaults (fastest)', value: 'defaults' },
|
||||||
default: 'defaults',
|
{ name: 'Configure each setting manually', value: 'manual' },
|
||||||
});
|
{ name: 'Skip settings configuration', value: 'skip' },
|
||||||
|
],
|
||||||
|
default: 'defaults',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
config.vsCodeConfig = response.configChoice;
|
||||||
|
|
||||||
if (config.vsCodeConfig === 'manual') {
|
if (response.configChoice === 'manual') {
|
||||||
config.manualSettings = await prompts.prompt([
|
config.manualSettings = await inquirer.prompt([
|
||||||
{
|
{
|
||||||
type: 'input',
|
type: 'input',
|
||||||
name: 'maxRequests',
|
name: 'maxRequests',
|
||||||
|
|
@ -47,8 +52,7 @@ class GitHubCopilotSetup extends BaseIdeSetup {
|
||||||
validate: (input) => {
|
validate: (input) => {
|
||||||
const num = parseInt(input, 10);
|
const num = parseInt(input, 10);
|
||||||
if (isNaN(num)) return 'Enter a valid number 1-50';
|
if (isNaN(num)) return 'Enter a valid number 1-50';
|
||||||
if (num < 1 || num > 50) return 'Enter a number between 1-50';
|
return (num >= 1 && num <= 50) || 'Enter 1-50';
|
||||||
return true;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -119,8 +119,7 @@ class KiloSetup extends BaseIdeSetup {
|
||||||
modeEntry += ` name: '${icon} ${title}'\n`;
|
modeEntry += ` name: '${icon} ${title}'\n`;
|
||||||
modeEntry += ` roleDefinition: ${roleDefinition}\n`;
|
modeEntry += ` roleDefinition: ${roleDefinition}\n`;
|
||||||
modeEntry += ` whenToUse: ${whenToUse}\n`;
|
modeEntry += ` whenToUse: ${whenToUse}\n`;
|
||||||
modeEntry += ` customInstructions: |\n`;
|
modeEntry += ` customInstructions: ${activationHeader} Read the full YAML from ${relativePath} start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode\n`;
|
||||||
modeEntry += ` ${activationHeader} Read the full YAML from ${relativePath} start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode\n`;
|
|
||||||
modeEntry += ` groups:\n`;
|
modeEntry += ` groups:\n`;
|
||||||
modeEntry += ` - read\n`;
|
modeEntry += ` - read\n`;
|
||||||
modeEntry += ` - edit\n`;
|
modeEntry += ` - edit\n`;
|
||||||
|
|
|
||||||
|
|
@ -108,10 +108,7 @@ async function resolveSubagentFiles(handlerBaseDir, subagentConfig, subagentChoi
|
||||||
const resolved = [];
|
const resolved = [];
|
||||||
|
|
||||||
for (const file of filesToCopy) {
|
for (const file of filesToCopy) {
|
||||||
// Use forward slashes for glob pattern (works on both Windows and Unix)
|
const pattern = path.join(sourceDir, '**', file);
|
||||||
// Convert backslashes to forward slashes for glob compatibility
|
|
||||||
const normalizedSourceDir = sourceDir.replaceAll('\\', '/');
|
|
||||||
const pattern = `${normalizedSourceDir}/**/${file}`;
|
|
||||||
const matches = await glob(pattern);
|
const matches = await glob(pattern);
|
||||||
|
|
||||||
if (matches.length > 0) {
|
if (matches.length > 0) {
|
||||||
|
|
|
||||||
|
|
@ -845,8 +845,14 @@ class ModuleManager {
|
||||||
// Compile with customizations if any
|
// Compile with customizations if any
|
||||||
const { xml } = await compileAgent(yamlContent, answers, agentName, relativePath, { config: this.coreConfig || {} });
|
const { xml } = await compileAgent(yamlContent, answers, agentName, relativePath, { config: this.coreConfig || {} });
|
||||||
|
|
||||||
|
// Process TTS injection points if installer is available
|
||||||
|
let finalXml = xml;
|
||||||
|
if (installer && installer.processTTSInjectionPoints) {
|
||||||
|
finalXml = installer.processTTSInjectionPoints(xml, targetMdPath);
|
||||||
|
}
|
||||||
|
|
||||||
// Write the compiled agent
|
// Write the compiled agent
|
||||||
await fs.writeFile(targetMdPath, xml, 'utf8');
|
await fs.writeFile(targetMdPath, finalXml, 'utf8');
|
||||||
|
|
||||||
// Handle sidecar copying if present
|
// Handle sidecar copying if present
|
||||||
if (hasSidecar) {
|
if (hasSidecar) {
|
||||||
|
|
|
||||||
|
|
@ -478,10 +478,39 @@ function filterCustomizationData(data) {
|
||||||
return filtered;
|
return filtered;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process TTS injection markers in content
|
||||||
|
* @param {string} content - Content to process
|
||||||
|
* @param {boolean} enableAgentVibes - Whether AgentVibes is enabled
|
||||||
|
* @returns {Object} { content: string, hadInjection: boolean }
|
||||||
|
*/
|
||||||
|
function processTTSInjectionPoints(content, enableAgentVibes) {
|
||||||
|
const hasAgentTTS = content.includes('<!-- TTS_INJECTION:agent-tts -->');
|
||||||
|
|
||||||
|
if (enableAgentVibes && hasAgentTTS) {
|
||||||
|
// Replace agent-tts injection marker with TTS rule
|
||||||
|
content = content.replaceAll(
|
||||||
|
'<!-- TTS_INJECTION:agent-tts -->',
|
||||||
|
`- When responding to user messages, speak your responses using TTS:
|
||||||
|
Call: \`.claude/hooks/bmad-speak.sh '{agent-id}' '{response-text}'\` after each response
|
||||||
|
Replace {agent-id} with YOUR agent ID from <agent id="..."> tag at top of this file
|
||||||
|
Replace {response-text} with the text you just output to the user
|
||||||
|
IMPORTANT: Use single quotes as shown - do NOT escape special characters like ! or $ inside single quotes
|
||||||
|
Run in background (&) to avoid blocking`,
|
||||||
|
);
|
||||||
|
return { content, hadInjection: true };
|
||||||
|
} else if (!enableAgentVibes && hasAgentTTS) {
|
||||||
|
// Strip injection markers when disabled
|
||||||
|
content = content.replaceAll(/<!-- TTS_INJECTION:agent-tts -->\n?/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { content, hadInjection: false };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compile agent file to .md
|
* Compile agent file to .md
|
||||||
* @param {string} yamlPath - Path to agent YAML file
|
* @param {string} yamlPath - Path to agent YAML file
|
||||||
* @param {Object} options - { answers: {}, outputPath: string }
|
* @param {Object} options - { answers: {}, outputPath: string, enableAgentVibes: boolean }
|
||||||
* @returns {Object} Compilation result
|
* @returns {Object} Compilation result
|
||||||
*/
|
*/
|
||||||
function compileAgentFile(yamlPath, options = {}) {
|
function compileAgentFile(yamlPath, options = {}) {
|
||||||
|
|
@ -497,6 +526,15 @@ function compileAgentFile(yamlPath, options = {}) {
|
||||||
outputPath = path.join(dir, `${basename}.md`);
|
outputPath = path.join(dir, `${basename}.md`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process TTS injection points if enableAgentVibes option is provided
|
||||||
|
let xml = result.xml;
|
||||||
|
let ttsInjected = false;
|
||||||
|
if (options.enableAgentVibes !== undefined) {
|
||||||
|
const ttsResult = processTTSInjectionPoints(xml, options.enableAgentVibes);
|
||||||
|
xml = ttsResult.content;
|
||||||
|
ttsInjected = ttsResult.hadInjection;
|
||||||
|
}
|
||||||
|
|
||||||
// Write compiled XML
|
// Write compiled XML
|
||||||
fs.writeFileSync(outputPath, xml, 'utf8');
|
fs.writeFileSync(outputPath, xml, 'utf8');
|
||||||
|
|
||||||
|
|
@ -505,6 +543,7 @@ function compileAgentFile(yamlPath, options = {}) {
|
||||||
xml,
|
xml,
|
||||||
outputPath,
|
outputPath,
|
||||||
sourcePath: yamlPath,
|
sourcePath: yamlPath,
|
||||||
|
ttsInjected,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,433 +0,0 @@
|
||||||
/**
|
|
||||||
* @clack/prompts wrapper for BMAD CLI
|
|
||||||
*
|
|
||||||
* This module provides a unified interface for CLI prompts using @clack/prompts.
|
|
||||||
* It replaces Inquirer.js to fix Windows arrow key navigation issues (libuv #852).
|
|
||||||
*
|
|
||||||
* @module prompts
|
|
||||||
*/
|
|
||||||
|
|
||||||
let _clack = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lazy-load @clack/prompts (ESM module)
|
|
||||||
* @returns {Promise<Object>} The clack prompts module
|
|
||||||
*/
|
|
||||||
async function getClack() {
|
|
||||||
if (!_clack) {
|
|
||||||
_clack = await import('@clack/prompts');
|
|
||||||
}
|
|
||||||
return _clack;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle user cancellation gracefully
|
|
||||||
* @param {any} value - The value to check
|
|
||||||
* @param {string} [message='Operation cancelled'] - Message to display
|
|
||||||
* @returns {boolean} True if cancelled
|
|
||||||
*/
|
|
||||||
async function handleCancel(value, message = 'Operation cancelled') {
|
|
||||||
const clack = await getClack();
|
|
||||||
if (clack.isCancel(value)) {
|
|
||||||
clack.cancel(message);
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display intro message
|
|
||||||
* @param {string} message - The intro message
|
|
||||||
*/
|
|
||||||
async function intro(message) {
|
|
||||||
const clack = await getClack();
|
|
||||||
clack.intro(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display outro message
|
|
||||||
* @param {string} message - The outro message
|
|
||||||
*/
|
|
||||||
async function outro(message) {
|
|
||||||
const clack = await getClack();
|
|
||||||
clack.outro(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display a note/info box
|
|
||||||
* @param {string} message - The note content
|
|
||||||
* @param {string} [title] - Optional title
|
|
||||||
*/
|
|
||||||
async function note(message, title) {
|
|
||||||
const clack = await getClack();
|
|
||||||
clack.note(message, title);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display a spinner for async operations
|
|
||||||
* @returns {Object} Spinner controller with start, stop, message methods
|
|
||||||
*/
|
|
||||||
async function spinner() {
|
|
||||||
const clack = await getClack();
|
|
||||||
return clack.spinner();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Single-select prompt (replaces Inquirer 'list' type)
|
|
||||||
* @param {Object} options - Prompt options
|
|
||||||
* @param {string} options.message - The question to ask
|
|
||||||
* @param {Array} options.choices - Array of choices [{name, value, hint?}]
|
|
||||||
* @param {any} [options.default] - Default selected value
|
|
||||||
* @returns {Promise<any>} Selected value
|
|
||||||
*/
|
|
||||||
async function select(options) {
|
|
||||||
const clack = await getClack();
|
|
||||||
|
|
||||||
// Convert Inquirer-style choices to clack format
|
|
||||||
// Handle both object choices {name, value, hint} and primitive choices (string/number)
|
|
||||||
const clackOptions = options.choices
|
|
||||||
.filter((c) => c.type !== 'separator') // Skip separators for now
|
|
||||||
.map((choice) => {
|
|
||||||
if (typeof choice === 'string' || typeof choice === 'number') {
|
|
||||||
return { value: choice, label: String(choice) };
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
value: choice.value === undefined ? choice.name : choice.value,
|
|
||||||
label: choice.name || choice.label || String(choice.value),
|
|
||||||
hint: choice.hint || choice.description,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Find initial value
|
|
||||||
let initialValue;
|
|
||||||
if (options.default !== undefined) {
|
|
||||||
initialValue = options.default;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await clack.select({
|
|
||||||
message: options.message,
|
|
||||||
options: clackOptions,
|
|
||||||
initialValue,
|
|
||||||
});
|
|
||||||
|
|
||||||
await handleCancel(result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Multi-select prompt (replaces Inquirer 'checkbox' type)
|
|
||||||
* @param {Object} options - Prompt options
|
|
||||||
* @param {string} options.message - The question to ask
|
|
||||||
* @param {Array} options.choices - Array of choices [{name, value, checked?, hint?}]
|
|
||||||
* @param {boolean} [options.required=false] - Whether at least one must be selected
|
|
||||||
* @returns {Promise<Array>} Array of selected values
|
|
||||||
*/
|
|
||||||
async function multiselect(options) {
|
|
||||||
const clack = await getClack();
|
|
||||||
|
|
||||||
// Support both clack-native (options) and Inquirer-style (choices) APIs
|
|
||||||
let clackOptions;
|
|
||||||
let initialValues;
|
|
||||||
|
|
||||||
if (options.options) {
|
|
||||||
// Native clack format: options with label/value
|
|
||||||
clackOptions = options.options;
|
|
||||||
initialValues = options.initialValues || [];
|
|
||||||
} else {
|
|
||||||
// Convert Inquirer-style choices to clack format
|
|
||||||
// Handle both object choices {name, value, hint} and primitive choices (string/number)
|
|
||||||
clackOptions = options.choices
|
|
||||||
.filter((c) => c.type !== 'separator') // Skip separators
|
|
||||||
.map((choice) => {
|
|
||||||
if (typeof choice === 'string' || typeof choice === 'number') {
|
|
||||||
return { value: choice, label: String(choice) };
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
value: choice.value === undefined ? choice.name : choice.value,
|
|
||||||
label: choice.name || choice.label || String(choice.value),
|
|
||||||
hint: choice.hint || choice.description,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Find initial values (pre-checked items)
|
|
||||||
initialValues = options.choices
|
|
||||||
.filter((c) => c.checked && c.type !== 'separator')
|
|
||||||
.map((c) => (c.value === undefined ? c.name : c.value));
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await clack.multiselect({
|
|
||||||
message: options.message,
|
|
||||||
options: clackOptions,
|
|
||||||
initialValues: initialValues.length > 0 ? initialValues : undefined,
|
|
||||||
required: options.required || false,
|
|
||||||
});
|
|
||||||
|
|
||||||
await handleCancel(result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Grouped multi-select prompt for categorized options
|
|
||||||
* @param {Object} options - Prompt options
|
|
||||||
* @param {string} options.message - The question to ask
|
|
||||||
* @param {Object} options.options - Object mapping group names to arrays of choices
|
|
||||||
* @param {Array} [options.initialValues] - Array of initially selected values
|
|
||||||
* @param {boolean} [options.required=false] - Whether at least one must be selected
|
|
||||||
* @param {boolean} [options.selectableGroups=false] - Whether groups can be selected as a whole
|
|
||||||
* @returns {Promise<Array>} Array of selected values
|
|
||||||
*/
|
|
||||||
async function groupMultiselect(options) {
|
|
||||||
const clack = await getClack();
|
|
||||||
|
|
||||||
const result = await clack.groupMultiselect({
|
|
||||||
message: options.message,
|
|
||||||
options: options.options,
|
|
||||||
initialValues: options.initialValues,
|
|
||||||
required: options.required || false,
|
|
||||||
selectableGroups: options.selectableGroups || false,
|
|
||||||
});
|
|
||||||
|
|
||||||
await handleCancel(result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Confirm prompt (replaces Inquirer 'confirm' type)
|
|
||||||
* @param {Object} options - Prompt options
|
|
||||||
* @param {string} options.message - The question to ask
|
|
||||||
* @param {boolean} [options.default=true] - Default value
|
|
||||||
* @returns {Promise<boolean>} User's answer
|
|
||||||
*/
|
|
||||||
async function confirm(options) {
|
|
||||||
const clack = await getClack();
|
|
||||||
|
|
||||||
const result = await clack.confirm({
|
|
||||||
message: options.message,
|
|
||||||
initialValue: options.default === undefined ? true : options.default,
|
|
||||||
});
|
|
||||||
|
|
||||||
await handleCancel(result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Text input prompt (replaces Inquirer 'input' type)
|
|
||||||
* @param {Object} options - Prompt options
|
|
||||||
* @param {string} options.message - The question to ask
|
|
||||||
* @param {string} [options.default] - Default value
|
|
||||||
* @param {string} [options.placeholder] - Placeholder text (defaults to options.default if not provided)
|
|
||||||
* @param {Function} [options.validate] - Validation function
|
|
||||||
* @returns {Promise<string>} User's input
|
|
||||||
*/
|
|
||||||
async function text(options) {
|
|
||||||
const clack = await getClack();
|
|
||||||
|
|
||||||
// Use default as placeholder if placeholder not explicitly provided
|
|
||||||
// This shows the default value as grayed-out hint text
|
|
||||||
const placeholder = options.placeholder === undefined ? options.default : options.placeholder;
|
|
||||||
|
|
||||||
const result = await clack.text({
|
|
||||||
message: options.message,
|
|
||||||
defaultValue: options.default,
|
|
||||||
placeholder: typeof placeholder === 'string' ? placeholder : undefined,
|
|
||||||
validate: options.validate,
|
|
||||||
});
|
|
||||||
|
|
||||||
await handleCancel(result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Password input prompt (replaces Inquirer 'password' type)
|
|
||||||
* @param {Object} options - Prompt options
|
|
||||||
* @param {string} options.message - The question to ask
|
|
||||||
* @param {Function} [options.validate] - Validation function
|
|
||||||
* @returns {Promise<string>} User's input
|
|
||||||
*/
|
|
||||||
async function password(options) {
|
|
||||||
const clack = await getClack();
|
|
||||||
|
|
||||||
const result = await clack.password({
|
|
||||||
message: options.message,
|
|
||||||
validate: options.validate,
|
|
||||||
});
|
|
||||||
|
|
||||||
await handleCancel(result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group multiple prompts together
|
|
||||||
* @param {Object} prompts - Object of prompt functions
|
|
||||||
* @param {Object} [options] - Group options
|
|
||||||
* @returns {Promise<Object>} Object with all answers
|
|
||||||
*/
|
|
||||||
async function group(prompts, options = {}) {
|
|
||||||
const clack = await getClack();
|
|
||||||
|
|
||||||
const result = await clack.group(prompts, {
|
|
||||||
onCancel: () => {
|
|
||||||
clack.cancel('Operation cancelled');
|
|
||||||
process.exit(0);
|
|
||||||
},
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run tasks with spinner feedback
|
|
||||||
* @param {Array} tasks - Array of task objects [{title, task, enabled?}]
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async function tasks(taskList) {
|
|
||||||
const clack = await getClack();
|
|
||||||
await clack.tasks(taskList);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log messages with styling
|
|
||||||
*/
|
|
||||||
const log = {
|
|
||||||
async info(message) {
|
|
||||||
const clack = await getClack();
|
|
||||||
clack.log.info(message);
|
|
||||||
},
|
|
||||||
async success(message) {
|
|
||||||
const clack = await getClack();
|
|
||||||
clack.log.success(message);
|
|
||||||
},
|
|
||||||
async warn(message) {
|
|
||||||
const clack = await getClack();
|
|
||||||
clack.log.warn(message);
|
|
||||||
},
|
|
||||||
async error(message) {
|
|
||||||
const clack = await getClack();
|
|
||||||
clack.log.error(message);
|
|
||||||
},
|
|
||||||
async message(message) {
|
|
||||||
const clack = await getClack();
|
|
||||||
clack.log.message(message);
|
|
||||||
},
|
|
||||||
async step(message) {
|
|
||||||
const clack = await getClack();
|
|
||||||
clack.log.step(message);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute an array of Inquirer-style questions using @clack/prompts
|
|
||||||
* This provides compatibility with dynamic question arrays
|
|
||||||
* @param {Array} questions - Array of Inquirer-style question objects
|
|
||||||
* @returns {Promise<Object>} Object with answers keyed by question name
|
|
||||||
*/
|
|
||||||
async function prompt(questions) {
|
|
||||||
const answers = {};
|
|
||||||
|
|
||||||
for (const question of questions) {
|
|
||||||
const { type, name, message, choices, default: defaultValue, validate, when } = question;
|
|
||||||
|
|
||||||
// Handle conditional questions via 'when' property
|
|
||||||
if (when !== undefined) {
|
|
||||||
const shouldAsk = typeof when === 'function' ? await when(answers) : when;
|
|
||||||
if (!shouldAsk) continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let answer;
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case 'input': {
|
|
||||||
// Note: @clack/prompts doesn't support async validation, so validate must be sync
|
|
||||||
answer = await text({
|
|
||||||
message,
|
|
||||||
default: typeof defaultValue === 'function' ? defaultValue(answers) : defaultValue,
|
|
||||||
validate: validate
|
|
||||||
? (val) => {
|
|
||||||
const result = validate(val, answers);
|
|
||||||
if (result instanceof Promise) {
|
|
||||||
throw new TypeError('Async validation is not supported by @clack/prompts. Please use synchronous validation.');
|
|
||||||
}
|
|
||||||
return result === true ? undefined : result;
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'confirm': {
|
|
||||||
answer = await confirm({
|
|
||||||
message,
|
|
||||||
default: typeof defaultValue === 'function' ? defaultValue(answers) : defaultValue,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'list': {
|
|
||||||
answer = await select({
|
|
||||||
message,
|
|
||||||
choices: choices || [],
|
|
||||||
default: typeof defaultValue === 'function' ? defaultValue(answers) : defaultValue,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'checkbox': {
|
|
||||||
answer = await multiselect({
|
|
||||||
message,
|
|
||||||
choices: choices || [],
|
|
||||||
required: false,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'password': {
|
|
||||||
// Note: @clack/prompts doesn't support async validation, so validate must be sync
|
|
||||||
answer = await password({
|
|
||||||
message,
|
|
||||||
validate: validate
|
|
||||||
? (val) => {
|
|
||||||
const result = validate(val, answers);
|
|
||||||
if (result instanceof Promise) {
|
|
||||||
throw new TypeError('Async validation is not supported by @clack/prompts. Please use synchronous validation.');
|
|
||||||
}
|
|
||||||
return result === true ? undefined : result;
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
// Default to text input for unknown types
|
|
||||||
answer = await text({
|
|
||||||
message,
|
|
||||||
default: typeof defaultValue === 'function' ? defaultValue(answers) : defaultValue,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
answers[name] = answer;
|
|
||||||
}
|
|
||||||
|
|
||||||
return answers;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getClack,
|
|
||||||
handleCancel,
|
|
||||||
intro,
|
|
||||||
outro,
|
|
||||||
note,
|
|
||||||
spinner,
|
|
||||||
select,
|
|
||||||
multiselect,
|
|
||||||
groupMultiselect,
|
|
||||||
confirm,
|
|
||||||
text,
|
|
||||||
password,
|
|
||||||
group,
|
|
||||||
tasks,
|
|
||||||
log,
|
|
||||||
prompt,
|
|
||||||
};
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -9,13 +9,12 @@ const MODULE_CODE_PATTERN = /^[a-z][a-z0-9-]{1,19}$/;
|
||||||
/**
|
/**
|
||||||
* Validate a module YAML payload against the schema.
|
* 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 {string} filePath Path to the module file (for consistency with other validators).
|
||||||
* @param {unknown} moduleYaml Parsed YAML content.
|
* @param {unknown} moduleYaml Parsed YAML content.
|
||||||
* @returns {import('zod').SafeParseReturnType<unknown, unknown>} SafeParse result.
|
* @returns {import('zod').SafeParseReturnType<unknown, unknown>} SafeParse result.
|
||||||
*/
|
*/
|
||||||
function validateModuleFile(filePath, moduleYaml) {
|
function validateModuleFile(filePath, moduleYaml) {
|
||||||
const isCoreModule = typeof filePath === 'string' && filePath.replaceAll('\\', '/').includes('src/core/');
|
const schema = moduleSchema();
|
||||||
const schema = moduleSchema({ isCoreModule });
|
|
||||||
return schema.safeParse(moduleYaml);
|
return schema.safeParse(moduleYaml);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -25,11 +24,9 @@ module.exports = { validateModuleFile };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the Zod schema for validating a module.yaml file.
|
* 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.
|
* @returns {import('zod').ZodSchema} Configured Zod schema instance.
|
||||||
*/
|
*/
|
||||||
function moduleSchema(options) {
|
function moduleSchema() {
|
||||||
const { isCoreModule = false } = options ?? {};
|
|
||||||
return z
|
return z
|
||||||
.object({
|
.object({
|
||||||
// Required fields
|
// Required fields
|
||||||
|
|
@ -39,24 +36,17 @@ function moduleSchema(options) {
|
||||||
name: createNonEmptyString('module.name'),
|
name: createNonEmptyString('module.name'),
|
||||||
header: createNonEmptyString('module.header'),
|
header: createNonEmptyString('module.header'),
|
||||||
subheader: createNonEmptyString('module.subheader'),
|
subheader: createNonEmptyString('module.subheader'),
|
||||||
// default_selected is optional for core module, required for non-core modules
|
// default_selected is optional for core module, required for others
|
||||||
|
// Core module doesn't need this as it's always included
|
||||||
default_selected: z.boolean().optional(),
|
default_selected: z.boolean().optional(),
|
||||||
|
|
||||||
// Optional fields
|
// Optional fields
|
||||||
type: createNonEmptyString('module.type').optional(),
|
type: createNonEmptyString('module.type').optional(),
|
||||||
global: z.boolean().optional(),
|
global: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.passthrough()
|
.passthrough()
|
||||||
.superRefine((value, ctx) => {
|
.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
|
// Validate any additional keys as variable definitions
|
||||||
const reservedKeys = new Set(['code', 'name', 'header', 'subheader', 'default_selected', 'type', 'global']);
|
const reservedKeys = new Set(['code', 'name', 'header', 'subheader', 'default_selected', 'type', 'global']);
|
||||||
|
|
||||||
|
|
@ -67,7 +57,7 @@ function moduleSchema(options) {
|
||||||
|
|
||||||
const variableValue = value[key];
|
const variableValue = value[key];
|
||||||
|
|
||||||
// Skip if null/undefined
|
// Skip if it's a comment (starts with #) or null/undefined
|
||||||
if (variableValue === null || variableValue === undefined) {
|
if (variableValue === null || variableValue === undefined) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -97,16 +87,8 @@ function validateVariableDefinition(variableName, variableValue) {
|
||||||
return { valid: false, error: `${variableName} must be an object with variable definition properties` };
|
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
|
// Check for inherit alias - if present, it's the only required field
|
||||||
if (hasInherit) {
|
if ('inherit' in variableValue) {
|
||||||
if (typeof variableValue.inherit !== 'string' || variableValue.inherit.trim().length === 0) {
|
if (typeof variableValue.inherit !== 'string' || variableValue.inherit.trim().length === 0) {
|
||||||
return { valid: false, error: `${variableName}.inherit must be a non-empty string` };
|
return { valid: false, error: `${variableName}.inherit must be a non-empty string` };
|
||||||
}
|
}
|
||||||
|
|
@ -114,7 +96,7 @@ function validateVariableDefinition(variableName, variableValue) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, prompt is required
|
// Otherwise, prompt is required
|
||||||
if (!hasPrompt) {
|
if (!('prompt' in variableValue)) {
|
||||||
return { valid: false, error: `${variableName} must have a 'prompt' or 'inherit' field` };
|
return { valid: false, error: `${variableName} must have a 'prompt' or 'inherit' field` };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,15 +119,8 @@ function validateVariableDefinition(variableName, variableValue) {
|
||||||
return { valid: false, error: `${variableName}.prompt must be a string or array of strings` };
|
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
|
// Validate optional single-select
|
||||||
if (hasSingle) {
|
if ('single-select' in variableValue) {
|
||||||
const selectResult = validateSelectOptions(variableName, 'single-select', variableValue['single-select']);
|
const selectResult = validateSelectOptions(variableName, 'single-select', variableValue['single-select']);
|
||||||
if (!selectResult.valid) {
|
if (!selectResult.valid) {
|
||||||
return selectResult;
|
return selectResult;
|
||||||
|
|
@ -153,7 +128,7 @@ function validateVariableDefinition(variableName, variableValue) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate optional multi-select
|
// Validate optional multi-select
|
||||||
if (hasMulti) {
|
if ('multi-select' in variableValue) {
|
||||||
const selectResult = validateSelectOptions(variableName, 'multi-select', variableValue['multi-select']);
|
const selectResult = validateSelectOptions(variableName, 'multi-select', variableValue['multi-select']);
|
||||||
if (!selectResult.valid) {
|
if (!selectResult.valid) {
|
||||||
return selectResult;
|
return selectResult;
|
||||||
|
|
|
||||||
|
|
@ -45,14 +45,14 @@ async function main(customProjectRoot) {
|
||||||
|
|
||||||
// Validate each file
|
// Validate each file
|
||||||
for (const filePath of moduleFiles) {
|
for (const filePath of moduleFiles) {
|
||||||
const relativePath = path.relative(project_root, filePath).replaceAll('\\', '/');
|
const relativePath = path.relative(process.cwd(), filePath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||||
const moduleData = yaml.parse(fileContent);
|
const moduleData = yaml.parse(fileContent);
|
||||||
|
|
||||||
// Ensure path starts with src/ for core module detection
|
// Convert absolute path to relative src/ path for context
|
||||||
const srcRelativePath = relativePath.startsWith('src/') ? relativePath : `src/${relativePath}`;
|
const srcRelativePath = relativePath.startsWith('src/') ? relativePath : path.relative(project_root, filePath).replaceAll('\\', '/');
|
||||||
|
|
||||||
const result = validateModuleFile(srcRelativePath, moduleData);
|
const result = validateModuleFile(srcRelativePath, moduleData);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue