Compare commits

..

1 Commits

Author SHA1 Message Date
Murat K Ozcan cd13dff8cb
Merge 638892289a into eeebf152af 2026-01-13 13:39:24 -06:00
33 changed files with 3159 additions and 2033 deletions

View File

@ -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.

View File

@ -60,8 +60,8 @@ If you are unsure, default to the integrated path for your track and adjust late
| `*framework` | Playwright/Cypress scaffold, `.env.example`, `.nvmrc`, sample specs | Use when no production-ready harness exists | - |
| `*ci` | CI workflow, selective test scripts, secrets checklist | Platform-aware (GitHub Actions default) | - |
| `*test-design` | Combined risk assessment, mitigation plan, and coverage strategy | Risk scoring + optional exploratory mode | **+ Exploratory**: Interactive UI discovery with browser automation (uncover actual functionality) |
| `*atdd` | Failing acceptance tests + implementation checklist | TDD red phase + optional recording mode | **+ Recording**: UI selectors verified with live browser; API tests benefit from trace analysis |
| `*automate` | Prioritized specs, fixtures, README/script updates, DoD summary | Optional healing/recording, avoid duplicate coverage | **+ Healing**: Visual debugging + trace analysis for test fixes; **+ Recording**: Verified selectors (UI) + network inspection (API) |
| `*atdd` | Failing acceptance tests + implementation checklist | TDD red phase + optional recording mode | **+ Recording**: AI generation verified with live browser (accurate selectors from real DOM) |
| `*automate` | Prioritized specs, fixtures, README/script updates, DoD summary | Optional healing/recording, avoid duplicate coverage | **+ Healing**: Pattern fixes enhanced with visual debugging + **+ Recording**: AI verified with live browser |
| `*test-review` | Test quality review report with 0-100 score, violations, fixes | Reviews tests against knowledge base patterns | - |
| `*nfr-assess` | NFR assessment report with actions | Focus on security/performance/reliability | - |
| `*trace` | Phase 1: Coverage matrix, recommendations. Phase 2: Gate decision (PASS/CONCERNS/FAIL/WAIVED) | Two-phase workflow: traceability + gate decision | - |
@ -308,7 +308,7 @@ Want to understand TEA principles and patterns in depth?
- [Engagement Models](/docs/explanation/tea/engagement-models.md) - TEA Lite, TEA Solo, TEA Integrated (5 models explained)
**Philosophy:**
- [Testing as Engineering](/docs/explanation/philosophy/testing-as-engineering.md) - **Start here to understand WHY TEA exists** - The problem with AI-generated tests and TEA's three-part solution
- [Testing as Engineering](/docs/explanation/philosophy/testing-as-engineering.md) - Why TEA exists, problem statement
## Optional Integrations

View File

@ -594,7 +594,7 @@ Client project 3 (Ad-hoc):
**When:** Adopt BMad Method, want full integration.
**Steps:**
1. Install BMad Method (see installation guide)
1. Install BMad Method (`npx bmad-method@alpha install`)
2. Run planning workflows (PRD, architecture)
3. Integrate TEA into Phase 3 (system-level test design)
4. Follow integrated lifecycle (per epic workflows)
@ -690,7 +690,7 @@ Each model uses different TEA workflows. See:
**Use-Case Guides:**
- [Using TEA with Existing Tests](/docs/how-to/brownfield/use-tea-with-existing-tests.md) - Model 5: Brownfield
- [Running TEA for Enterprise](/docs/how-to/enterprise/use-tea-for-enterprise.md) - Enterprise integration
- [Running TEA for Enterprise](/docs/how-to/workflows/run-tea-for-enterprise.md) - Enterprise integration
**All Workflow Guides:**
- [How to Run Test Design](/docs/how-to/workflows/run-test-design.md) - Used in TEA Solo and Integrated

View File

