Compare commits
6 Commits
033731244b
...
9285742cfe
| Author | SHA1 | Date |
|---|---|---|
|
|
9285742cfe | |
|
|
5881790068 | |
|
|
15574d94e9 | |
|
|
3b8e7d5dde | |
|
|
d83a88da66 | |
|
|
7b68d1a326 |
|
|
@ -10,6 +10,7 @@ permissions:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
bundle-and-publish:
|
bundle-and-publish:
|
||||||
|
if: ${{ false }} # Temporarily disabled while web bundles are paused.
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout BMAD-METHOD
|
- name: Checkout BMAD-METHOD
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ If you're using TEA Solo or don't have BMad artifacts:
|
||||||
|
|
||||||
**What are you testing?**
|
**What are you testing?**
|
||||||
```
|
```
|
||||||
TodoMVC React application at https://todomvc.com/examples/react/
|
TodoMVC React application at https://todomvc.com/examples/react/dist/
|
||||||
Features: Create todos, mark as complete, filter by status, delete todos
|
Features: Create todos, mark as complete, filter by status, delete todos
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -293,7 +293,7 @@ TEA workflows may use environment variables for test configuration.
|
||||||
**Playwright:**
|
**Playwright:**
|
||||||
```bash
|
```bash
|
||||||
# .env
|
# .env
|
||||||
BASE_URL=https://todomvc.com/examples/react/
|
BASE_URL=https://todomvc.com/examples/react/dist/
|
||||||
API_BASE_URL=https://api.example.com
|
API_BASE_URL=https://api.example.com
|
||||||
TEST_USER_EMAIL=test@example.com
|
TEST_USER_EMAIL=test@example.com
|
||||||
TEST_USER_PASSWORD=password123
|
TEST_USER_PASSWORD=password123
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ By the end of this 30-minute tutorial, you'll have:
|
||||||
|
|
||||||
- Node.js installed (v18 or later)
|
- Node.js installed (v18 or later)
|
||||||
- 30 minutes of focused time
|
- 30 minutes of focused time
|
||||||
- We'll use TodoMVC (<https://todomvc.com/examples/react/>) as our demo app
|
- We'll use TodoMVC (<https://todomvc.com/examples/react/dist/>) as our demo app
|
||||||
|
|
||||||
## TEA Approaches Explained
|
## TEA Approaches Explained
|
||||||
|
|
||||||
|
|
@ -36,7 +36,7 @@ This tutorial focuses on **TEA Lite** - the fastest way to see TEA in action.
|
||||||
|
|
||||||
We'll test TodoMVC, a standard demo app used across testing documentation.
|
We'll test TodoMVC, a standard demo app used across testing documentation.
|
||||||
|
|
||||||
**Demo App:** <https://todomvc.com/examples/react/>
|
**Demo App:** <https://todomvc.com/examples/react/dist/>
|
||||||
|
|
||||||
No installation needed - TodoMVC runs in your browser. Open the link above and:
|
No installation needed - TodoMVC runs in your browser. Open the link above and:
|
||||||
1. Add a few todos (type and press Enter)
|
1. Add a few todos (type and press Enter)
|
||||||
|
|
@ -171,7 +171,7 @@ In your chat with TEA, run:
|
||||||
```
|
```
|
||||||
|
|
||||||
**Q: What are you testing?**
|
**Q: What are you testing?**
|
||||||
A: "TodoMVC React app at <https://todomvc.com/examples/react/> - focus on the test design we just created"
|
A: "TodoMVC React app at <https://todomvc.com/examples/react/dist/> - focus on the test design we just created"
|
||||||
|
|
||||||
**Q: Reference existing docs?**
|
**Q: Reference existing docs?**
|
||||||
A: "Yes, use test-design-epic-1.md"
|
A: "Yes, use test-design-epic-1.md"
|
||||||
|
|
@ -187,7 +187,7 @@ import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
test.describe('TodoMVC - Core Functionality', () => {
|
test.describe('TodoMVC - Core Functionality', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('https://todomvc.com/examples/react/');
|
await page.goto('https://todomvc.com/examples/react/dist/');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should create a new todo', async ({ page }) => {
|
test('should create a new todo', async ({ page }) => {
|
||||||
|
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
const { Command } = require('commander');
|
|
||||||
const { WebBundler } = require('./web-bundler');
|
|
||||||
|
|
||||||
const program = new Command();
|
|
||||||
const bundler = new WebBundler();
|
|
||||||
|
|
||||||
program.name('bundle-web').description('Generate BMAD web bundles').version('1.0.0');
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('list')
|
|
||||||
.description('List available modules and agents')
|
|
||||||
.action(async () => {
|
|
||||||
await bundler.list();
|
|
||||||
});
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('clean')
|
|
||||||
.description('Remove all generated web bundles')
|
|
||||||
.action(async () => {
|
|
||||||
await bundler.clean();
|
|
||||||
});
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('all')
|
|
||||||
.description('Bundle all modules')
|
|
||||||
.action(async () => {
|
|
||||||
await bundler.bundleAll();
|
|
||||||
});
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('rebundle')
|
|
||||||
.description('Clean and bundle all modules')
|
|
||||||
.action(async () => {
|
|
||||||
await bundler.clean();
|
|
||||||
await bundler.bundleAll();
|
|
||||||
});
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('module')
|
|
||||||
.description('Bundle a specific module')
|
|
||||||
.argument('<name>', 'module name')
|
|
||||||
.action(async (name) => {
|
|
||||||
await bundler.bundleModule(name);
|
|
||||||
});
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('agent')
|
|
||||||
.description('Bundle a specific agent')
|
|
||||||
.argument('<module>', 'module name')
|
|
||||||
.argument('<agent>', 'agent name')
|
|
||||||
.action(async (moduleName, agentName) => {
|
|
||||||
await bundler.bundleAgentByName(moduleName, agentName);
|
|
||||||
});
|
|
||||||
|
|
||||||
program
|
|
||||||
.command('team')
|
|
||||||
.description('Bundle a specific team (not currently implemented)')
|
|
||||||
.argument('<module>', 'module name')
|
|
||||||
.argument('<team>', 'team name')
|
|
||||||
.action(async (moduleName, teamName) => {
|
|
||||||
throw new Error(`Team bundling is not implemented: ${moduleName}/${teamName}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
program.parseAsync(process.argv).catch((error) => {
|
|
||||||
console.error(error.message || error);
|
|
||||||
process.exitCode = 1;
|
|
||||||
});
|
|
||||||
|
|
@ -1,156 +0,0 @@
|
||||||
const path = require('node:path');
|
|
||||||
const fs = require('fs-extra');
|
|
||||||
const chalk = require('chalk');
|
|
||||||
const { XmlHandler } = require('../lib/xml-handler');
|
|
||||||
|
|
||||||
class WebBundler {
|
|
||||||
constructor(options = {}) {
|
|
||||||
this.projectRoot = options.projectRoot || path.resolve(__dirname, '../../..');
|
|
||||||
this.modulesRoot = options.modulesRoot || path.join(this.projectRoot, 'src', 'modules');
|
|
||||||
this.outputRoot = options.outputRoot || path.join(this.projectRoot, 'web-bundles');
|
|
||||||
this.logger = options.logger || console;
|
|
||||||
this.xmlHandler = new XmlHandler();
|
|
||||||
}
|
|
||||||
|
|
||||||
async listModules() {
|
|
||||||
if (!(await fs.pathExists(this.modulesRoot))) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const entries = await fs.readdir(this.modulesRoot, { withFileTypes: true });
|
|
||||||
return entries
|
|
||||||
.filter((entry) => entry.isDirectory())
|
|
||||||
.map((entry) => entry.name)
|
|
||||||
.sort();
|
|
||||||
}
|
|
||||||
|
|
||||||
async listAgents(moduleName) {
|
|
||||||
const agentsDir = path.join(this.modulesRoot, moduleName, 'agents');
|
|
||||||
if (!(await fs.pathExists(agentsDir))) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const files = await this.findAgentFiles(agentsDir);
|
|
||||||
return files.map((file) => path.basename(file, '.agent.yaml')).sort();
|
|
||||||
}
|
|
||||||
|
|
||||||
async list() {
|
|
||||||
const modules = await this.listModules();
|
|
||||||
|
|
||||||
if (modules.length === 0) {
|
|
||||||
this.logger.log('No modules found.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(chalk.cyan('Available modules and agents:'));
|
|
||||||
for (const moduleName of modules) {
|
|
||||||
const agents = await this.listAgents(moduleName);
|
|
||||||
this.logger.log(` ${moduleName}: ${agents.length} agent(s)`);
|
|
||||||
for (const agent of agents) {
|
|
||||||
this.logger.log(` - ${agent}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async clean() {
|
|
||||||
if (await fs.pathExists(this.outputRoot)) {
|
|
||||||
await fs.remove(this.outputRoot);
|
|
||||||
}
|
|
||||||
this.logger.log(chalk.green('OK: Cleaned web-bundles output'));
|
|
||||||
}
|
|
||||||
|
|
||||||
async bundleAll() {
|
|
||||||
const modules = await this.listModules();
|
|
||||||
if (modules.length === 0) {
|
|
||||||
this.logger.log('No modules found to bundle.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.ensureDir(this.outputRoot);
|
|
||||||
for (const moduleName of modules) {
|
|
||||||
await this.bundleModule(moduleName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async bundleModule(moduleName) {
|
|
||||||
const moduleRoot = path.join(this.modulesRoot, moduleName);
|
|
||||||
if (!(await fs.pathExists(moduleRoot))) {
|
|
||||||
throw new Error(`Module not found: ${moduleName}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const agentsDir = path.join(moduleRoot, 'agents');
|
|
||||||
if (!(await fs.pathExists(agentsDir))) {
|
|
||||||
this.logger.log(chalk.yellow(`Skipping ${moduleName}: no agents directory`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const outputModuleDir = path.join(this.outputRoot, moduleName, 'agents');
|
|
||||||
await fs.remove(outputModuleDir);
|
|
||||||
await fs.ensureDir(outputModuleDir);
|
|
||||||
|
|
||||||
const agentFiles = await this.findAgentFiles(agentsDir);
|
|
||||||
if (agentFiles.length === 0) {
|
|
||||||
this.logger.log(chalk.yellow(`Skipping ${moduleName}: no agent files found`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.log(chalk.cyan(`Bundling ${moduleName} (${agentFiles.length} agent(s))`));
|
|
||||||
for (const agentFile of agentFiles) {
|
|
||||||
await this.bundleAgentFile(moduleName, agentFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async bundleAgentByName(moduleName, agentName) {
|
|
||||||
const agentsDir = path.join(this.modulesRoot, moduleName, 'agents');
|
|
||||||
if (!(await fs.pathExists(agentsDir))) {
|
|
||||||
throw new Error(`Agents directory not found for module: ${moduleName}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const agentFiles = await this.findAgentFiles(agentsDir);
|
|
||||||
const match = agentFiles.find((file) => path.basename(file, '.agent.yaml') === agentName);
|
|
||||||
if (!match) {
|
|
||||||
throw new Error(`Agent not found: ${moduleName}/${agentName}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const outputModuleDir = path.join(this.outputRoot, moduleName, 'agents');
|
|
||||||
await fs.ensureDir(outputModuleDir);
|
|
||||||
await this.bundleAgentFile(moduleName, match);
|
|
||||||
}
|
|
||||||
|
|
||||||
async bundleAgentFile(moduleName, agentFile) {
|
|
||||||
const agentName = path.basename(agentFile, '.agent.yaml');
|
|
||||||
const outputFile = path.join(this.outputRoot, moduleName, 'agents', `${agentName}.xml`);
|
|
||||||
|
|
||||||
const bundled = await this.xmlHandler.buildFromYaml(agentFile, null, { forWebBundle: true });
|
|
||||||
const xml = this.extractXmlBlock(bundled);
|
|
||||||
|
|
||||||
await fs.writeFile(outputFile, xml, 'utf8');
|
|
||||||
this.logger.log(chalk.green(` OK: ${moduleName}/${agentName}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
async findAgentFiles(rootDir) {
|
|
||||||
const entries = await fs.readdir(rootDir, { withFileTypes: true });
|
|
||||||
const files = [];
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
const fullPath = path.join(rootDir, entry.name);
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
files.push(...(await this.findAgentFiles(fullPath)));
|
|
||||||
} else if (entry.isFile() && entry.name.endsWith('.agent.yaml')) {
|
|
||||||
files.push(fullPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return files;
|
|
||||||
}
|
|
||||||
|
|
||||||
extractXmlBlock(content) {
|
|
||||||
const match = content.match(/```xml\s*([\s\S]*?)```/);
|
|
||||||
if (!match) {
|
|
||||||
return content.trim() + '\n';
|
|
||||||
}
|
|
||||||
return match[1].trim() + '\n';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { WebBundler };
|
|
||||||
Loading…
Reference in New Issue