@ -220,8 +220,8 @@ test('should update profile', async ({ apiRequest, authToken, log }) => {
// Use API request fixture (matches pure function signature)
const { status, body } = await apiRequest({
method: 'PATCH',
url: '/api/profile',
data: { name: 'New Name' },
url: '/api/profile', // Pure function uses 'url' (not 'path')
data: { name: 'New Name' }, // Pure function uses 'data' (not 'body')
headers: { Authorization: `Bearer ${authToken}` }
});

View File

@ -484,31 +484,22 @@ await page.waitForSelector('.success', { timeout: 30000 });
All developers:
```typescript
import { test } from '@seontechnologies/playwright-utils/fixtures';
import { test } from '@seontechnologies/playwright-utils/recurse/fixtures';
test('job completion', async ({ apiRequest, recurse }) => {
// Start async job
const { body: job } = await apiRequest({
method: 'POST',
path: '/api/jobs'
test('job completion', async ({ page, recurse }) => {
await page.click('button');
const result = await recurse({
fn: () => apiRequest({ method: 'GET', path: '/api/job/123' }),
predicate: (job) => job.status === 'complete',
timeout: 30000
});
// Poll until complete (correct API: command, predicate, options)
const result = await recurse(
() => apiRequest({ method: 'GET', path: `/api/jobs/${job.id}` }),
(response) => response.body.status === 'completed', // response.body from apiRequest
{
timeout: 30000,
interval: 2000,
log: 'Waiting for job to complete'
}
);
expect(result.body.status).toBe('completed');
expect(result.status).toBe('complete');
});
```
**Result:** Consistent pattern using correct playwright-utils API (command, predicate, options).
**Result:** Consistent pattern, established best practice.
## Technical Implementation
@ -529,7 +520,7 @@ For details on the knowledge base index, see:
**Overview:**
- [TEA Overview](/docs/explanation/features/tea-overview.md) - Knowledge base in workflows
- [Testing as Engineering](/docs/explanation/philosophy/testing-as-engineering.md) - **Foundation: Context engineering philosophy** (why knowledge base solves AI test problems)
- [Testing as Engineering](/docs/explanation/philosophy/testing-as-engineering.md) - Context engineering philosophy
## Practical Guides

View File

@ -125,40 +125,6 @@ test('should load dashboard data', async ({ page }) => {
- No fixed timeout (fast when API is fast)
- Validates API response (catch backend errors)
**With Playwright Utils (Even Cleaner):**
```typescript
import { test } from '@seontechnologies/playwright-utils/fixtures';
import { expect } from '@playwright/test';
test('should load dashboard data', async ({ page, interceptNetworkCall }) => {
// Set up interception BEFORE navigation
const dashboardCall = interceptNetworkCall({
method: 'GET',
url: '**/api/dashboard'
});
// Navigate
await page.goto('/dashboard');
// Wait for API response (automatic JSON parsing)
const { status, responseJson: data } = await dashboardCall;
// Validate API response
expect(status).toBe(200);
expect(data.items).toBeDefined();
// Assert UI matches API data
await expect(page.locator('.data-table')).toBeVisible();
await expect(page.locator('.data-table tr')).toHaveCount(data.items.length);
});
```
**Playwright Utils Benefits:**
- Automatic JSON parsing (no `await response.json()`)
- Returns `{ status, responseJson, requestJson }` structure
- Cleaner API (no need to check `resp.ok()`)
- Same intercept-before-navigate pattern
### Intercept-Before-Navigate Pattern
**Key insight:** Set up wait BEFORE triggering the action.
@ -230,7 +196,6 @@ sequenceDiagram
### TEA Generates Network-First Tests
**Vanilla Playwright:**
```typescript
// When you run *atdd or *automate, TEA generates:
@ -254,37 +219,6 @@ test('should create user', async ({ page }) => {
});
```
**With Playwright Utils (if `tea_use_playwright_utils: true`):**
```typescript
import { test } from '@seontechnologies/playwright-utils/fixtures';
import { expect } from '@playwright/test';
test('should create user', async ({ page, interceptNetworkCall }) => {
// TEA uses interceptNetworkCall for cleaner interception
const createUserCall = interceptNetworkCall({
method: 'POST',
url: '**/api/users'
});
await page.getByLabel('Name').fill('Test User');
await page.getByRole('button', { name: 'Submit' }).click();
// Wait for response (automatic JSON parsing)
const { status, responseJson: user } = await createUserCall;
// Validate both API and UI
expect(status).toBe(201);
expect(user.id).toBeDefined();
await expect(page.locator('.success')).toContainText(user.name);
});
```
**Playwright Utils Benefits:**
- Automatic JSON parsing (`responseJson` ready to use)
- No manual `await response.json()`
- Returns `{ status, responseJson }` structure
- Cleaner, more readable code
### TEA Reviews for Hard Waits
When you run `*test-review`:
@ -318,7 +252,6 @@ await responsePromise; // ✅
### Basic Response Wait
**Vanilla Playwright:**
```typescript
// Wait for any successful response
const promise = page.waitForResponse(resp => resp.ok());
@ -326,23 +259,8 @@ await page.click('button');
await promise;
```
**With Playwright Utils:**
```typescript
import { test } from '@seontechnologies/playwright-utils/fixtures';
test('basic wait', async ({ page, interceptNetworkCall }) => {
const responseCall = interceptNetworkCall({ url: '**' }); // Match any
await page.click('button');
const { status } = await responseCall;
expect(status).toBe(200);
});
```
---
### Specific URL Match
**Vanilla Playwright:**
```typescript
// Wait for specific endpoint
const promise = page.waitForResponse(
@ -352,21 +270,8 @@ await page.goto('/user/123');
await promise;
```
**With Playwright Utils:**
```typescript
test('specific URL', async ({ page, interceptNetworkCall }) => {
const userCall = interceptNetworkCall({ url: '**/api/users/123' });
await page.goto('/user/123');
const { status, responseJson } = await userCall;
expect(status).toBe(200);
});
```
---
### Method + Status Match
**Vanilla Playwright:**
```typescript
// Wait for POST that returns 201
const promise = page.waitForResponse(
@ -379,24 +284,8 @@ await page.click('button[type="submit"]');
await promise;
```
**With Playwright Utils:**
```typescript
test('method and status', async ({ page, interceptNetworkCall }) => {
const createCall = interceptNetworkCall({
method: 'POST',
url: '**/api/users'
});
await page.click('button[type="submit"]');
const { status, responseJson } = await createCall;
expect(status).toBe(201); // Explicit status check
});
```
---
### Multiple Responses
**Vanilla Playwright:**
```typescript
// Wait for multiple API calls
const [usersResp, postsResp] = await Promise.all([
@ -409,29 +298,8 @@ const users = await usersResp.json();
const posts = await postsResp.json();
```
**With Playwright Utils:**
```typescript
test('multiple responses', async ({ page, interceptNetworkCall }) => {
const usersCall = interceptNetworkCall({ url: '**/api/users' });
const postsCall = interceptNetworkCall({ url: '**/api/posts' });
await page.goto('/dashboard'); // Triggers both
const [{ responseJson: users }, { responseJson: posts }] = await Promise.all([
usersCall,
postsCall
]);
expect(users).toBeInstanceOf(Array);
expect(posts).toBeInstanceOf(Array);
});
```
---
### Validate Response Data
**Vanilla Playwright:**
```typescript
// Verify API response before asserting UI
const promise = page.waitForResponse(
@ -451,28 +319,6 @@ expect(order.total).toBeGreaterThan(0);
await expect(page.locator('.order-confirmation')).toContainText(order.id);
```
**With Playwright Utils:**
```typescript
test('validate response data', async ({ page, interceptNetworkCall }) => {
const checkoutCall = interceptNetworkCall({
method: 'POST',
url: '**/api/checkout'
});
await page.click('button:has-text("Complete Order")');
const { status, responseJson: order } = await checkoutCall;
// Response validation (automatic JSON parsing)
expect(status).toBe(200);
expect(order.status).toBe('confirmed');
expect(order.total).toBeGreaterThan(0);
// UI validation
await expect(page.locator('.order-confirmation')).toContainText(order.id);
});
```
## Advanced Patterns
### HAR Recording for Offline Testing
@ -635,36 +481,6 @@ test('dashboard loads data', async ({ page }) => {
- Validates UI matches API (catch frontend bugs)
- Works in any environment (local, CI, staging)
**With Playwright Utils (Even Better):**
```typescript
import { test } from '@seontechnologies/playwright-utils/fixtures';
test('dashboard loads data', async ({ page, interceptNetworkCall }) => {
const dashboardCall = interceptNetworkCall({
method: 'GET',
url: '**/api/dashboard'
});
await page.goto('/dashboard');
const { status, responseJson: { items } } = await dashboardCall;
// Validate API response (automatic JSON parsing)
expect(status).toBe(200);
expect(items).toHaveLength(5);
// Validate UI matches API
await expect(page.locator('table tr')).toHaveCount(items.length);
});
```
**Additional Benefits:**
- No manual `await response.json()` (automatic parsing)
- Cleaner destructuring of nested data
- Consistent API across all network calls
---
### Form Submission
**Traditional (Flaky):**
@ -697,35 +513,6 @@ test('form submission', async ({ page }) => {
});
```
**With Playwright Utils:**
```typescript
import { test } from '@seontechnologies/playwright-utils/fixtures';
test('form submission', async ({ page, interceptNetworkCall }) => {
const submitCall = interceptNetworkCall({
method: 'POST',
url: '**/api/submit'
});
await page.getByLabel('Email').fill('test@example.com');
await page.getByRole('button', { name: 'Submit' }).click();
const { status, responseJson: result } = await submitCall;
// Automatic JSON parsing, no manual await
expect(status).toBe(200);
expect(result.success).toBe(true);
await expect(page.locator('.success')).toBeVisible();
});
```
**Progression:**
- Traditional: Hard waits (flaky)
- Network-First (Vanilla): waitForResponse (deterministic)
- Network-First (PW-Utils): interceptNetworkCall (deterministic + cleaner API)
---
## Common Misconceptions
### "I Already Use waitForSelector"
@ -758,57 +545,29 @@ await page.waitForSelector('.success'); // Then validate UI
### "Too Much Boilerplate"
**Problem:** `waitForResponse` is verbose, repeated in every test.
**Solution:** Extract to fixtures (see Fixture Architecture)
**Solution:** Use Playwright Utils `interceptNetworkCall` - built-in fixture that reduces boilerplate.
**Vanilla Playwright (Repetitive):**
```typescript
test('test 1', async ({ page }) => {
const promise = page.waitForResponse(
resp => resp.url().includes('/api/submit') && resp.ok()
// Create reusable fixture
export const test = base.extend({
waitForApi: async ({ page }, use) => {
await use((urlPattern: string) => {
// Returns promise immediately (doesn't await)
return page.waitForResponse(
resp => resp.url().includes(urlPattern) && resp.ok()
);
await page.click('button');
await promise;
});
}
});
test('test 2', async ({ page }) => {
const promise = page.waitForResponse(
resp => resp.url().includes('/api/load') && resp.ok()
);
await page.click('button');
await promise;
});
// Repeated pattern in every test
```
**With Playwright Utils (Cleaner):**
```typescript
import { test } from '@seontechnologies/playwright-utils/fixtures';
test('test 1', async ({ page, interceptNetworkCall }) => {
const submitCall = interceptNetworkCall({ url: '**/api/submit' });
await page.click('button');
const { status, responseJson } = await submitCall;
expect(status).toBe(200);
});
test('test 2', async ({ page, interceptNetworkCall }) => {
const loadCall = interceptNetworkCall({ url: '**/api/load' });
await page.click('button');
const { responseJson } = await loadCall;
// Automatic JSON parsing, cleaner API
// Use in tests
test('test', async ({ page, waitForApi }) => {
const promise = waitForApi('/api/submit'); // Get promise
await page.click('button'); // Trigger action
await promise; // Wait for response
});
```
**Benefits:**
- Less boilerplate (fixture handles complexity)
- Automatic JSON parsing
- Glob pattern matching (`**/api/**`)
- Consistent API across all tests
See [Integrate Playwright Utils](/docs/how-to/customization/integrate-playwright-utils.md#intercept-network-call) for setup.
## Technical Implementation
For detailed network-first patterns, see the knowledge base:

View File

@ -573,7 +573,7 @@ flowchart TD
- [How to Run NFR Assessment](/docs/how-to/workflows/run-nfr-assess.md) - NFR risk assessment
**Use-Case Guides:**
- [Running TEA for Enterprise](/docs/how-to/enterprise/use-tea-for-enterprise.md) - Enterprise risk management
- [Running TEA for Enterprise](/docs/how-to/workflows/run-tea-for-enterprise.md) - Enterprise risk management
## Reference

View File

@ -107,7 +107,7 @@ test('flaky test', async ({ page }) => {
});
```
**Good Example (Vanilla Playwright):**
**Good Example:**
```typescript
test('deterministic test', async ({ page }) => {
const responsePromise = page.waitForResponse(
@ -126,43 +126,12 @@ test('deterministic test', async ({ page }) => {
});
```
**With Playwright Utils (Even Cleaner):**
```typescript
import { test } from '@seontechnologies/playwright-utils/fixtures';
import { expect } from '@playwright/test';
test('deterministic test', async ({ page, interceptNetworkCall }) => {
const submitCall = interceptNetworkCall({
method: 'POST',
url: '**/api/submit'
});
await page.click('button');
// Wait for actual response (automatic JSON parsing)
const { status, responseJson } = await submitCall;
expect(status).toBe(200);
// Modal should ALWAYS show (make it deterministic)
await expect(page.locator('.modal')).toBeVisible();
await page.click('.dismiss');
// Explicit assertion (fails if not visible)
await expect(page.locator('.success')).toBeVisible();
});
```
**Why both work:**
**Why it works:**
- Waits for actual event (network response)
- No conditionals (behavior is deterministic)
- Assertions fail loudly (no silent failures)
- Same result every run (deterministic)
**Playwright Utils additional benefits:**
- Automatic JSON parsing
- `{ status, responseJson }` structure (can validate response data)
- No manual `await response.json()`
### 2. Isolation (No Dependencies)
**Rule:** Test runs independently, no shared state.
@ -183,7 +152,7 @@ test('create user', async ({ apiRequest }) => {
const { body } = await apiRequest({
method: 'POST',
path: '/api/users',
body: { email: 'test@example.com' } (hard-coded)
body: { email: 'test@example.com' } // 'body' not 'data' (hard-coded)
});
userId = body.id; // Store in global
});
@ -193,7 +162,7 @@ test('update user', async ({ apiRequest }) => {
await apiRequest({
method: 'PATCH',
path: `/api/users/${userId}`,
body: { name: 'Updated' }
body: { name: 'Updated' } // 'body' not 'data'
});
// No cleanup - leaves user in database
});
@ -244,7 +213,7 @@ test('should update user profile', async ({ apiRequest }) => {
const { status: createStatus, body: user } = await apiRequest({
method: 'POST',
path: '/api/users',
body: { email: testEmail, name: faker.person.fullName() }
body: { email: testEmail, name: faker.person.fullName() } // 'body' not 'data'
});
expect(createStatus).toBe(201);
@ -253,7 +222,7 @@ test('should update user profile', async ({ apiRequest }) => {
const { status, body: updated } = await apiRequest({
method: 'PATCH',
path: `/api/users/${user.id}`,
body: { name: 'Updated Name' }
body: { name: 'Updated Name' } // 'body' not 'data'
});
expect(status).toBe(200);
@ -443,7 +412,7 @@ test('slow test', async ({ page }) => {
**Total time:** 3+ minutes (95 seconds wasted on hard waits)
**Good Example (Vanilla Playwright):**
**Good Example:**
```typescript
// ✅ Fast test (< 10 seconds)
test('fast test', async ({ page }) => {
@ -467,50 +436,8 @@ test('fast test', async ({ page }) => {
});
```
**With Playwright Utils:**
```typescript
import { test } from '@seontechnologies/playwright-utils/fixtures';
import { expect } from '@playwright/test';
test('fast test', async ({ page, interceptNetworkCall }) => {
// Set up interception
const resultCall = interceptNetworkCall({
method: 'GET',
url: '**/api/result'
});
await page.goto('/');
// Direct navigation (skip intermediate pages)
await page.goto('/page-10');
// Efficient selector
await page.getByRole('button', { name: 'Submit' }).click();
// Wait for actual response (automatic JSON parsing)
const { status, responseJson } = await resultCall;
expect(status).toBe(200);
await expect(page.locator('.result')).toBeVisible();
// Can also validate response data if needed
// expect(responseJson.data).toBeDefined();
});
```
**Total time:** < 10 seconds (no wasted waits)
**Both examples achieve:**
- No hard waits (wait for actual events)
- Direct navigation (skip unnecessary steps)
- Efficient selectors (getByRole)
- Fast execution
**Playwright Utils bonus:**
- Can validate API response data easily
- Automatic JSON parsing
- Cleaner API
## TEA's Quality Scoring
TEA reviews tests against these standards in `*test-review`:
@ -894,7 +821,7 @@ For detailed test quality patterns, see:
**Use-Case Guides:**
- [Using TEA with Existing Tests](/docs/how-to/brownfield/use-tea-with-existing-tests.md) - Improve legacy quality
- [Running TEA for Enterprise](/docs/how-to/enterprise/use-tea-for-enterprise.md) - Enterprise quality thresholds
- [Running TEA for Enterprise](/docs/how-to/workflows/run-tea-for-enterprise.md) - Enterprise quality thresholds
## Reference

View File

@ -150,40 +150,34 @@ test('checkout completes', async ({ page }) => {
});
```
**After (With Playwright Utils - Cleaner API):**
**After (With Playwright Utils + Auto Error Detection):**
```typescript
import { test } from '@seontechnologies/playwright-utils/fixtures';
import { expect } from '@playwright/test';
import { test } from '@seontechnologies/playwright-utils/network-error-monitor/fixtures';
test('checkout completes', async ({ page, interceptNetworkCall }) => {
// Use interceptNetworkCall for cleaner network interception
const checkoutCall = interceptNetworkCall({
method: 'POST',
url: '**/api/checkout'
});
// That's it! Just import the fixture - monitoring is automatic
test('checkout completes', async ({ page }) => {
const checkoutPromise = page.waitForResponse(
resp => resp.url().includes('/api/checkout') && resp.ok()
);
await page.click('button[name="checkout"]');
const response = await checkoutPromise;
const order = await response.json();
// Wait for response (automatic JSON parsing)
const { status, responseJson: order } = await checkoutCall;
// Validate API response
expect(status).toBe(200);
expect(order.status).toBe('confirmed');
// Validate UI
await expect(page.locator('.confirmation')).toBeVisible();
// Zero setup - automatically fails if ANY 4xx/5xx occurred
// Error message: "Network errors detected: POST 500 /api/payment"
});
```
**Playwright Utils Benefits:**
- `interceptNetworkCall` for cleaner network interception
- Automatic JSON parsing (`responseJson` ready to use)
- No manual `await response.json()`
- Glob pattern matching (`**/api/checkout`)
- Cleaner, more maintainable code
**For automatic error detection,** use `network-error-monitor` fixture separately. See [Integrate Playwright Utils](/docs/how-to/customization/integrate-playwright-utils.md#network-error-monitor).
- Auto-enabled by fixture import (zero code changes)
- Catches silent backend errors (500, 503, 504)
- Test fails even if UI shows cached/stale success message
- Structured error report in test output
- No manual error checking needed
**Priority 3: P1 Requirements**
```
@ -359,7 +353,7 @@ test.skip('flaky test - needs fixing', async ({ page }) => {
# Quarantined Tests
| Test | Reason | Owner | Target Fix Date |
| ------------------- | -------------------------- | -------- | --------------- |
|------|--------|-------|----------------|
| checkout.spec.ts:45 | Hard wait causes flakiness | QA Team | 2026-01-20 |
| profile.spec.ts:28 | Conditional flow control | Dev Team | 2026-01-25 |
```
@ -405,7 +399,7 @@ Same process
# Test Suite Status
| Directory | Tests | Quality Score | Status | Notes |
| ------------------ | ----- | ------------- | ------------- | -------------- |
|-----------|-------|---------------|--------|-------|
| tests/auth/ | 15 | 85/100 | ✅ Modernized | Week 1 cleanup |
| tests/api/ | 32 | 78/100 | ⚠️ In Progress | Week 2 |
| tests/e2e/ | 28 | 62/100 | ❌ Legacy | Week 3 planned |
@ -471,26 +465,15 @@ Incremental changes = lower risk
**Solution:**
```
1. Configure parallel execution (shard tests across workers)
2. Add selective testing (run only affected tests on PR)
3. Run full suite nightly only
4. Optimize slow tests (remove hard waits, improve selectors)
1. Run *ci to add selective testing
2. Run only affected tests on PR
3. Run full suite nightly
4. Parallelize with sharding
Before: 4 hours sequential
After: 15 minutes with sharding + selective testing
```
**How `*ci` helps:**
- Scaffolds CI configuration with parallel sharding examples
- Provides selective testing script templates
- Documents burn-in and optimization strategies
- But YOU configure workers, test selection, and optimization
**With Playwright Utils burn-in:**
- Smart selective testing based on git diff
- Volume control (run percentage of affected tests)
- See [Integrate Playwright Utils](/docs/how-to/customization/integrate-playwright-utils.md#burn-in)
### "We Have Tests But They Always Fail"
**Problem:** Tests are so flaky they're ignored.
@ -547,6 +530,43 @@ Don't let perfect be the enemy of good
*trace Phase 2 - Gate decision
```
## Success Stories
### Example: E-Commerce Platform
**Starting Point:**
- 200 E2E tests, 30% passing, 15-minute flakiness
- No API tests
- No coverage visibility
**After 3 Months with TEA:**
- 150 E2E tests (removed duplicates), 95% passing, <1% flakiness
- 300 API tests added (faster, more reliable)
- P0 coverage: 100%, P1 coverage: 85%
- Quality score: 82/100
**How:**
- Month 1: Baseline + fix top 20 flaky tests
- Month 2: Add API tests for critical path
- Month 3: Improve quality + expand P1 coverage
### Example: SaaS Application
**Starting Point:**
- 50 tests, quality score 48/100
- Hard waits everywhere
- Tests take 45 minutes
**After 6 Weeks with TEA:**
- 120 tests, quality score 78/100
- No hard waits (network-first patterns)
- Tests take 8 minutes (parallel execution)
**How:**
- Week 1-2: Replace hard waits with network-first
- Week 3-4: Add selective testing + CI parallelization
- Week 5-6: Generate tests for gaps with *automate
## Related Guides
**Workflow Guides:**

View File

@ -18,25 +18,17 @@ MCP (Model Context Protocol) servers enable AI agents to interact with live brow
## When to Use This
**For UI Testing:**
- Want exploratory mode in `*test-design` (browser-based UI discovery)
- Want recording mode in `*atdd` or `*automate` (verify selectors with live browser)
- Want recording mode in `*atdd` (verify selectors with live browser)
- Want healing mode in `*automate` (fix tests with visual debugging)
- Debugging complex UI issues
- Need accurate selectors from actual DOM
- Debugging complex UI interactions
**For API Testing:**
- Want healing mode in `*automate` (analyze failures with trace data)
- Need to debug test failures (network responses, request/response data, timing)
- Want to inspect trace files (network traffic, errors, race conditions)
**For Both:**
- Visual debugging (trace viewer shows network + UI)
- Test failure analysis (MCP can run tests and extract errors)
- Understanding complex test failures (network + DOM together)
**Don't use if:**
- You're new to TEA (adds complexity)
- You don't have MCP servers configured
- Your tests work fine without it
- You're testing APIs only (no UI)
## Prerequisites
@ -79,11 +71,13 @@ MCP (Model Context Protocol) servers enable AI agents to interact with live brow
Both servers work together to provide full TEA MCP capabilities.
## Setup
## Installation
### 1. Configure MCP Servers
### Step 1: Configure MCP Servers in IDE
Add to your IDE's MCP configuration:
Add this configuration to your IDE's MCP settings. See [TEA Overview](/docs/explanation/features/tea-overview.md#playwright-mcp-enhancements) for IDE-specific configuration locations.
**MCP Configuration:**
```json
{
@ -100,20 +94,36 @@ Add to your IDE's MCP configuration:
}
```
See [TEA Overview](/docs/explanation/features/tea-overview.md#playwright-mcp-enhancements) for IDE-specific config locations.
### Step 2: Install Playwright Browsers
### 2. Enable in BMAD
```bash
npx playwright install
```
Answer "Yes" when prompted during installation, or set in config:
### Step 3: Enable in TEA Config
Edit `_bmad/bmm/config.yaml`:
```yaml
# _bmad/bmm/config.yaml
tea_use_mcp_enhancements: true
```
### 3. Verify MCPs Running
### Step 4: Restart IDE
Ensure your MCP servers are running in your IDE.
Restart your IDE to load MCP server configuration.
### Step 5: Verify MCP Servers
Check MCP servers are running:
**In Cursor:**
- Open command palette (Cmd/Ctrl + Shift + P)
- Search "MCP"
- Should see "Playwright" and "Playwright Test" servers listed
**In VS Code:**
- Check Claude extension settings
- Verify MCP servers are enabled
## How MCP Enhances TEA Workflows
@ -152,14 +162,16 @@ I'll design tests for these interactions."
**Without MCP:**
- TEA generates selectors from best practices
- TEA infers API patterns from documentation
- May use `getByRole()` that doesn't match actual app
- Selectors might need adjustment
**With MCP (Recording Mode):**
**For UI Tests:**
**With MCP:**
TEA verifies selectors with live browser:
```
[TEA navigates to /login with live browser]
[Inspects actual form fields]
"Let me verify the login form selectors"
[TEA navigates to /login]
[Inspects form fields]
"I see:
- Email input has label 'Email Address' (not 'Email')
@ -169,58 +181,47 @@ I'll design tests for these interactions."
I'll use these exact selectors."
```
**For API Tests:**
```
[TEA analyzes trace files from test runs]
[Inspects network requests/responses]
"I see the API returns:
- POST /api/login → 200 with { token, userId }
- Response time: 150ms
- Required headers: Content-Type, Authorization
I'll validate these in tests."
**Generated test:**
```typescript
await page.getByLabel('Email Address').fill('test@example.com');
await page.getByLabel('Your Password').fill('password');
await page.getByRole('button', { name: 'Sign In' }).click();
// Selectors verified against actual DOM
```
**Benefits:**
- UI: Accurate selectors from real DOM
- API: Validated request/response patterns from trace
- Both: Tests work on first run
- Accurate selectors from real DOM
- Tests work on first run
- No trial-and-error selector debugging
### *automate: Healing + Recording Modes
### *automate: Healing Mode
**Without MCP:**
- TEA analyzes test code only
- Suggests fixes based on static analysis
- Generates tests from documentation/code
- Can't verify fixes work
**With MCP:**
**Healing Mode (UI + API):**
TEA uses visual debugging:
```
"This test is failing. Let me debug with trace viewer"
[TEA opens trace file]
[Analyzes screenshots + network tab]
[Analyzes screenshots]
[Identifies selector changed]
UI failures: "Button selector changed from 'Save' to 'Save Changes'"
API failures: "Response structure changed, expected {id} got {userId}"
"The button selector changed from 'Save' to 'Save Changes'
I'll update the test and verify it works"
[TEA makes fixes]
[Verifies with trace analysis]
```
**Recording Mode (UI + API):**
```
UI: [Inspects actual DOM, generates verified selectors]
API: [Analyzes network traffic, validates request/response patterns]
[Generates tests with verified patterns]
[Tests work on first run]
[TEA makes fix]
[Runs test with MCP]
[Confirms test passes]
```
**Benefits:**
- Visual debugging + trace analysis (not just UI)
- Verified selectors (UI) + network patterns (API)
- Tests verified against actual application behavior
- Visual debugging during healing
- Verified fixes (not guesses)
- Faster resolution
## Usage Examples
@ -289,6 +290,43 @@ Fixing selector and verifying...
Updated test with corrected selector.
```
## Configuration Options
### MCP Server Arguments
**Playwright MCP with custom port:**
```json
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@latest", "--port", "3000"]
}
}
}
```
**Playwright Test with specific browser:**
```json
{
"mcpServers": {
"playwright-test": {
"command": "npx",
"args": ["playwright", "run-test-mcp-server", "--browser", "chromium"]
}
}
}
```
### Environment Variables
```bash
# .env
PLAYWRIGHT_BROWSER=chromium # Browser for MCP
PLAYWRIGHT_HEADLESS=false # Show browser during MCP
PLAYWRIGHT_SLOW_MO=100 # Slow down for visibility
```
## Troubleshooting
### MCP Servers Not Running
@ -395,6 +433,107 @@ tea_use_mcp_enhancements: true
tea_use_mcp_enhancements: false
```
## Best Practices
### Use MCP for Complex UIs
**Simple UI (skip MCP):**
```
Standard login form with email/password
TEA can infer selectors without MCP
```
**Complex UI (use MCP):**
```
Multi-step wizard with dynamic fields
Conditional UI elements
Third-party components
Custom form widgets
```
### Start Without MCP, Enable When Needed
**Learning path:**
1. Week 1-2: TEA without MCP (learn basics)
2. Week 3: Enable MCP (explore advanced features)
3. Week 4+: Use MCP selectively (when it adds value)
### Combine with Playwright Utils
**Powerful combination:**
```yaml
tea_use_playwright_utils: true
tea_use_mcp_enhancements: true
```
**Benefits:**
- Playwright Utils provides production-ready utilities
- MCP verifies utilities work with actual app
- Best of both worlds
### Use for Test Healing
**Scenario:** Test suite has 50 failing tests after UI update.
**With MCP:**
```
*automate (healing mode)
TEA:
1. Opens trace viewer for each failure
2. Identifies changed selectors
3. Updates tests with corrected selectors
4. Verifies fixes with browser
5. Provides updated tests
Result: 45/50 tests auto-healed
```
### Use for New Team Members
**Onboarding:**
```
New developer: "I don't know this codebase's UI"
Senior: "Run *test-design with MCP exploratory mode"
TEA explores UI and generates documentation:
- UI structure discovered
- Interactive elements mapped
- Test design created automatically
```
## Security Considerations
### MCP Servers Have Browser Access
**What MCP can do:**
- Navigate to any URL
- Click any element
- Fill any form
- Access browser storage
- Read page content
**Best practices:**
- Only configure MCP in trusted environments
- Don't use MCP on production sites (use staging/dev)
- Review generated tests before running on production
- Keep MCP config in local files (not committed)
### Protect Credentials
**Don't:**
```
"TEA, login with mypassword123"
# Password visible in chat history
```
**Do:**
```
"TEA, login using credentials from .env"
# Password loaded from environment, not in chat
```
## Related Guides
**Getting Started:**

View File

@ -62,7 +62,7 @@ Edit `_bmad/bmm/config.yaml`:
tea_use_playwright_utils: true
```
**Note:** If you enabled this during BMad installation, it's already set.
**Note:** If you enabled this during installation (`npx bmad-method@alpha install`), it's already set.
### Step 3: Verify Installation
@ -175,16 +175,13 @@ Reviews against playwright-utils best practices:
### *ci Workflow
**Without Playwright Utils:**
- Parallel sharding
- Burn-in loops (basic shell scripts)
- CI triggers (PR, push, schedule)
- Artifact collection
Basic CI configuration
**With Playwright Utils:**
Enhanced with smart testing:
- Burn-in utility (git diff-based, volume control)
- Selective testing (skip config/docs/types changes)
- Test prioritization by file changes
Enhanced CI with:
- Burn-in utility for smart test selection
- Selective testing based on git diff
- Test prioritization
## Available Utilities
@ -192,18 +189,6 @@ Enhanced with smart testing:
Typed HTTP client with schema validation.
**Official Docs:** <https://seontechnologies.github.io/playwright-utils/api-request.html>
**Why Use This?**
| Vanilla Playwright | api-request Utility |
|-------------------|---------------------|
| Manual `await response.json()` | Automatic JSON parsing |
| `response.status()` + separate body parsing | Returns `{ status, body }` structure |
| No built-in retry | Automatic retry for 5xx errors |
| No schema validation | Single-line `.validateSchema()` |
| Verbose status checking | Clean destructuring |
**Usage:**
```typescript
import { test } from '@seontechnologies/playwright-utils/api-request/fixtures';
@ -221,7 +206,7 @@ test('should create user', async ({ apiRequest }) => {
method: 'POST',
path: '/api/users', // Note: 'path' not 'url'
body: { name: 'Test User', email: 'test@example.com' } // Note: 'body' not 'data'
}).validateSchema(UserSchema); // Chained method (can await separately if needed)
}).validateSchema(UserSchema); // Note: chained method
expect(status).toBe(201);
expect(body.id).toBeDefined();
@ -239,17 +224,6 @@ test('should create user', async ({ apiRequest }) => {
Authentication session management with token persistence.
**Official Docs:** <https://seontechnologies.github.io/playwright-utils/auth-session.html>
**Why Use This?**
| Vanilla Playwright Auth | auth-session |
|------------------------|--------------|
| Re-authenticate every test run (slow) | Authenticate once, persist to disk |
| Single user per setup | Multi-user support (roles, accounts) |
| No token expiration handling | Automatic token renewal |
| Manual session management | Provider pattern (flexible auth) |
**Usage:**
```typescript
import { test } from '@seontechnologies/playwright-utils/auth-session/fixtures';
@ -288,17 +262,6 @@ async function globalSetup() {
Record and replay network traffic (HAR) for offline testing.
**Official Docs:** <https://seontechnologies.github.io/playwright-utils/network-recorder.html>
**Why Use This?**
| Vanilla Playwright HAR | network-recorder |
|------------------------|------------------|
| Manual `routeFromHAR()` configuration | Automatic HAR management with `PW_NET_MODE` |
| Separate record/playback test files | Same test, switch env var |
| No CRUD detection | Stateful mocking (POST/PUT/DELETE work) |
| Manual HAR file paths | Auto-organized by test name |
**Usage:**
```typescript
import { test } from '@seontechnologies/playwright-utils/network-recorder/fixtures';
@ -338,17 +301,6 @@ PW_NET_MODE=playback npx playwright test
Spy or stub network requests with automatic JSON parsing.
**Official Docs:** <https://seontechnologies.github.io/playwright-utils/intercept-network-call.html>
**Why Use This?**
| Vanilla Playwright | interceptNetworkCall |
|-------------------|----------------------|
| Route setup + response waiting (separate steps) | Single declarative call |
| Manual `await response.json()` | Automatic JSON parsing (`responseJson`) |
| Complex filter predicates | Simple glob patterns (`**/api/**`) |
| Verbose syntax | Concise, readable API |
**Usage:**
```typescript
import { test } from '@seontechnologies/playwright-utils/fixtures';
@ -385,17 +337,6 @@ test('should handle API errors', async ({ page, interceptNetworkCall }) => {
Async polling for eventual consistency (Cypress-style).
**Official Docs:** <https://seontechnologies.github.io/playwright-utils/recurse.html>
**Why Use This?**
| Manual Polling | recurse Utility |
|----------------|-----------------|
| `while` loops with `waitForTimeout` | Smart polling with exponential backoff |
| Hard-coded retry logic | Configurable timeout/interval |
| No logging visibility | Optional logging with custom messages |
| Verbose, error-prone | Clean, readable API |
**Usage:**
```typescript
import { test } from '@seontechnologies/playwright-utils/fixtures';
@ -432,17 +373,6 @@ test('should wait for async job completion', async ({ apiRequest, recurse }) =>
Structured logging that integrates with Playwright reports.
**Official Docs:** <https://seontechnologies.github.io/playwright-utils/log.html>
**Why Use This?**
| Console.log / print | log Utility |
|--------------------|-------------|
| Not in test reports | Integrated with Playwright reports |
| No step visualization | `.step()` shows in Playwright UI |
| Manual object formatting | Logs objects seamlessly |
| No structured output | JSON artifacts for debugging |
**Usage:**
```typescript
import { log } from '@seontechnologies/playwright-utils';
@ -466,24 +396,13 @@ test('should login', async ({ page }) => {
- Direct import (no fixture needed for basic usage)
- Structured logs in test reports
- `.step()` shows in Playwright UI
- Logs objects seamlessly (no special handling needed)
- Supports object logging with `.debug()`
- Trace test execution
### file-utils
Read and validate CSV, PDF, XLSX, ZIP files.
**Official Docs:** <https://seontechnologies.github.io/playwright-utils/file-utils.html>
**Why Use This?**
| Vanilla Playwright | file-utils |
|-------------------|------------|
| ~80 lines per CSV flow | ~10 lines end-to-end |
| Manual download event handling | `handleDownload()` encapsulates all |
| External parsing libraries | Auto-parsing (CSV, XLSX, PDF, ZIP) |
| No validation helpers | Built-in validation (headers, row count) |
**Usage:**
```typescript
import { handleDownload, readCSV } from '@seontechnologies/playwright-utils/file-utils';
@ -525,17 +444,6 @@ test('should export valid CSV', async ({ page }) => {
Smart test selection with git diff analysis for CI optimization.
**Official Docs:** <https://seontechnologies.github.io/playwright-utils/burn-in.html>
**Why Use This?**
| Playwright `--only-changed` | burn-in Utility |
|-----------------------------|-----------------|
| Config changes trigger all tests | Smart filtering (skip configs, types, docs) |
| All or nothing | Volume control (run percentage) |
| No customization | Custom dependency analysis |
| Slow CI on minor changes | Fast CI with intelligent selection |
**Usage:**
```typescript
// scripts/burn-in-changed.ts
@ -582,7 +490,6 @@ export default config;
```
**Benefits:**
- **Ensure flake-free tests upfront** - Never deal with test flake again
- Smart filtering (skip config, types, docs changes)
- Volume control (run percentage of affected tests)
- Git diff-based test selection
@ -592,17 +499,6 @@ export default config;
Automatically detect HTTP 4xx/5xx errors during tests.
**Official Docs:** <https://seontechnologies.github.io/playwright-utils/network-error-monitor.html>
**Why Use This?**
| Vanilla Playwright | network-error-monitor |
|-------------------|----------------------|
| UI passes, backend 500 ignored | Auto-fails on any 4xx/5xx |
| Manual error checking | Zero boilerplate (auto-enabled) |
| Silent failures slip through | Acts like Sentry for tests |
| No domino effect prevention | Limits cascading failures |
**Usage:**
```typescript
import { test } from '@seontechnologies/playwright-utils/network-error-monitor/fixtures';
@ -644,76 +540,98 @@ test.describe('error handling',
**Benefits:**
- Auto-enabled (zero setup)
- Catches silent backend failures (500, 503, 504)
- **Prevents domino effect** (limits cascading failures from one bad endpoint)
- Opt-out with annotations for validation tests
- Structured error reporting (JSON artifacts)
- Catches silent backend failures
- Opt-out with annotations
- Structured error reporting
## Fixture Composition
**Option 1: Use Package's Combined Fixtures (Simplest)**
Combine utilities using `mergeTests`:
**Option 1: Use Combined Fixtures (Simplest)**
```typescript
// Import all utilities at once
import { test } from '@seontechnologies/playwright-utils/fixtures';
import { log } from '@seontechnologies/playwright-utils';
import { expect } from '@playwright/test';
test('api test', async ({ apiRequest, interceptNetworkCall }) => {
await log.info('Fetching users');
test('full test', async ({ apiRequest, authToken, interceptNetworkCall }) => {
await log.info('Starting test'); // log is direct import
const { status, body } = await apiRequest({
method: 'GET',
path: '/api/users'
path: '/api/data',
headers: { Authorization: `Bearer ${authToken}` }
});
await log.info('Data fetched', body);
expect(status).toBe(200);
});
```
**Option 2: Create Custom Merged Fixtures (Selective)**
**Note:** `log` is imported directly (not a fixture). `authToken` requires auth-session provider setup.
**File 1: support/merged-fixtures.ts**
**Option 2: Merge Individual Fixtures (Selective)**
```typescript
import { test as base, mergeTests } from '@playwright/test';
import { test as apiRequest } from '@seontechnologies/playwright-utils/api-request/fixtures';
import { test as interceptNetworkCall } from '@seontechnologies/playwright-utils/intercept-network-call/fixtures';
import { test as networkErrorMonitor } from '@seontechnologies/playwright-utils/network-error-monitor/fixtures';
import { test as base } from '@playwright/test';
import { mergeTests } from '@playwright/test';
import { test as apiRequestFixture } from '@seontechnologies/playwright-utils/api-request/fixtures';
import { test as recurseFixture } from '@seontechnologies/playwright-utils/recurse/fixtures';
import { log } from '@seontechnologies/playwright-utils';
// Merge only what you need
// Merge only the fixtures you need
export const test = mergeTests(
base,
apiRequest,
interceptNetworkCall,
networkErrorMonitor
apiRequestFixture,
recurseFixture
);
export const expect = base.expect;
export { log };
```
export { expect } from '@playwright/test';
**File 2: tests/api/users.spec.ts**
```typescript
import { test, expect, log } from '../support/merged-fixtures';
test('api test', async ({ apiRequest, interceptNetworkCall }) => {
await log.info('Fetching users');
// Use merged utilities in tests
test('selective test', async ({ apiRequest, recurse }) => {
await log.info('Starting test'); // log is direct import, not fixture
const { status, body } = await apiRequest({
method: 'GET',
path: '/api/users'
path: '/api/data'
});
await log.info('Data fetched', body);
expect(status).toBe(200);
});
```
**Contrast:**
- Option 1: All utilities available, zero setup
- Option 2: Pick utilities you need, one central file
**Note:** `log` is a direct utility (not a fixture), so import it separately.
**See working examples:** <https://github.com/seontechnologies/playwright-utils/tree/main/playwright/support>
**Recommended:** Use Option 1 (combined fixtures) unless you need fine control over which utilities are included.
## Configuration
### Environment Variables
```bash
# .env
PLAYWRIGHT_UTILS_LOG_LEVEL=debug # debug | info | warn | error
PLAYWRIGHT_UTILS_RETRY_ATTEMPTS=3
PLAYWRIGHT_UTILS_TIMEOUT=30000
```
### Playwright Config
```typescript
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
// Playwright Utils works with standard Playwright config
baseURL: process.env.BASE_URL || 'http://localhost:3000',
extraHTTPHeaders: {
// Add headers used by utilities
}
}
});
```
## Troubleshooting
@ -780,6 +698,47 @@ expect(status).toBe(200);
## Migration Guide
### Migrating Existing Tests
**Before (Vanilla Playwright):**
```typescript
test('should access protected route', async ({ page, request }) => {
// Manual auth token fetch
const response = await request.post('/api/auth/login', {
data: { email: 'test@example.com', password: 'pass' }
});
const { token } = await response.json();
// Manual token storage
await page.goto('/dashboard');
await page.evaluate((token) => {
localStorage.setItem('authToken', token);
}, token);
await expect(page).toHaveURL('/dashboard');
});
```
**After (With Playwright Utils):**
```typescript
import { test } from '@seontechnologies/playwright-utils/auth-session/fixtures';
test('should access protected route', async ({ page, authToken }) => {
// authToken automatically fetched and persisted by fixture
await page.goto('/dashboard');
// Token is already in place (no manual storage needed)
await expect(page).toHaveURL('/dashboard');
});
```
**Benefits:**
- Token fetched once, reused across all tests (persisted to disk)
- No manual token storage or management
- Automatic token renewal if expired
- Multi-user support via `authOptions.userIdentifier`
- 10 lines → 5 lines (less code)
## Related Guides
**Getting Started:**
@ -796,7 +755,6 @@ expect(status).toBe(200);
## Understanding the Concepts
- [Testing as Engineering](/docs/explanation/philosophy/testing-as-engineering.md) - **Why Playwright Utils matters** (part of TEA's three-part solution)
- [Fixture Architecture](/docs/explanation/tea/fixture-architecture.md) - Pure function → fixture pattern
- [Network-First Patterns](/docs/explanation/tea/network-first-patterns.md) - Network utilities explained
- [Test Quality Standards](/docs/explanation/tea/test-quality-standards.md) - Patterns PW-Utils enforces

View File

@ -90,14 +90,16 @@ TEA will ask what test levels to generate:
- E2E tests (browser-based, full user journey)
- API tests (backend only, faster)
- Component tests (UI components in isolation)
- Mix of levels (see [API Tests First, E2E Later](#api-tests-first-e2e-later) tip)
- Mix of levels
**Recommended approach:** Generate API tests first, then E2E tests (see [API Tests First, E2E Later](#api-tests-first-e2e-later) tip below).
### Component Testing by Framework
TEA generates component tests using framework-appropriate tools:
| Your Framework | Component Testing Tool |
| -------------- | ------------------------------------------- |
|----------------|----------------------|
| **Cypress** | Cypress Component Testing (*.cy.tsx) |
| **Playwright** | Vitest + React Testing Library (*.test.tsx) |
@ -188,7 +190,7 @@ test.describe('Profile API', () => {
const { status, body } = await apiRequest({
method: 'PATCH',
path: '/api/profile',
body: {
body: { // 'body' not 'data'
name: 'Updated Name',
email: 'updated@example.com'
}
@ -203,7 +205,7 @@ test.describe('Profile API', () => {
const { status, body } = await apiRequest({
method: 'PATCH',
path: '/api/profile',
body: { email: 'invalid-email' }
body: { email: 'invalid-email' } // 'body' not 'data'
});
expect(status).toBe(400);
@ -224,28 +226,52 @@ test.describe('Profile API', () => {
```typescript
import { test, expect } from '@playwright/test';
test('should edit and save profile', async ({ page }) => {
test.describe('Profile Page', () => {
test.beforeEach(async ({ page }) => {
// Login first
await page.goto('/login');
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Sign in' }).click();
});
// Navigate to profile
test('should display current profile information', async ({ page }) => {
await page.goto('/profile');
// Edit profile
await expect(page.getByText('test@example.com')).toBeVisible();
await expect(page.getByText('Test User')).toBeVisible();
});
test('should edit and save profile', async ({ page }) => {
await page.goto('/profile');
// Click edit
await page.getByRole('button', { name: 'Edit Profile' }).click();
// Modify fields
await page.getByLabel('Name').fill('Updated Name');
await page.getByLabel('Email').fill('updated@example.com');
// Save
await page.getByRole('button', { name: 'Save' }).click();
// Verify success
await expect(page.getByText('Profile updated')).toBeVisible();
await expect(page.getByText('Profile updated successfully')).toBeVisible();
await expect(page.getByText('Updated Name')).toBeVisible();
});
test('should show validation error for invalid email', async ({ page }) => {
await page.goto('/profile');
await page.getByRole('button', { name: 'Edit Profile' }).click();
await page.getByLabel('Email').fill('invalid-email');
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Invalid email format')).toBeVisible();
});
});
```
TEA generates additional E2E tests for display, validation errors, etc. based on acceptance criteria.
#### Implementation Checklist
TEA also provides an implementation checklist:
@ -374,13 +400,18 @@ Run `*test-design` before `*atdd` for better results:
*atdd # Generate tests based on design
```
### MCP Enhancements (Optional)
### Recording Mode Note
If you have MCP servers configured (`tea_use_mcp_enhancements: true`), TEA can use them during `*atdd`.
**Recording mode is NOT typically used with ATDD** because ATDD generates tests for features that don't exist yet (no UI to record against).
**Note:** ATDD is for features that don't exist yet, so recording mode (verify selectors with live UI) only applies if you have skeleton/mockup UI already implemented. For typical ATDD (no UI yet), TEA infers selectors from best practices.
If you have a skeleton UI or are refining existing tests, use `*automate` with recording mode instead. See [How to Run Automate](/docs/how-to/workflows/run-automate.md).
See [Enable MCP Enhancements](/docs/how-to/customization/enable-tea-mcp-enhancements.md) for setup.
**Recording mode is only applicable for ATDD in the rare case where:**
- You have skeleton/mockup UI already implemented
- You want to verify selector patterns before full implementation
- You're doing "UI-first" development (unusual for TDD)
For most ATDD workflows, **skip recording mode** - TEA will infer selectors from best practices.
### Focus on P0/P1 Scenarios
@ -413,6 +444,43 @@ TEA generates deterministic tests by default:
Don't modify these patterns - they prevent flakiness!
## Common Issues
### Tests Don't Fail Initially
**Problem:** Tests pass on first run but feature doesn't exist.
**Cause:** Tests are hitting wrong endpoints or checking wrong things.
**Solution:** Review generated tests - ensure they match your feature requirements.
### Too Many Tests Generated
**Problem:** TEA generated 50 tests for a simple feature.
**Cause:** Didn't specify priorities or scope.
**Solution:** Be specific:
```
Generate ONLY:
- P0 scenarios (2-3 tests)
- Happy path for API
- One E2E test for full flow
```
### Selectors Are Fragile
**Problem:** E2E tests use brittle selectors (CSS, XPath).
**Solution:** Use MCP recording mode or specify accessible selectors:
```
Use accessible locators:
- getByRole()
- getByLabel()
- getByText()
Avoid CSS selectors
```
## Related Guides
- [How to Run Test Design](/docs/how-to/workflows/run-test-design.md) - Plan before generating
@ -421,7 +489,6 @@ Don't modify these patterns - they prevent flakiness!
## Understanding the Concepts
- [Testing as Engineering](/docs/explanation/philosophy/testing-as-engineering.md) - **Why TEA generates quality tests** (foundational)
- [Risk-Based Testing](/docs/explanation/tea/risk-based-testing.md) - Why P0 vs P3 matters
- [Test Quality Standards](/docs/explanation/tea/test-quality-standards.md) - What makes tests good
- [Network-First Patterns](/docs/explanation/tea/network-first-patterns.md) - Avoiding flakiness

View File

@ -221,7 +221,7 @@ testWithAuth.describe('Profile API', () => {
const { status, body } = await apiRequest({
method: 'PATCH',
path: '/api/profile',
body: { name: 'Updated Name', bio: 'Test bio' },
body: { name: 'Updated Name', bio: 'Test bio' }, // 'body' not 'data'
headers: { Authorization: `Bearer ${authToken}` }
}).validateSchema(ProfileSchema); // Chained validation
@ -233,7 +233,7 @@ testWithAuth.describe('Profile API', () => {
const { status, body } = await apiRequest({
method: 'PATCH',
path: '/api/profile',
body: { email: 'invalid-email' },
body: { email: 'invalid-email' }, // 'body' not 'data'
headers: { Authorization: `Bearer ${authToken}` }
});
@ -250,31 +250,58 @@ testWithAuth.describe('Profile API', () => {
- Automatic retry for 5xx errors
- Less boilerplate (no manual `await response.json()` everywhere)
#### E2E Tests (`tests/e2e/profile.spec.ts`):
#### E2E Tests (`tests/e2e/profile-workflow.spec.ts`):
```typescript
import { test, expect } from '@playwright/test';
test('should edit profile', async ({ page }) => {
test.describe('Profile Management Workflow', () => {
test.beforeEach(async ({ page }) => {
// Login
await page.goto('/login');
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Sign in' }).click();
// Edit profile
// Wait for login to complete
await expect(page).toHaveURL(/\/dashboard/);
});
test('should view and edit profile', async ({ page }) => {
// Navigate to profile
await page.goto('/profile');
// Verify profile displays
await expect(page.getByText('test@example.com')).toBeVisible();
// Edit profile
await page.getByRole('button', { name: 'Edit Profile' }).click();
await page.getByLabel('Name').fill('New Name');
await page.getByRole('button', { name: 'Save' }).click();
// Verify success
await expect(page.getByText('Profile updated')).toBeVisible();
await expect(page.getByText('New Name')).toBeVisible();
});
test('should show validation errors', async ({ page }) => {
await page.goto('/profile');
await page.getByRole('button', { name: 'Edit Profile' }).click();
// Enter invalid email
await page.getByLabel('Email').fill('invalid');
await page.getByRole('button', { name: 'Save' }).click();
// Verify error shown
await expect(page.getByText('Invalid email format')).toBeVisible();
// Profile should not be updated
await page.reload();
await expect(page.getByText('test@example.com')).toBeVisible();
});
});
```
TEA generates additional tests for validation, edge cases, etc. based on priorities.
#### Fixtures (`tests/support/fixtures/profile.ts`):
**Vanilla Playwright:**
@ -478,7 +505,7 @@ Compare against:
TEA supports component testing using framework-appropriate tools:
| Your Framework | Component Testing Tool | Tests Location |
| -------------- | ------------------------------ | ----------------------------------------- |
|----------------|----------------------|----------------|
| **Cypress** | Cypress Component Testing | `tests/component/` |
| **Playwright** | Vitest + React Testing Library | `tests/component/` or `src/**/*.test.tsx` |
@ -541,14 +568,25 @@ Don't duplicate that coverage
TEA will analyze existing tests and only generate new scenarios.
### MCP Enhancements (Optional)
### Use Healing Mode (Optional)
If you have MCP servers configured (`tea_use_mcp_enhancements: true`), TEA can use them during `*automate` for:
If MCP enhancements enabled (`tea_use_mcp_enhancements: true`):
- **Healing mode:** Fix broken selectors, update assertions, enhance with trace analysis
- **Recording mode:** Verify selectors with live browser, capture network requests
When prompted, select "healing mode" to:
- Fix broken selectors in existing tests
- Update outdated assertions
- Enhance with trace viewer insights
No prompts - TEA uses MCPs automatically when available. See [Enable MCP Enhancements](/docs/how-to/customization/enable-tea-mcp-enhancements.md) for setup.
See [Enable MCP Enhancements](/docs/how-to/customization/enable-tea-mcp-enhancements.md)
### Use Recording Mode (Optional)
If MCP enhancements enabled:
When prompted, select "recording mode" to:
- Verify selectors against live browser
- Generate accurate locators from actual DOM
- Capture network requests
### Generate Tests Incrementally
@ -624,11 +662,21 @@ We already have these tests:
Generate tests for scenarios NOT covered by those files
```
### MCP Enhancements for Better Selectors
### Selectors Are Fragile
If you have MCP servers configured, TEA verifies selectors against live browser. Otherwise, TEA generates accessible selectors (`getByRole`, `getByLabel`) by default.
**Problem:** E2E tests use brittle CSS selectors.
Setup: Answer "Yes" to MCPs in BMad installer + configure MCP servers in your IDE. See [Enable MCP Enhancements](/docs/how-to/customization/enable-tea-mcp-enhancements.md).
**Solution:** Request accessible selectors:
```
Use accessible locators:
- getByRole()
- getByLabel()
- getByText()
Avoid CSS selectors like .class-name or #id
```
Or use MCP recording mode for verified selectors.
## Related Guides
@ -638,7 +686,6 @@ Setup: Answer "Yes" to MCPs in BMad installer + configure MCP servers in your ID
## Understanding the Concepts
- [Testing as Engineering](/docs/explanation/philosophy/testing-as-engineering.md) - **Why TEA generates quality tests** (foundational)
- [Risk-Based Testing](/docs/explanation/tea/risk-based-testing.md) - Why prioritize P0 over P3
- [Test Quality Standards](/docs/explanation/tea/test-quality-standards.md) - What makes tests good
- [Fixture Architecture](/docs/explanation/tea/fixture-architecture.md) - Reusable test patterns

View File

@ -662,7 +662,7 @@ Assess categories incrementally, not all at once.
- [How to Run Trace](/docs/how-to/workflows/run-trace.md) - Gate decision complements NFR
- [How to Run Test Review](/docs/how-to/workflows/run-test-review.md) - Quality complements NFR
- [Run TEA for Enterprise](/docs/how-to/enterprise/use-tea-for-enterprise.md) - Enterprise workflow
- [Run TEA for Enterprise](/docs/how-to/workflows/run-tea-for-enterprise.md) - Enterprise workflow
## Understanding the Concepts

View File

@ -63,7 +63,7 @@ TEA will ask where requirements are defined.
**Options:**
| Source | Example | Best For |
| --------------- | ----------------------------- | ---------------------- |
|--------|---------|----------|
| **Story file** | `story-profile-management.md` | Single story coverage |
| **Test design** | `test-design-epic-1.md` | Epic coverage |
| **PRD** | `PRD.md` | System-level coverage |
@ -114,7 +114,7 @@ TEA generates a comprehensive traceability matrix.
## Coverage Summary
| Metric | Count | Percentage |
| ---------------------- | ----- | ---------- |
|--------|-------|------------|
| **Total Requirements** | 15 | 100% |
| **Full Coverage** | 11 | 73% |
| **Partial Coverage** | 3 | 20% |
@ -123,7 +123,7 @@ TEA generates a comprehensive traceability matrix.
### By Priority
| Priority | Total | Covered | Percentage |
| -------- | ----- | ------- | ----------------- |
|----------|-------|---------|------------|
| **P0** | 5 | 5 | 100% ✅ |
| **P1** | 6 | 5 | 83% ⚠️ |
| **P2** | 3 | 1 | 33% ⚠️ |
@ -224,7 +224,7 @@ TEA generates a comprehensive traceability matrix.
### Critical Gaps (Must Fix Before Release)
| Gap | Requirement | Priority | Risk | Recommendation |
| --- | ------------------------ | -------- | ---- | ------------------- |
|-----|-------------|----------|------|----------------|
| 1 | Bio field not tested | P0 | High | Add E2E + API tests |
| 2 | Avatar upload not tested | P0 | High | Add E2E + API tests |
@ -235,7 +235,7 @@ TEA generates a comprehensive traceability matrix.
### Non-Critical Gaps (Can Defer)
| Gap | Requirement | Priority | Risk | Recommendation |
| --- | ------------------------- | -------- | ---- | ------------------- |
|-----|-------------|----------|------|----------------|
| 3 | Profile export not tested | P2 | Low | Add in v1.3 release |
**Estimated Effort:** 2 hours
@ -297,7 +297,7 @@ test('should update bio via API', async ({ apiRequest, authToken }) => {
const { status, body } = await apiRequest({
method: 'PATCH',
path: '/api/profile',
body: { bio: 'Updated bio' },
body: { bio: 'Updated bio' }, // 'body' not 'data'
headers: { Authorization: `Bearer ${authToken}` }
});
@ -443,7 +443,7 @@ TEA makes evidence-based gate decision and writes to separate file.
## Coverage Analysis
| Priority | Required Coverage | Actual Coverage | Status |
| -------- | ----------------- | --------------- | --------------------- |
|----------|------------------|-----------------|--------|
| **P0** | 100% | 100% | ✅ PASS |
| **P1** | 90% | 100% | ✅ PASS |
| **P2** | 50% | 33% | ⚠️ Below (acceptable) |
@ -457,7 +457,7 @@ TEA makes evidence-based gate decision and writes to separate file.
## Quality Metrics
| Metric | Threshold | Actual | Status |
| ------------------ | --------- | ------ | ------ |
|--------|-----------|--------|--------|
| P0/P1 Coverage | >95% | 100% | ✅ |
| Test Quality Score | >80 | 84 | ✅ |
| NFR Status | PASS | PASS | ✅ |
@ -502,7 +502,7 @@ TEA makes evidence-based gate decision and writes to separate file.
TEA uses deterministic rules when decision_mode = "deterministic":
| P0 Coverage | P1 Coverage | Overall Coverage | Decision |
| ----------- | ----------- | ---------------- | ---------------------------- |
|-------------|-------------|------------------|----------|
| 100% | ≥90% | ≥80% | **PASS** ✅ |
| 100% | 80-89% | ≥80% | **CONCERNS** ⚠️ |
| <100% | Any | Any | **FAIL** |
@ -684,7 +684,7 @@ Track improvement over time:
## Coverage Trend
| Date | Epic | P0/P1 Coverage | Quality Score | Status |
| ---------- | -------- | -------------- | ------------- | -------------- |
|------|------|----------------|---------------|--------|
| 2026-01-01 | Baseline | 45% | - | Starting point |
| 2026-01-08 | Epic 1 | 78% | 72 | Improving |
| 2026-01-15 | Epic 2 | 92% | 84 | Near target |

View File

@ -290,84 +290,137 @@ burn-in:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
```
#### Burn-In Testing
#### Helper Scripts
**Option 1: Classic Burn-In (Playwright Built-In)**
TEA generates shell scripts for CI and local development.
**Test Scripts** (`package.json`):
```json
{
"scripts": {
"test": "playwright test",
"test:burn-in": "playwright test --repeat-each=5 --retries=0"
"test:headed": "playwright test --headed",
"test:debug": "playwright test --debug",
"test:smoke": "playwright test --grep @smoke",
"test:critical": "playwright test --grep @critical",
"test:changed": "./scripts/test-changed.sh",
"test:burn-in": "./scripts/burn-in.sh",
"test:report": "playwright show-report",
"ci:local": "./scripts/ci-local.sh"
}
}
```
**How it works:**
- Runs every test 5 times
- Fails if any iteration fails
- Detects flakiness before merge
**Selective Testing Script** (`scripts/test-changed.sh`):
**Use when:** Small test suite, want to run everything multiple times
```bash
#!/bin/bash
# Run only tests for changed files
---
CHANGED_FILES=$(git diff --name-only origin/main...HEAD)
**Option 2: Smart Burn-In (Playwright Utils)**
if echo "$CHANGED_FILES" | grep -q "src/.*\.ts$"; then
echo "Running affected tests..."
npm run test:e2e -- --grep="$(echo $CHANGED_FILES | sed 's/src\///g' | sed 's/\.ts//g')"
else
echo "No test-affecting changes detected"
fi
```
If `tea_use_playwright_utils: true`:
**Burn-In Script** (`scripts/burn-in.sh`):
```bash
#!/bin/bash
# Run tests multiple times to detect flakiness
ITERATIONS=${BURN_IN_ITERATIONS:-5}
FAILURES=0
for i in $(seq 1 $ITERATIONS); do
echo "=== Burn-in iteration $i/$ITERATIONS ==="
if npm test; then
echo "✓ Iteration $i passed"
else
echo "✗ Iteration $i failed"
FAILURES=$((FAILURES + 1))
fi
done
if [ $FAILURES -gt 0 ]; then
echo "❌ Tests failed in $FAILURES/$ITERATIONS iterations"
exit 1
fi
echo "✅ All $ITERATIONS iterations passed"
```
**Local CI Mirror Script** (`scripts/ci-local.sh`):
```bash
#!/bin/bash
# Mirror CI execution locally for debugging
echo "🔍 Running CI pipeline locally..."
# Lint
npm run lint || exit 1
# Tests
npm run test || exit 1
# Burn-in (reduced iterations for local)
for i in {1..3}; do
echo "🔥 Burn-in $i/3"
npm test || exit 1
done
echo "✅ Local CI pipeline passed"
```
**Make scripts executable:**
```bash
chmod +x scripts/*.sh
```
**Alternative: Smart Burn-In with Playwright Utils**
If `tea_use_playwright_utils: true`, you can use git diff-based burn-in:
**scripts/burn-in-changed.ts:**
```typescript
// scripts/burn-in-changed.ts
import { runBurnIn } from '@seontechnologies/playwright-utils/burn-in';
async function main() {
await runBurnIn({
configPath: 'playwright.burn-in.config.ts',
baseBranch: 'main'
});
}
main().catch(console.error);
```
**playwright.burn-in.config.ts:**
```typescript
// playwright.burn-in.config.ts
import type { BurnInConfig } from '@seontechnologies/playwright-utils/burn-in';
const config: BurnInConfig = {
skipBurnInPatterns: ['**/config/**', '**/*.md', '**/*types*'],
burnInTestPercentage: 0.3,
burnIn: { repeatEach: 5, retries: 0 }
burnInTestPercentage: 0.3, // Run 30% of affected tests
burnIn: { repeatEach: 5, retries: 1 }
};
export default config;
```
**package.json:**
```json
{
"scripts": {
"test:burn-in": "tsx scripts/burn-in-changed.ts"
}
}
```
**Benefits over shell script:**
- Only runs tests affected by git changes (faster)
- Smart filtering (skips config, docs, types)
- Volume control (run percentage, not all tests)
**How it works:**
- Git diff analysis (only affected tests)
- Smart filtering (skip configs, docs, types)
- Volume control (run 30% of affected tests)
- Each test runs 5 times
**Use when:** Large test suite, want intelligent selection
---
**Comparison:**
| Feature | Classic Burn-In | Smart Burn-In (PW-Utils) |
|---------|----------------|--------------------------|
| Changed 1 file | Runs all 500 tests × 5 = 2500 runs | Runs 3 affected tests × 5 = 15 runs |
| Config change | Runs all tests | Skips (no tests affected) |
| Type change | Runs all tests | Skips (no runtime impact) |
| Setup | Zero config | Requires config file |
**Recommendation:** Start with classic (simple), upgrade to smart (faster) when suite grows.
**Example:** Changed 1 file → runs 3 affected tests 5 times = 15 runs (not 500 tests × 5 = 2500 runs)
### 6. Configure Secrets

File diff suppressed because it is too large Load Diff

View File

@ -15,9 +15,9 @@ Complete reference for all TEA (Test Architect) configuration options.
**Purpose:** Project-specific configuration values for your repository
**Created By:** BMad installer
**Created By:** `npx bmad-method@alpha install` command
**Status:** Typically gitignored (user-specific values)
**Status:** Gitignored (not committed to repository)
**Usage:** Edit this file to change TEA behavior in your project
@ -155,7 +155,17 @@ Would you like to enable MCP enhancements in Test Architect?
}
```
**Configuration:** Refer to your AI agent's documentation for MCP server setup instructions.
**Configuration Location (IDE-Specific):**
**Cursor:**
```
~/.cursor/config.json or workspace .cursor/config.json
```
**VS Code with Claude:**
```
~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json
```
**Example (Enable):**
```yaml
@ -354,9 +364,9 @@ tea_use_playwright_utils: true
tea_use_mcp_enhancements: false
```
**Individual config (typically gitignored):**
**Individual config (gitignored):**
```yaml
# _bmad/bmm/config.yaml (user adds to .gitignore)
# _bmad/bmm/config.yaml (gitignored)
user_name: John Doe
user_skill_level: expert
tea_use_mcp_enhancements: true # Individual preference
@ -397,7 +407,7 @@ _bmad/bmm/config.yaml.example # Template for team
package.json # Dependencies
```
**Recommended for .gitignore:**
**Gitignore:**
```
_bmad/bmm/config.yaml # User-specific values
.env # Secrets
@ -410,7 +420,8 @@ _bmad/bmm/config.yaml # User-specific values
```markdown
## Setup
1. Install BMad
1. Install BMad:
npx bmad-method@alpha install
2. Copy config template:
cp _bmad/bmm/config.yaml.example _bmad/bmm/config.yaml
@ -547,48 +558,48 @@ npx playwright install
## Configuration Examples
### Recommended Setup (Full Stack)
```yaml
# _bmad/bmm/config.yaml
project_name: my-project
user_skill_level: beginner # or intermediate/expert
output_folder: _bmad-output
tea_use_playwright_utils: true # Recommended
tea_use_mcp_enhancements: true # Recommended
```
**Why recommended:**
- Playwright Utils: Production-ready fixtures and utilities
- MCP enhancements: Live browser verification, visual debugging
- Together: The three-part stack (see [Testing as Engineering](/docs/explanation/philosophy/testing-as-engineering.md))
**Prerequisites:**
```bash
npm install -D @seontechnologies/playwright-utils
# Configure MCP servers in IDE (see Enable MCP Enhancements guide)
```
**Best for:** Everyone (beginners learn good patterns from day one)
---
### Minimal Setup (Learning Only)
### Minimal Setup (Defaults)
```yaml
# _bmad/bmm/config.yaml
project_name: my-project
user_skill_level: intermediate
output_folder: _bmad-output
tea_use_playwright_utils: false
tea_use_mcp_enhancements: false
```
**Best for:**
- First-time TEA users (keep it simple initially)
- Quick experiments
- Learning basics before adding integrations
- New projects
- Learning TEA
- Simple testing needs
**Note:** Can enable integrations later as you learn
---
### Advanced Setup (All Features)
```yaml
# _bmad/bmm/config.yaml
project_name: enterprise-app
user_skill_level: expert
output_folder: docs/testing
planning_artifacts: docs/planning
implementation_artifacts: docs/implementation
project_knowledge: docs
tea_use_playwright_utils: true
tea_use_mcp_enhancements: true
```
**Prerequisites:**
```bash
npm install -D @seontechnologies/playwright-utils
# Configure MCP servers in IDE
```
**Best for:**
- Enterprise projects
- Teams with established testing practices
- Projects needing advanced TEA features
---
@ -611,7 +622,7 @@ output_folder: ../../_bmad-output/web
# apps/api/_bmad/bmm/config.yaml
project_name: api-service
output_folder: ../../_bmad-output/api
tea_use_playwright_utils: false # Using vanilla Playwright only
tea_use_playwright_utils: false # API tests don't need it
```
---
@ -631,9 +642,9 @@ planning_artifacts: _bmad-output/planning-artifacts
implementation_artifacts: _bmad-output/implementation-artifacts
project_knowledge: docs
# TEA Configuration (Recommended: Enable both for full stack)
tea_use_playwright_utils: true # Recommended - production-ready utilities
tea_use_mcp_enhancements: true # Recommended - live browser verification
# TEA Configuration
tea_use_playwright_utils: false # Set true if using @seontechnologies/playwright-utils
tea_use_mcp_enhancements: false # Set true if MCP servers configured in IDE
# Languages
communication_language: english
@ -657,6 +668,74 @@ document_output_language: english
---
## FAQ
### When should I enable playwright-utils?
**Enable if:**
- You're using or planning to use `@seontechnologies/playwright-utils`
- You want production-ready fixtures and utilities
- Your team benefits from standardized patterns
- You need utilities like `apiRequest`, `authSession`, `networkRecorder`
**Skip if:**
- You're just learning TEA (keep it simple)
- You have your own fixture library
- You don't need the utilities
### When should I enable MCP enhancements?
**Enable if:**
- You want live browser verification during test generation
- You're debugging complex UI issues
- You want exploratory mode in `*test-design`
- You want recording mode in `*atdd` for accurate selectors
**Skip if:**
- You're new to TEA (adds complexity)
- You don't have MCP servers configured
- Your tests work fine without it
### Can I change config after installation?
**Yes!** Edit `_bmad/bmm/config.yaml` anytime.
**Important:** Start fresh chat after config changes (TEA loads config at workflow start).
### Can I have different configs per branch?
**Yes:**
```bash
# feature branch
git checkout feature/new-testing
# Edit config for experimentation
vim _bmad/bmm/config.yaml
# main branch
git checkout main
# Config reverts to main branch values
```
Config is gitignored, so each branch can have different values.
### How do I share config with team?
**Use config.yaml.example:**
```bash
# Commit template
cp _bmad/bmm/config.yaml _bmad/bmm/config.yaml.example
git add _bmad/bmm/config.yaml.example
git commit -m "docs: add BMad config template"
```
**Team members copy template:**
```bash
cp _bmad/bmm/config.yaml.example _bmad/bmm/config.yaml
# Edit with their values
```
---
## See Also
### How-To Guides

View File

@ -167,10 +167,11 @@ Feature flag testing, contract testing, and API testing patterns.
### Playwright-Utils Integration
Patterns for using `@seontechnologies/playwright-utils` package (9 utilities).
Patterns for using `@seontechnologies/playwright-utils` package (11 utilities).
| Fragment | Description | Key Topics |
|----------|-------------|-----------|
| overview | Playwright Utils installation, design principles, fixture patterns | Getting started, principles, setup |
| [api-request](../../../src/modules/bmm/testarch/knowledge/api-request.md) | Typed HTTP client, schema validation, retry logic | API calls, HTTP, validation |
| [auth-session](../../../src/modules/bmm/testarch/knowledge/auth-session.md) | Token persistence, multi-user, API/browser authentication | Auth patterns, session management |
| [network-recorder](../../../src/modules/bmm/testarch/knowledge/network-recorder.md) | HAR record/playback, CRUD detection for offline testing | Offline testing, network replay |
@ -180,8 +181,9 @@ Patterns for using `@seontechnologies/playwright-utils` package (9 utilities).
| [file-utils](../../../src/modules/bmm/testarch/knowledge/file-utils.md) | CSV/XLSX/PDF/ZIP handling with download support | File validation, exports |
| [burn-in](../../../src/modules/bmm/testarch/knowledge/burn-in.md) | Smart test selection with git diff analysis | CI optimization, selective testing |
| [network-error-monitor](../../../src/modules/bmm/testarch/knowledge/network-error-monitor.md) | Auto-detect HTTP 4xx/5xx errors during tests | Error monitoring, silent failures |
| [fixtures-composition](../../../src/modules/bmm/testarch/knowledge/fixtures-composition.md) | mergeTests composition patterns for combining utilities | Fixture merging, utility composition |
**Note:** `fixtures-composition` is listed under Architecture & Fixtures (general Playwright `mergeTests` pattern, applies to all fixtures).
**Note:** All 11 playwright-utils fragments are in the same `knowledge/` directory as other fragments.
**Used in:** `*framework` (if `tea_use_playwright_utils: true`), `*atdd`, `*automate`, `*test-review`, `*ci`
@ -209,11 +211,51 @@ risk-governance,Risk Governance,Risk scoring and gate decisions,risk;governance,
- `tags` - Searchable tags (semicolon-separated)
- `fragment_file` - Relative path to fragment markdown file
**Fragment Location:** `src/modules/bmm/testarch/knowledge/` (all 33 fragments in single directory)
## Fragment Locations
**Manifest:** `src/modules/bmm/testarch/tea-index.csv`
**Knowledge Base Directory:**
```
src/modules/bmm/testarch/knowledge/
├── api-request.md
├── api-testing-patterns.md
├── auth-session.md
├── burn-in.md
├── ci-burn-in.md
├── component-tdd.md
├── contract-testing.md
├── data-factories.md
├── email-auth.md
├── error-handling.md
├── feature-flags.md
├── file-utils.md
├── fixture-architecture.md
├── fixtures-composition.md
├── intercept-network-call.md
├── log.md
├── network-error-monitor.md
├── network-first.md
├── network-recorder.md
├── nfr-criteria.md
├── playwright-config.md
├── probability-impact.md
├── recurse.md
├── risk-governance.md
├── selector-resilience.md
├── selective-testing.md
├── test-healing-patterns.md
├── test-levels-framework.md
├── test-priorities-matrix.md
├── test-quality.md
├── timing-debugging.md
└── visual-debugging.md
```
---
**All fragments in single directory** (no subfolders)
**Manifest:**
```
src/modules/bmm/testarch/tea-index.csv
```
## Workflow Fragment Loading
@ -329,6 +371,207 @@ Each TEA workflow loads specific fragments:
---
## Key Fragments Explained
### test-quality.md
**What it covers:**
- Execution time limits (< 1.5 minutes)
- Test size limits (< 300 lines)
- No hard waits (waitForTimeout banned)
- No conditionals for flow control
- No try-catch for flow control
- Assertions must be explicit
- Self-cleaning tests for parallel execution
**Why it matters:**
This is the Definition of Done for test quality. All TEA workflows reference this for quality standards.
**Code examples:** 12+
---
### network-first.md
**What it covers:**
- Intercept-before-navigate pattern
- Wait for network responses, not timeouts
- HAR capture for offline testing
- Deterministic waiting strategies
**Why it matters:**
Prevents 90% of test flakiness. Core pattern for reliable E2E tests.
**Code examples:** 15+
---
### fixture-architecture.md
**What it covers:**
- Build pure functions first
- Wrap in framework fixtures second
- Compose with mergeTests
- Enable reusability and testability
**Why it matters:**
Foundation of scalable test architecture. Makes utilities reusable and unit-testable.
**Code examples:** 10+
---
### risk-governance.md
**What it covers:**
- Risk scoring matrix (Probability × Impact)
- Risk categories (TECH, SEC, PERF, DATA, BUS, OPS)
- Gate decision rules (PASS/CONCERNS/FAIL/WAIVED)
- Mitigation planning
**Why it matters:**
Objective, data-driven release decisions. Removes politics from quality gates.
**Code examples:** 5
---
### test-priorities-matrix.md
**What it covers:**
- P0: Critical path (100% coverage required)
- P1: High value (90% coverage target)
- P2: Medium value (50% coverage target)
- P3: Low value (20% coverage target)
- Execution ordering (P0 → P1 → P2 → P3)
**Why it matters:**
Focus testing effort on what matters. Don't waste time on P3 edge cases.
**Code examples:** 8
---
## Using Fragments Directly
### As a Learning Resource
Read fragments to learn patterns:
```bash
# Read fixture architecture pattern
cat src/modules/bmm/testarch/knowledge/fixture-architecture.md
# Read network-first pattern
cat src/modules/bmm/testarch/knowledge/network-first.md
```
### As Team Guidelines
Use fragments as team documentation:
```markdown
# Team Testing Guidelines
## Fixture Architecture
See: src/modules/bmm/testarch/knowledge/fixture-architecture.md
All fixtures must follow the pure function → fixture wrapper pattern.
## Network Patterns
See: src/modules/bmm/testarch/knowledge/network-first.md
All tests must use network-first patterns. No hard waits allowed.
```
### As Code Review Checklist
Reference fragments in code review:
```markdown
## PR Review Checklist
- [ ] Tests follow test-quality.md standards (no hard waits, < 300 lines)
- [ ] Selectors follow selector-resilience.md (prefer getByRole)
- [ ] Network patterns follow network-first.md (wait for responses)
- [ ] Fixtures follow fixture-architecture.md (pure functions)
```
## Fragment Statistics
**Total Fragments:** 33
**Total Size:** ~600 KB (all fragments combined)
**Average Fragment Size:** ~18 KB
**Largest Fragment:** contract-testing.md (~28 KB)
**Smallest Fragment:** burn-in.md (~7 KB)
**By Category:**
- Architecture & Fixtures: 4 fragments
- Data & Setup: 3 fragments
- Network & Reliability: 4 fragments
- Test Execution & CI: 3 fragments
- Quality & Standards: 5 fragments
- Risk & Gates: 3 fragments
- Selectors & Timing: 3 fragments
- Feature Flags & Patterns: 3 fragments
- Playwright-Utils Integration: 8 fragments
**Note:** Statistics may drift with updates. All fragments are in the same `knowledge/` directory.
## Contributing to Knowledge Base
### Adding New Fragments
1. Create fragment in `src/modules/bmm/testarch/knowledge/`
2. Follow existing format (Principle, Rationale, Pattern Examples)
3. Add to `tea-index.csv` with metadata
4. Update workflow instructions to load fragment
5. Test with TEA workflow
### Updating Existing Fragments
1. Edit fragment markdown file
2. Update `tea-index.csv` if metadata changes (line count, examples)
3. Test with affected workflows
4. Ensure no breaking changes to patterns
### Fragment Quality Standards
**Good fragment:**
- Principle stated clearly
- Rationale explains why
- Multiple pattern examples with code
- Good vs bad comparisons
- Self-contained (links to other fragments minimal)
**Example structure:**
```markdown
# Fragment Name
## Principle
[One sentence - what is this pattern?]
## Rationale
[Why use this instead of alternatives?]
## Pattern Examples
### Example 1: Basic Usage
[Code example with explanation]
### Example 2: Advanced Pattern
[Code example with explanation]
## Anti-Patterns
### Don't Do This
[Bad code example]
[Why it's bad]
## Related Patterns
- [Other fragment](../other-fragment.md)
```
## Related
- [TEA Overview](/docs/explanation/features/tea-overview.md) - How knowledge base fits in TEA

View File

@ -51,7 +51,9 @@ You've just explored the features we'll test!
### Install BMad Method
Install BMad (see installation guide for latest command).
```bash
npx bmad-method@alpha install
```
When prompted:
- **Select modules:** Choose "BMM: BMad Method" (press Space, then Enter)
@ -270,7 +272,7 @@ test('should mark todo as complete', async ({ page, apiRequest }) => {
const { status, body: todo } = await apiRequest({
method: 'POST',
path: '/api/todos',
body: { title: 'Complete tutorial' }
body: { title: 'Complete tutorial' } // 'body' not 'data'
});
expect(status).toBe(201);
@ -391,7 +393,7 @@ See [How to Run ATDD](/docs/how-to/workflows/run-atdd.md) for the TDD approach.
**Explanation** (understanding-oriented):
- [TEA Overview](/docs/explanation/features/tea-overview.md) - Complete TEA capabilities
- [Testing as Engineering](/docs/explanation/philosophy/testing-as-engineering.md) - **Why TEA exists** (problem + solution)
- [Testing as Engineering](/docs/explanation/philosophy/testing-as-engineering.md) - Design philosophy
- [Risk-Based Testing](/docs/explanation/tea/risk-based-testing.md) - How risk scoring works
**Reference** (quick lookup):

175
package-lock.json generated
View File

@ -19,6 +19,7 @@
"fs-extra": "^11.3.0",
"glob": "^11.0.3",
"ignore": "^7.0.5",
"inquirer": "^9.3.8",
"js-yaml": "^4.1.0",
"ora": "^5.4.1",
"semver": "^7.6.3",
@ -33,7 +34,6 @@
"devDependencies": {
"@astrojs/sitemap": "^3.6.0",
"@astrojs/starlight": "^0.37.0",
"@clack/prompts": "^0.11.0",
"@eslint/js": "^9.33.0",
"archiver": "^7.0.1",
"astro": "^5.16.0",
@ -755,29 +755,6 @@
"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==",
"dev": true,
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"@clack/core": "0.5.0",
"picocolors": "^1.0.0",
"sisteransi": "^1.0.5"
}
},
"node_modules/@colors/colors": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
@ -2020,6 +1997,36 @@
"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": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
@ -3633,7 +3640,7 @@
"version": "25.0.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.16.0"
@ -4021,7 +4028,6 @@
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
"integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"type-fest": "^0.21.3"
@ -4037,7 +4043,6 @@
"version": "0.21.3",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=10"
@ -5591,6 +5596,12 @@
"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": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@ -5771,6 +5782,15 @@
"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": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@ -8243,6 +8263,22 @@
"@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": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@ -8378,6 +8414,43 @@
"dev": true,
"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": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz",
@ -11496,6 +11569,15 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz",
@ -13218,6 +13300,15 @@
"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": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@ -13242,6 +13333,15 @@
"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": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -13262,6 +13362,12 @@
],
"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": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz",
@ -14135,7 +14241,6 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD"
},
"node_modules/type-check": {
@ -14220,7 +14325,7 @@
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/unicode-properties": {
@ -15153,6 +15258,18 @@
"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": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz",

View File

@ -68,7 +68,6 @@
]
},
"dependencies": {
"@clack/prompts": "^0.11.0",
"@kayvan/markdown-tree-parser": "^1.6.1",
"boxen": "^5.1.2",
"chalk": "^4.1.2",
@ -79,6 +78,7 @@
"fs-extra": "^11.3.0",
"glob": "^11.0.3",
"ignore": "^7.0.5",
"inquirer": "^9.3.8",
"js-yaml": "^4.1.0",
"ora": "^5.4.1",
"semver": "^7.6.3",

View File

@ -3,7 +3,7 @@ const path = require('node:path');
const fs = require('node:fs');
// 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) {
try {
process.stdin.resume();

View File

@ -71,10 +71,14 @@ module.exports = {
console.log(chalk.dim(' • ElevenLabs AI (150+ premium voices)'));
console.log(chalk.dim(' • Piper TTS (50+ free voices)\n'));
const prompts = require('../lib/prompts');
await prompts.text({
const { default: inquirer } = await import('inquirer');
await inquirer.prompt([
{
type: 'input',
name: 'continue',
message: chalk.green('Press Enter to start AgentVibes installer...'),
});
},
]);
console.log('');

View File

@ -4,7 +4,15 @@ const yaml = require('yaml');
const chalk = require('chalk');
const { getProjectRoot, getModulePath } = require('../../../lib/project-root');
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 {
constructor() {
@ -175,6 +183,7 @@ class ConfigCollector {
* @returns {boolean} True if new fields were prompted, false if all fields existed
*/
async collectModuleConfigQuick(moduleName, projectDir, silentMode = true) {
const inquirer = await getInquirer();
this.currentProjectDir = projectDir;
// Load existing config if not already loaded
@ -350,7 +359,7 @@ class ConfigCollector {
// Only show header if we actually have questions
CLIUtils.displayModuleConfigHeader(moduleName, moduleConfig.header, moduleConfig.subheader);
console.log(); // Line break before questions
const promptedAnswers = await prompts.prompt(questions);
const promptedAnswers = await inquirer.prompt(questions);
// Merge prompted answers with static answers
Object.assign(allAnswers, promptedAnswers);
@ -493,6 +502,7 @@ class ConfigCollector {
* @param {boolean} skipCompletion - Skip showing completion message (for early core collection)
*/
async collectModuleConfig(moduleName, projectDir, skipLoadExisting = false, skipCompletion = false) {
const inquirer = await getInquirer();
this.currentProjectDir = projectDir;
// Load existing config if needed and not already loaded
if (!skipLoadExisting && !this.existingConfig) {
@ -587,7 +597,7 @@ class ConfigCollector {
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
let customize = true;
if (moduleName !== 'core') {
const customizeAnswer = await prompts.prompt([
const customizeAnswer = await inquirer.prompt([
{
type: 'confirm',
name: 'customize',
@ -604,7 +614,7 @@ class ConfigCollector {
if (questionsWithoutDefaults.length > 0) {
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);
}
@ -618,7 +628,7 @@ class ConfigCollector {
allAnswers[question.name] = question.default;
}
} else {
const promptedAnswers = await prompts.prompt(questions);
const promptedAnswers = await inquirer.prompt(questions);
Object.assign(allAnswers, promptedAnswers);
}
}
@ -740,7 +750,7 @@ class ConfigCollector {
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
// 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',
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} key - Config key
* @param {Object} item - Config item definition
@ -997,7 +1007,7 @@ class ConfigCollector {
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
if (existingValue !== null && existingValue !== undefined && questionType !== 'list') {
question.default = existingValue;

View File

@ -16,7 +16,6 @@ const { CLIUtils } = require('../../../lib/cli-utils');
const { ManifestGenerator } = require('./manifest-generator');
const { IdeConfigManager } = require('./ide-config-manager');
const { CustomHandler } = require('../custom/handler');
const prompts = require('../../../lib/prompts');
// BMAD installation folder name - this is constant and should never change
const BMAD_FOLDER_NAME = '_bmad';
@ -759,9 +758,6 @@ class Installer {
config.skipIde = toolSelection.skipIde;
const ideConfigurations = toolSelection.configurations;
// Add spacing after prompts before installation progress
console.log('');
if (spinner.isSpinning) {
spinner.text = 'Continuing installation...';
} else {
@ -2143,11 +2139,15 @@ class Installer {
* Private: Prompt for update action
*/
async promptUpdateAction() {
const action = await prompts.select({
const { default: inquirer } = await import('inquirer');
return await inquirer.prompt([
{
type: 'list',
name: 'action',
message: 'What would you like to do?',
choices: [{ name: 'Update existing installation', value: 'update' }],
});
return { action };
},
]);
}
/**
@ -2156,6 +2156,8 @@ class Installer {
* @param {Object} _legacyV4 - Legacy V4 detection result (unused in simplified version)
*/
async handleLegacyV4Migration(_projectDir, _legacyV4) {
const { default: inquirer } = await import('inquirer');
console.log('');
console.log(chalk.yellow.bold('⚠️ Legacy BMAD v4 detected'));
console.log(chalk.yellow('─'.repeat(80)));
@ -2170,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('');
const proceed = await prompts.select({
const { proceed } = await inquirer.prompt([
{
type: 'list',
name: 'proceed',
message: 'What would you like to do?',
choices: [
{
name: 'Exit and clean up manually (recommended)',
value: 'exit',
hint: 'Exit installation',
short: 'Exit installation',
},
{
name: 'Continue with installation anyway',
value: 'continue',
hint: 'Continue',
short: 'Continue',
},
],
default: 'exit',
});
},
]);
if (proceed === 'exit') {
console.log('');
@ -2431,6 +2437,7 @@ class Installer {
console.log(chalk.yellow(`\n⚠️ Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`));
const { default: inquirer } = await import('inquirer');
let keptCount = 0;
let updatedCount = 0;
let removedCount = 0;
@ -2444,12 +2451,12 @@ class Installer {
{
name: 'Keep installed (will not be processed)',
value: 'keep',
hint: 'Keep',
short: 'Keep',
},
{
name: 'Specify new source location',
value: 'update',
hint: 'Update',
short: 'Update',
},
];
@ -2458,27 +2465,33 @@ class Installer {
choices.push({
name: '⚠️ REMOVE module completely (destructive!)',
value: 'remove',
hint: 'Remove',
short: 'Remove',
});
}
const action = await prompts.select({
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: `How would you like to handle "${missing.name}"?`,
choices,
});
},
]);
switch (action) {
case 'update': {
// Use sync validation because @clack/prompts doesn't support async validate
const newSourcePath = await prompts.text({
const { newSourcePath } = await inquirer.prompt([
{
type: 'input',
name: 'newSourcePath',
message: 'Enter the new path to the custom module:',
default: missing.sourcePath,
validate: (input) => {
validate: async (input) => {
if (!input || input.trim() === '') {
return 'Please enter a path';
}
const expandedPath = path.resolve(input.trim());
if (!fs.pathExistsSync(expandedPath)) {
if (!(await fs.pathExists(expandedPath))) {
return 'Path does not exist';
}
// Check if it looks like a valid module
@ -2486,12 +2499,13 @@ class Installer {
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; // clack expects undefined for valid input
return true;
},
});
},
]);
// Update the source in manifest
const resolvedPath = path.resolve(newSourcePath.trim());
@ -2517,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(` Module location: ${path.join(bmadDir, missing.id)}`));
const confirmDelete = await prompts.confirm({
const { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: chalk.red.bold('Are you absolutely sure you want to delete this module?'),
default: false,
});
},
]);
if (confirmDelete) {
const typedConfirm = await prompts.text({
if (confirm) {
const { typedConfirm } = await inquirer.prompt([
{
type: 'input',
name: 'typedConfirm',
message: chalk.red.bold('Type "DELETE" to confirm permanent deletion:'),
validate: (input) => {
if (input !== 'DELETE') {
return chalk.red('You must type "DELETE" exactly to proceed');
}
return; // clack expects undefined for valid input
return true;
},
});
},
]);
if (typedConfirm === 'DELETE') {
// 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)) {
const fsExtra = require('fs-extra');
await fsExtra.remove(modulePath);
console.log(chalk.yellow(` ✓ Deleted module directory: ${path.relative(projectRoot, modulePath)}`));
}
await this.manifest.removeModule(bmadDir, missing.id);
await this.manifest.removeCustomModule(bmadDir, missing.id);
await this.manifest.removeModule(bmadDir, moduleId);
await this.manifest.removeCustomModule(bmadDir, moduleId);
console.log(chalk.yellow(` ✓ Removed from manifest`));
// Also remove from installedModules list
if (installedModules && installedModules.includes(missing.id)) {
const index = installedModules.indexOf(missing.id);
if (installedModules && installedModules.includes(moduleId)) {
const index = installedModules.indexOf(moduleId);
if (index !== -1) {
installedModules.splice(index, 1);
}
@ -2569,7 +2591,7 @@ class Installer {
}
case 'keep': {
keptCount++;
keptModulesWithoutSources.push(missing.id);
keptModulesWithoutSources.push(moduleId);
console.log(chalk.dim(` Module will be kept as-is`));
break;

View File

@ -13,7 +13,6 @@ const {
resolveSubagentFiles,
} = require('./shared/module-injections');
const { getAgentsFromBmad, getAgentsFromDir } = require('./shared/bmad-artifacts');
const prompts = require('../../../lib/prompts');
/**
* Google Antigravity IDE setup handler
@ -27,21 +26,6 @@ class AntigravitySetup extends BaseIdeSetup {
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
* @param {Object} options - Configuration options
@ -73,7 +57,21 @@ class AntigravitySetup extends BaseIdeSetup {
config.subagentChoices = await this.promptSubagentInstallation(injectionConfig.subagents);
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) {
@ -299,7 +297,20 @@ class AntigravitySetup extends BaseIdeSetup {
choices = await this.promptSubagentInstallation(config.subagents);
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,8 +334,13 @@ class AntigravitySetup extends BaseIdeSetup {
* Prompt user for subagent installation preferences
*/
async promptSubagentInstallation(subagentConfig) {
const { default: inquirer } = await import('inquirer');
// First ask if they want to install subagents
const install = await prompts.select({
const { install } = await inquirer.prompt([
{
type: 'list',
name: 'install',
message: 'Would you like to install Antigravity subagents for enhanced functionality?',
choices: [
{ name: 'Yes, install all subagents', value: 'all' },
@ -332,7 +348,8 @@ class AntigravitySetup extends BaseIdeSetup {
{ name: 'No, skip subagent installation', value: 'none' },
],
default: 'all',
});
},
]);
if (install === 'selective') {
// Show list of available subagents with descriptions
@ -344,14 +361,18 @@ class AntigravitySetup extends BaseIdeSetup {
'document-reviewer.md': 'Document quality review',
};
const selected = await prompts.multiselect({
message: `Select subagents to install ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
const { selected } = await inquirer.prompt([
{
type: 'checkbox',
name: 'selected',
message: 'Select subagents to install:',
choices: subagentConfig.files.map((file) => ({
name: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
value: file,
checked: true,
})),
});
},
]);
return { install: 'selective', selected };
}

View File

@ -13,7 +13,6 @@ const {
resolveSubagentFiles,
} = require('./shared/module-injections');
const { getAgentsFromBmad, getAgentsFromDir } = require('./shared/bmad-artifacts');
const prompts = require('../../../lib/prompts');
/**
* Claude Code IDE setup handler
@ -26,21 +25,6 @@ class ClaudeCodeSetup extends BaseIdeSetup {
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
* @param {Object} options - Configuration options
@ -72,7 +56,21 @@ class ClaudeCodeSetup extends BaseIdeSetup {
config.subagentChoices = await this.promptSubagentInstallation(injectionConfig.subagents);
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) {
@ -307,7 +305,20 @@ class ClaudeCodeSetup extends BaseIdeSetup {
choices = await this.promptSubagentInstallation(config.subagents);
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,8 +342,13 @@ class ClaudeCodeSetup extends BaseIdeSetup {
* Prompt user for subagent installation preferences
*/
async promptSubagentInstallation(subagentConfig) {
const { default: inquirer } = await import('inquirer');
// First ask if they want to install subagents
const install = await prompts.select({
const { install } = await inquirer.prompt([
{
type: 'list',
name: 'install',
message: 'Would you like to install Claude Code subagents for enhanced functionality?',
choices: [
{ name: 'Yes, install all subagents', value: 'all' },
@ -340,7 +356,8 @@ class ClaudeCodeSetup extends BaseIdeSetup {
{ name: 'No, skip subagent installation', value: 'none' },
],
default: 'all',
});
},
]);
if (install === 'selective') {
// Show list of available subagents with descriptions
@ -352,14 +369,18 @@ class ClaudeCodeSetup extends BaseIdeSetup {
'document-reviewer.md': 'Document quality review',
};
const selected = await prompts.multiselect({
message: `Select subagents to install ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
options: subagentConfig.files.map((file) => ({
label: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
const { selected } = await inquirer.prompt([
{
type: 'checkbox',
name: 'selected',
message: 'Select subagents to install:',
choices: subagentConfig.files.map((file) => ({
name: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
value: file,
checked: true,
})),
initialValues: subagentConfig.files,
});
},
]);
return { install: 'selective', selected };
}

View File

@ -6,7 +6,6 @@ const { BaseIdeSetup } = require('./_base-ide');
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
const { getTasksFromBmad } = require('./shared/bmad-artifacts');
const prompts = require('../../../lib/prompts');
/**
* Codex setup handler (CLI mode)
@ -22,11 +21,16 @@ class CodexSetup extends BaseIdeSetup {
* @returns {Object} Collected configuration
*/
async collectConfiguration(options = {}) {
const { default: inquirer } = await import('inquirer');
let confirmed = false;
let installLocation = 'global';
while (!confirmed) {
installLocation = await prompts.select({
const { location } = await inquirer.prompt([
{
type: 'list',
name: 'location',
message: 'Where would you like to install Codex CLI prompts?',
choices: [
{
@ -39,7 +43,10 @@ class CodexSetup extends BaseIdeSetup {
},
],
default: 'global',
});
},
]);
installLocation = location;
// Display detailed instructions for the chosen option
console.log('');
@ -50,10 +57,16 @@ class CodexSetup extends BaseIdeSetup {
}
// Confirm the choice
confirmed = await prompts.confirm({
const { proceed } = await inquirer.prompt([
{
type: 'confirm',
name: 'proceed',
message: 'Proceed with this installation option?',
default: true,
});
},
]);
confirmed = proceed;
if (!confirmed) {
console.log(chalk.yellow("\n Let's choose a different installation option.\n"));

View File

@ -2,7 +2,6 @@ const path = require('node:path');
const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk');
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
const prompts = require('../../../lib/prompts');
/**
* GitHub Copilot setup handler
@ -22,12 +21,16 @@ class GitHubCopilotSetup extends BaseIdeSetup {
* @returns {Object} Collected configuration
*/
async collectConfiguration(options = {}) {
const { default: inquirer } = await import('inquirer');
const config = {};
console.log('\n' + chalk.blue(' 🔧 VS Code Settings Configuration'));
console.log(chalk.dim(' GitHub Copilot works best with specific settings\n'));
config.vsCodeConfig = await prompts.select({
const response = await inquirer.prompt([
{
type: 'list',
name: 'configChoice',
message: 'How would you like to configure VS Code settings?',
choices: [
{ name: 'Use recommended defaults (fastest)', value: 'defaults' },
@ -35,10 +38,12 @@ class GitHubCopilotSetup extends BaseIdeSetup {
{ name: 'Skip settings configuration', value: 'skip' },
],
default: 'defaults',
});
},
]);
config.vsCodeConfig = response.configChoice;
if (config.vsCodeConfig === 'manual') {
config.manualSettings = await prompts.prompt([
if (response.configChoice === 'manual') {
config.manualSettings = await inquirer.prompt([
{
type: 'input',
name: 'maxRequests',
@ -47,8 +52,7 @@ class GitHubCopilotSetup extends BaseIdeSetup {
validate: (input) => {
const num = parseInt(input, 10);
if (isNaN(num)) return 'Enter a valid number 1-50';
if (num < 1 || num > 50) return 'Enter a number between 1-50';
return true;
return (num >= 1 && num <= 50) || 'Enter 1-50';
},
},
{

View File

@ -1,432 +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,
});
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,
};

View File

@ -4,21 +4,16 @@ const os = require('node:os');
const fs = require('fs-extra');
const { CLIUtils } = require('./cli-utils');
const { CustomHandler } = require('../installers/lib/custom/handler');
const prompts = require('./prompts');
// Separator class for visual grouping in select/multiselect prompts
// Note: @clack/prompts doesn't support separators natively, they are filtered out
class Separator {
constructor(text = '────────') {
this.line = text;
this.name = text;
// Lazy-load inquirer (ESM module) to avoid ERR_REQUIRE_ESM
let _inquirer = null;
async function getInquirer() {
if (!_inquirer) {
_inquirer = (await import('inquirer')).default;
}
type = 'separator';
return _inquirer;
}
// Separator for choice lists (compatible interface)
const choiceUtils = { Separator };
/**
* UI utilities for the installer
*/
@ -28,6 +23,7 @@ class UI {
* @returns {Object} Installation configuration
*/
async promptInstall() {
const inquirer = await getInquirer();
CLIUtils.displayLogo();
// Display version-specific start message from install-messages.yaml
@ -117,20 +113,26 @@ class UI {
console.log(chalk.yellow('─'.repeat(80)));
console.log('');
const proceed = await prompts.select({
const { proceed } = await inquirer.prompt([
{
type: 'list',
name: 'proceed',
message: 'What would you like to do?',
choices: [
{
name: 'Cancel and do a fresh install (recommended)',
value: 'cancel',
short: 'Cancel installation',
},
{
name: 'Proceed anyway (will attempt update, potentially may fail or have unstable behavior)',
value: 'proceed',
short: 'Proceed with update',
},
],
default: 'cancel',
});
},
]);
if (proceed === 'cancel') {
console.log('');
@ -186,10 +188,14 @@ class UI {
// If Claude Code was selected, ask about TTS
if (claudeCodeSelected) {
const enableTts = await prompts.confirm({
const { enableTts } = await inquirer.prompt([
{
type: 'confirm',
name: 'enableTts',
message: 'Claude Code supports TTS (Text-to-Speech). Would you like to enable it?',
default: false,
});
},
]);
if (enableTts) {
agentVibesConfig = { enabled: true, alreadyInstalled: false };
@ -244,11 +250,18 @@ class UI {
// Common actions
choices.push({ name: 'Modify BMAD Installation', value: 'update' });
actionType = await prompts.select({
const promptResult = await inquirer.prompt([
{
type: 'list',
name: 'actionType',
message: 'What would you like to do?',
choices: choices,
default: choices[0].value,
});
default: choices[0].value, // Use the first option as default
},
]);
// Extract actionType from prompt result
actionType = promptResult.actionType;
// Handle quick update separately
if (actionType === 'quick-update') {
@ -277,10 +290,14 @@ class UI {
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
console.log(chalk.dim(` Found existing modules: ${[...installedModuleIds].join(', ')}`));
const changeModuleSelection = await prompts.confirm({
const { changeModuleSelection } = await inquirer.prompt([
{
type: 'confirm',
name: 'changeModuleSelection',
message: 'Modify official module selection (BMad Method, BMad Builder, Creative Innovation Suite)?',
default: false,
});
},
]);
let selectedModules = [];
if (changeModuleSelection) {
@ -293,10 +310,14 @@ class UI {
// After module selection, ask about custom modules
console.log('');
const changeCustomModules = await prompts.confirm({
const { changeCustomModules } = await inquirer.prompt([
{
type: 'confirm',
name: 'changeCustomModules',
message: 'Modify custom module selection (add, update, or remove custom modules/agents/workflows)?',
default: false,
});
},
]);
let customModuleResult = { selectedCustomModules: [], customContentConfig: { hasCustomContent: false } };
if (changeCustomModules) {
@ -331,10 +352,15 @@ class UI {
let enableTts = false;
if (hasClaudeCode) {
enableTts = await prompts.confirm({
const { enableTts: enable } = await inquirer.prompt([
{
type: 'confirm',
name: 'enableTts',
message: 'Claude Code supports TTS (Text-to-Speech). Would you like to enable it?',
default: false,
});
},
]);
enableTts = enable;
}
// Core config with existing defaults (ask after TTS)
@ -359,10 +385,14 @@ class UI {
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
// Ask about official modules for new installations
const wantsOfficialModules = await prompts.confirm({
const { wantsOfficialModules } = await inquirer.prompt([
{
type: 'confirm',
name: 'wantsOfficialModules',
message: 'Will you be installing any official BMad modules (BMad Method, BMad Builder, Creative Innovation Suite)?',
default: true,
});
},
]);
let selectedOfficialModules = [];
if (wantsOfficialModules) {
@ -371,10 +401,14 @@ class UI {
}
// Ask about custom content
const wantsCustomContent = await prompts.confirm({
const { wantsCustomContent } = await inquirer.prompt([
{
type: 'confirm',
name: 'wantsCustomContent',
message: 'Would you like to install a local custom module (this includes custom agents and workflows also)?',
default: false,
});
},
]);
if (wantsCustomContent) {
customContentConfig = await this.promptCustomContentSource();
@ -425,6 +459,7 @@ class UI {
* @returns {Object} Tool configuration
*/
async promptToolSelection(projectDir, selectedModules) {
const inquirer = await getInquirer();
// Check for existing configured IDEs - use findBmadDir to detect custom folder names
const { Detector } = require('../installers/lib/core/detector');
const { Installer } = require('../installers/lib/core/installer');
@ -442,14 +477,13 @@ class UI {
const preferredIdes = ideManager.getPreferredIdes();
const otherIdes = ideManager.getOtherIdes();
// Build grouped options object for groupMultiselect
const groupedOptions = {};
// Build IDE choices array with separators
const ideChoices = [];
const processedIdes = new Set();
const initialValues = [];
// First, add previously configured IDEs at the top, marked with ✅
if (configuredIdes.length > 0) {
const configuredGroup = [];
ideChoices.push(new inquirer.Separator('── Previously Configured ──'));
for (const ideValue of configuredIdes) {
// Skip empty or invalid IDE values
if (!ideValue || typeof ideValue !== 'string') {
@ -462,71 +496,81 @@ class UI {
const ide = preferredIde || otherIde;
if (ide) {
configuredGroup.push({
label: `${ide.name}`,
ideChoices.push({
name: `${ide.name}`,
value: ide.value,
checked: true, // Previously configured IDEs are checked by default
});
processedIdes.add(ide.value);
initialValues.push(ide.value); // Pre-select configured IDEs
} else {
// Warn about unrecognized IDE (but don't fail)
console.log(chalk.yellow(`⚠️ Previously configured IDE '${ideValue}' is no longer available`));
}
}
if (configuredGroup.length > 0) {
groupedOptions['Previously Configured'] = configuredGroup;
}
}
// Add preferred tools (excluding already processed)
const remainingPreferred = preferredIdes.filter((ide) => !processedIdes.has(ide.value));
if (remainingPreferred.length > 0) {
groupedOptions['Recommended Tools'] = remainingPreferred.map((ide) => {
processedIdes.add(ide.value);
return {
label: `${ide.name}`,
ideChoices.push(new inquirer.Separator('── Recommended Tools ──'));
for (const ide of remainingPreferred) {
ideChoices.push({
name: `${ide.name}`,
value: ide.value,
};
checked: false,
});
processedIdes.add(ide.value);
}
}
// Add other tools (excluding already processed)
const remainingOther = otherIdes.filter((ide) => !processedIdes.has(ide.value));
if (remainingOther.length > 0) {
groupedOptions['Additional Tools'] = remainingOther.map((ide) => ({
label: ide.name,
ideChoices.push(new inquirer.Separator('── Additional Tools ──'));
for (const ide of remainingOther) {
ideChoices.push({
name: ide.name,
value: ide.value,
}));
checked: false,
});
}
}
let selectedIdes = [];
let answers;
let userConfirmedNoTools = false;
// Loop until user selects at least one tool OR explicitly confirms no tools
while (!userConfirmedNoTools) {
selectedIdes = await prompts.groupMultiselect({
message: `Select tools to configure ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
options: groupedOptions,
initialValues: initialValues.length > 0 ? initialValues : undefined,
required: false,
});
answers = await inquirer.prompt([
{
type: 'checkbox',
name: 'ides',
message: 'Select tools to configure:',
choices: ideChoices,
pageSize: 30,
},
]);
// If tools were selected, we're done
if (selectedIdes && selectedIdes.length > 0) {
if (answers.ides && answers.ides.length > 0) {
break;
}
// Warn that no tools were selected - users often miss the spacebar requirement
console.log();
console.log(chalk.red.bold('⚠️ WARNING: No tools were selected!'));
console.log(chalk.red(' You must press SPACE to select items, then ENTER to confirm.'));
console.log(chalk.red(' You must press SPACEBAR to select items, then ENTER to confirm.'));
console.log(chalk.red(' Simply highlighting an item does NOT select it.'));
console.log();
const goBack = await prompts.confirm({
const { goBack } = await inquirer.prompt([
{
type: 'confirm',
name: 'goBack',
message: chalk.yellow('Would you like to go back and select at least one tool?'),
default: true,
});
},
]);
if (goBack) {
// Re-display a message before looping back
@ -538,8 +582,8 @@ class UI {
}
return {
ides: selectedIdes || [],
skipIde: !selectedIdes || selectedIdes.length === 0,
ides: answers.ides || [],
skipIde: !answers.ides || answers.ides.length === 0,
};
}
@ -548,17 +592,23 @@ class UI {
* @returns {Object} Update configuration
*/
async promptUpdate() {
const backupFirst = await prompts.confirm({
const inquirer = await getInquirer();
const answers = await inquirer.prompt([
{
type: 'confirm',
name: 'backupFirst',
message: 'Create backup before updating?',
default: true,
});
const preserveCustomizations = await prompts.confirm({
},
{
type: 'confirm',
name: 'preserveCustomizations',
message: 'Preserve local customizations?',
default: true,
});
},
]);
return { backupFirst, preserveCustomizations };
return answers;
}
/**
@ -567,17 +617,27 @@ class UI {
* @returns {Array} Selected modules
*/
async promptModules(modules) {
const inquirer = await getInquirer();
const choices = modules.map((mod) => ({
name: `${mod.name} - ${mod.description}`,
value: mod.id,
checked: false,
}));
const selectedModules = await prompts.multiselect({
message: `Select modules to add ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
const { selectedModules } = await inquirer.prompt([
{
type: 'checkbox',
name: 'selectedModules',
message: 'Select modules to add:',
choices,
required: true,
});
validate: (answer) => {
if (answer.length === 0) {
return 'You must choose at least one module.';
}
return true;
},
},
]);
return selectedModules;
}
@ -589,10 +649,17 @@ class UI {
* @returns {boolean} User confirmation
*/
async confirm(message, defaultValue = false) {
return await prompts.confirm({
const inquirer = await getInquirer();
const { confirmed } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirmed',
message,
default: defaultValue,
});
},
]);
return confirmed;
}
/**
@ -686,9 +753,10 @@ class UI {
* Get module choices for selection
* @param {Set} installedModuleIds - Currently installed module IDs
* @param {Object} customContentConfig - Custom content configuration
* @returns {Array} Module choices for prompt
* @returns {Array} Module choices for inquirer
*/
async getModuleChoices(installedModuleIds, customContentConfig = null) {
const inquirer = await getInquirer();
const moduleChoices = [];
const isNewInstallation = installedModuleIds.size === 0;
@ -743,9 +811,9 @@ class UI {
if (allCustomModules.length > 0) {
// Add separator for custom content, all custom modules, and official content separator
moduleChoices.push(
new choiceUtils.Separator('── Custom Content ──'),
new inquirer.Separator('── Custom Content ──'),
...allCustomModules,
new choiceUtils.Separator('── Official Content ──'),
new inquirer.Separator('── Official Content ──'),
);
}
@ -769,43 +837,44 @@ class UI {
* @returns {Array} Selected module IDs
*/
async selectModules(moduleChoices, defaultSelections = []) {
// Mark choices as checked based on defaultSelections
const choicesWithDefaults = moduleChoices.map((choice) => ({
...choice,
checked: defaultSelections.includes(choice.value),
}));
const inquirer = await getInquirer();
const moduleAnswer = await inquirer.prompt([
{
type: 'checkbox',
name: 'modules',
message: 'Select modules to install:',
choices: moduleChoices,
default: defaultSelections,
},
]);
const selected = await prompts.multiselect({
message: `Select modules to install ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
choices: choicesWithDefaults,
required: false,
});
const selected = moduleAnswer.modules || [];
return selected || [];
return selected;
}
/**
* Prompt for directory selection
* @returns {Object} Directory answer from prompt
* @returns {Object} Directory answer from inquirer
*/
async promptForDirectory() {
// Use sync validation because @clack/prompts doesn't support async validate
const directory = await prompts.text({
message: 'Installation directory:',
const inquirer = await getInquirer();
return await inquirer.prompt([
{
type: 'input',
name: 'directory',
message: `Installation directory:`,
default: process.cwd(),
placeholder: process.cwd(),
validate: (input) => this.validateDirectorySync(input),
});
// Apply filter logic
let filteredDir = directory;
if (!filteredDir || filteredDir.trim() === '') {
filteredDir = process.cwd();
} else {
filteredDir = this.expandUserPath(filteredDir);
validate: async (input) => this.validateDirectory(input),
filter: (input) => {
// If empty, use the default
if (!input || input.trim() === '') {
return process.cwd();
}
return { directory: filteredDir };
return this.expandUserPath(input);
},
},
]);
}
/**
@ -846,92 +915,45 @@ class UI {
* @returns {boolean} Whether user confirmed
*/
async confirmDirectory(directory) {
const inquirer = await getInquirer();
const dirExists = await fs.pathExists(directory);
if (dirExists) {
const proceed = await prompts.confirm({
message: 'Install to this directory?',
const confirmAnswer = await inquirer.prompt([
{
type: 'confirm',
name: 'proceed',
message: `Install to this directory?`,
default: true,
});
},
]);
if (!proceed) {
if (!confirmAnswer.proceed) {
console.log(chalk.yellow("\nLet's try again with a different path.\n"));
}
return proceed;
return confirmAnswer.proceed;
} else {
// Ask for confirmation to create the directory
const create = await prompts.confirm({
const createConfirm = await inquirer.prompt([
{
type: 'confirm',
name: 'create',
message: `The directory '${directory}' doesn't exist. Would you like to create it?`,
default: false,
});
},
]);
if (!create) {
if (!createConfirm.create) {
console.log(chalk.yellow("\nLet's try again with a different path.\n"));
}
return create;
return createConfirm.create;
}
}
/**
* Validate directory path for installation (sync version for clack prompts)
* @param {string} input - User input path
* @returns {string|undefined} Error message or undefined if valid
*/
validateDirectorySync(input) {
// Allow empty input to use the default
if (!input || input.trim() === '') {
return; // Empty means use default, undefined = valid for clack
}
let expandedPath;
try {
expandedPath = this.expandUserPath(input.trim());
} catch (error) {
return error.message;
}
// Check if the path exists
const pathExists = fs.pathExistsSync(expandedPath);
if (!pathExists) {
// Find the first existing parent directory
const existingParent = this.findExistingParentSync(expandedPath);
if (!existingParent) {
return 'Cannot create directory: no existing parent directory found';
}
// Check if the existing parent is writable
try {
fs.accessSync(existingParent, fs.constants.W_OK);
// Path doesn't exist but can be created - will prompt for confirmation later
return;
} catch {
// Provide a detailed error message explaining both issues
return `Directory '${expandedPath}' does not exist and cannot be created: parent directory '${existingParent}' is not writable`;
}
}
// If it exists, validate it's a directory and writable
const stat = fs.statSync(expandedPath);
if (!stat.isDirectory()) {
return `Path exists but is not a directory: ${expandedPath}`;
}
// Check write permissions
try {
fs.accessSync(expandedPath, fs.constants.W_OK);
} catch {
return `Directory is not writable: ${expandedPath}`;
}
return;
}
/**
* Validate directory path for installation (async version)
* Validate directory path for installation
* @param {string} input - User input path
* @returns {string|true} Error message or true if valid
*/
@ -987,28 +1009,7 @@ class UI {
}
/**
* Find the first existing parent directory (sync version)
* @param {string} targetPath - The path to check
* @returns {string|null} The first existing parent directory, or null if none found
*/
findExistingParentSync(targetPath) {
let currentPath = path.resolve(targetPath);
// Walk up the directory tree until we find an existing directory
while (currentPath !== path.dirname(currentPath)) {
// Stop at root
const parent = path.dirname(currentPath);
if (fs.pathExistsSync(parent)) {
return parent;
}
currentPath = parent;
}
return null; // No existing parent found (shouldn't happen in practice)
}
/**
* Find the first existing parent directory (async version)
* Find the first existing parent directory
* @param {string} targetPath - The path to check
* @returns {string|null} The first existing parent directory, or null if none found
*/
@ -1070,7 +1071,7 @@ class UI {
* @sideeffects None - pure user input collection, no files written
* @edgecases Shows warning if user enables TTS but AgentVibes not detected
* @calledby promptInstall() during installation flow, after core config, before IDE selection
* @calls checkAgentVibesInstalled(), prompts.select(), chalk.green/yellow/dim()
* @calls checkAgentVibesInstalled(), inquirer.prompt(), chalk.green/yellow/dim()
*
* AI NOTE: This prompt is strategically positioned in installation flow:
* - AFTER core config (user_name, etc)
@ -1101,6 +1102,7 @@ class UI {
* - GitHub Issue: paulpreibisch/AgentVibes#36
*/
async promptAgentVibes(projectDir) {
const inquirer = await getInquirer();
CLIUtils.displaySection('🎤 Voice Features', 'Enable TTS for multi-agent conversations');
// Check if AgentVibes is already installed
@ -1112,19 +1114,23 @@ class UI {
console.log(chalk.dim(' AgentVibes not detected'));
}
const enableTts = await prompts.confirm({
const answers = await inquirer.prompt([
{
type: 'confirm',
name: 'enableTts',
message: 'Enable Agents to Speak Out loud (powered by Agent Vibes? Claude Code only currently)',
default: false,
});
default: false, // Default to yes - recommended for best experience
},
]);
if (enableTts && !agentVibesInstalled) {
if (answers.enableTts && !agentVibesInstalled) {
console.log(chalk.yellow('\n ⚠️ AgentVibes not installed'));
console.log(chalk.dim(' Install AgentVibes separately to enable TTS:'));
console.log(chalk.dim(' https://github.com/paulpreibisch/AgentVibes\n'));
}
return {
enabled: enableTts,
enabled: answers.enableTts,
alreadyInstalled: agentVibesInstalled,
};
}
@ -1242,75 +1248,30 @@ class UI {
return existingInstall.ides || [];
}
/**
* Validate custom content path synchronously
* @param {string} input - User input path
* @returns {string|undefined} Error message or undefined if valid
*/
validateCustomContentPathSync(input) {
// Allow empty input to cancel
if (!input || input.trim() === '') {
return; // Allow empty to exit
}
try {
// Expand the path
const expandedPath = this.expandUserPath(input.trim());
// Check if path exists
if (!fs.pathExistsSync(expandedPath)) {
return 'Path does not exist';
}
// Check if it's a directory
const stat = fs.statSync(expandedPath);
if (!stat.isDirectory()) {
return 'Path must be a directory';
}
// Check for module.yaml in the root
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
if (!fs.pathExistsSync(moduleYamlPath)) {
return 'Directory must contain a module.yaml file in the root';
}
// Try to parse the module.yaml to get the module ID
try {
const yaml = require('yaml');
const content = fs.readFileSync(moduleYamlPath, 'utf8');
const moduleData = yaml.parse(content);
if (!moduleData.code) {
return 'module.yaml must contain a "code" field for the module ID';
}
} catch (error) {
return 'Invalid module.yaml file: ' + error.message;
}
return; // Valid
} catch (error) {
return 'Error validating path: ' + error.message;
}
}
/**
* Prompt user for custom content source location
* @returns {Object} Custom content configuration
*/
async promptCustomContentSource() {
const inquirer = await getInquirer();
const customContentConfig = { hasCustomContent: true, sources: [] };
// Keep asking for more sources until user is done
while (true) {
// First ask if user wants to add another module or continue
if (customContentConfig.sources.length > 0) {
const action = await prompts.select({
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: 'Would you like to:',
choices: [
{ name: 'Add another custom module', value: 'add' },
{ name: 'Continue with installation', value: 'continue' },
],
default: 'continue',
});
},
]);
if (action === 'continue') {
break;
@ -1321,11 +1282,57 @@ class UI {
let isValid = false;
while (!isValid) {
// Use sync validation because @clack/prompts doesn't support async validate
const inputPath = await prompts.text({
const { path: inputPath } = await inquirer.prompt([
{
type: 'input',
name: 'path',
message: 'Enter the path to your custom content folder (or press Enter to cancel):',
validate: (input) => this.validateCustomContentPathSync(input),
});
validate: async (input) => {
// Allow empty input to cancel
if (!input || input.trim() === '') {
return true; // Allow empty to exit
}
try {
// Expand the path
const expandedPath = this.expandUserPath(input.trim());
// Check if path exists
if (!(await fs.pathExists(expandedPath))) {
return 'Path does not exist';
}
// Check if it's a directory
const stat = await fs.stat(expandedPath);
if (!stat.isDirectory()) {
return 'Path must be a directory';
}
// Check for module.yaml in the root
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
if (!(await fs.pathExists(moduleYamlPath))) {
return 'Directory must contain a module.yaml file in the root';
}
// Try to parse the module.yaml to get the module ID
try {
const yaml = require('yaml');
const content = await fs.readFile(moduleYamlPath, 'utf8');
const moduleData = yaml.parse(content);
if (!moduleData.code) {
return 'module.yaml must contain a "code" field for the module ID';
}
} catch (error) {
return 'Invalid module.yaml file: ' + error.message;
}
return true;
} catch (error) {
return 'Error validating path: ' + error.message;
}
},
},
]);
// If user pressed Enter without typing anything, exit the loop
if (!inputPath || inputPath.trim() === '') {
@ -1357,10 +1364,14 @@ class UI {
}
// Ask if user wants to add these to the installation
const shouldInstall = await prompts.confirm({
const { shouldInstall } = await inquirer.prompt([
{
type: 'confirm',
name: 'shouldInstall',
message: `Install ${customContentConfig.sources.length} custom module(s) now?`,
default: true,
});
},
]);
if (shouldInstall) {
customContentConfig.selected = true;
@ -1380,6 +1391,7 @@ class UI {
* @returns {Object} Result with selected custom modules and custom content config
*/
async handleCustomModulesInModifyFlow(directory, selectedModules) {
const inquirer = await getInquirer();
// Get existing installation to find custom modules
const { existingInstall } = await this.getExistingInstallation(directory);
@ -1439,11 +1451,16 @@ class UI {
choices.push({ name: 'Add new custom modules', value: 'add' }, { name: 'Cancel (no custom modules)', value: 'cancel' });
}
const customAction = await prompts.select({
message: cachedCustomModules.length > 0 ? 'What would you like to do with custom modules?' : 'Would you like to add custom modules?',
const { customAction } = await inquirer.prompt([
{
type: 'list',
name: 'customAction',
message:
cachedCustomModules.length > 0 ? 'What would you like to do with custom modules?' : 'Would you like to add custom modules?',
choices: choices,
default: cachedCustomModules.length > 0 ? 'keep' : 'add',
});
},
]);
switch (customAction) {
case 'keep': {
@ -1455,18 +1472,21 @@ class UI {
case 'select': {
// Let user choose which to keep
const selectChoices = cachedCustomModules.map((m) => ({
const choices = cachedCustomModules.map((m) => ({
name: `${m.name} ${chalk.gray(`(${m.id})`)}`,
value: m.id,
checked: m.checked,
}));
const keepModules = await prompts.multiselect({
message: `Select custom modules to keep ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
choices: selectChoices,
required: false,
});
result.selectedCustomModules = keepModules || [];
const { keepModules } = await inquirer.prompt([
{
type: 'checkbox',
name: 'keepModules',
message: 'Select custom modules to keep:',
choices: choices,
default: cachedCustomModules.filter((m) => m.checked).map((m) => m.id),
},
]);
result.selectedCustomModules = keepModules;
break;
}
@ -1566,6 +1586,7 @@ class UI {
* @returns {Promise<boolean>} True if user wants to proceed, false if they cancel
*/
async showOldAlphaVersionWarning(installedVersion, currentVersion, bmadFolderName) {
const inquirer = await getInquirer();
const versionInfo = this.checkAlphaVersionAge(installedVersion, currentVersion);
// Also warn if version is unknown or can't be parsed (legacy/unsupported)
@ -1606,20 +1627,26 @@ class UI {
console.log(chalk.yellow('─'.repeat(80)));
console.log('');
const proceed = await prompts.select({
const { proceed } = await inquirer.prompt([
{
type: 'list',
name: 'proceed',
message: 'What would you like to do?',
choices: [
{
name: 'Proceed with update anyway (may have issues)',
value: 'proceed',
short: 'Proceed with update',
},
{
name: 'Cancel (recommended - do a fresh install instead)',
value: 'cancel',
short: 'Cancel installation',
},
],
default: 'cancel',
});
},
]);
if (proceed === 'cancel') {
console.log('');