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 | - | | `*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) | - | | `*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) | | `*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 | | `*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**: Visual debugging + trace analysis for test fixes; **+ Recording**: Verified selectors (UI) + network inspection (API) | | `*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 | - | | `*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 | - | | `*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 | - | | `*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) - [Engagement Models](/docs/explanation/tea/engagement-models.md) - TEA Lite, TEA Solo, TEA Integrated (5 models explained)
**Philosophy:** **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 ## Optional Integrations

View File

@ -594,7 +594,7 @@ Client project 3 (Ad-hoc):
**When:** Adopt BMad Method, want full integration. **When:** Adopt BMad Method, want full integration.
**Steps:** **Steps:**
1. Install BMad Method (see installation guide) 1. Install BMad Method (`npx bmad-method@alpha install`)
2. Run planning workflows (PRD, architecture) 2. Run planning workflows (PRD, architecture)
3. Integrate TEA into Phase 3 (system-level test design) 3. Integrate TEA into Phase 3 (system-level test design)
4. Follow integrated lifecycle (per epic workflows) 4. Follow integrated lifecycle (per epic workflows)
@ -690,7 +690,7 @@ Each model uses different TEA workflows. See:
**Use-Case Guides:** **Use-Case Guides:**
- [Using TEA with Existing Tests](/docs/how-to/brownfield/use-tea-with-existing-tests.md) - Model 5: Brownfield - [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:** **All Workflow Guides:**
- [How to Run Test Design](/docs/how-to/workflows/run-test-design.md) - Used in TEA Solo and Integrated - [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) // Use API request fixture (matches pure function signature)
const { status, body } = await apiRequest({ const { status, body } = await apiRequest({
method: 'PATCH', method: 'PATCH',
url: '/api/profile', url: '/api/profile', // Pure function uses 'url' (not 'path')
data: { name: 'New Name' }, data: { name: 'New Name' }, // Pure function uses 'data' (not 'body')
headers: { Authorization: `Bearer ${authToken}` } headers: { Authorization: `Bearer ${authToken}` }
}); });

View File

@ -484,31 +484,22 @@ await page.waitForSelector('.success', { timeout: 30000 });
All developers: All developers:
```typescript ```typescript
import { test } from '@seontechnologies/playwright-utils/fixtures'; import { test } from '@seontechnologies/playwright-utils/recurse/fixtures';
test('job completion', async ({ apiRequest, recurse }) => { test('job completion', async ({ page, recurse }) => {
// Start async job await page.click('button');
const { body: job } = await apiRequest({
method: 'POST', const result = await recurse({
path: '/api/jobs' fn: () => apiRequest({ method: 'GET', path: '/api/job/123' }),
predicate: (job) => job.status === 'complete',
timeout: 30000
}); });
// Poll until complete (correct API: command, predicate, options) expect(result.status).toBe('complete');
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');
}); });
``` ```
**Result:** Consistent pattern using correct playwright-utils API (command, predicate, options). **Result:** Consistent pattern, established best practice.
## Technical Implementation ## Technical Implementation
@ -529,7 +520,7 @@ For details on the knowledge base index, see:
**Overview:** **Overview:**
- [TEA Overview](/docs/explanation/features/tea-overview.md) - Knowledge base in workflows - [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 ## Practical Guides

View File

@ -125,40 +125,6 @@ test('should load dashboard data', async ({ page }) => {
- No fixed timeout (fast when API is fast) - No fixed timeout (fast when API is fast)
- Validates API response (catch backend errors) - 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 ### Intercept-Before-Navigate Pattern
**Key insight:** Set up wait BEFORE triggering the action. **Key insight:** Set up wait BEFORE triggering the action.
@ -230,7 +196,6 @@ sequenceDiagram
### TEA Generates Network-First Tests ### TEA Generates Network-First Tests
**Vanilla Playwright:**
```typescript ```typescript
// When you run *atdd or *automate, TEA generates: // 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 ### TEA Reviews for Hard Waits
When you run `*test-review`: When you run `*test-review`:
@ -318,7 +252,6 @@ await responsePromise; // ✅
### Basic Response Wait ### Basic Response Wait
**Vanilla Playwright:**
```typescript ```typescript
// Wait for any successful response // Wait for any successful response
const promise = page.waitForResponse(resp => resp.ok()); const promise = page.waitForResponse(resp => resp.ok());
@ -326,23 +259,8 @@ await page.click('button');
await promise; 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 ### Specific URL Match
**Vanilla Playwright:**
```typescript ```typescript
// Wait for specific endpoint // Wait for specific endpoint
const promise = page.waitForResponse( const promise = page.waitForResponse(
@ -352,21 +270,8 @@ await page.goto('/user/123');
await promise; 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 ### Method + Status Match
**Vanilla Playwright:**
```typescript ```typescript
// Wait for POST that returns 201 // Wait for POST that returns 201
const promise = page.waitForResponse( const promise = page.waitForResponse(
@ -379,24 +284,8 @@ await page.click('button[type="submit"]');
await promise; 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 ### Multiple Responses
**Vanilla Playwright:**
```typescript ```typescript
// Wait for multiple API calls // Wait for multiple API calls
const [usersResp, postsResp] = await Promise.all([ const [usersResp, postsResp] = await Promise.all([
@ -409,29 +298,8 @@ const users = await usersResp.json();
const posts = await postsResp.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 ### Validate Response Data
**Vanilla Playwright:**
```typescript ```typescript
// Verify API response before asserting UI // Verify API response before asserting UI
const promise = page.waitForResponse( const promise = page.waitForResponse(
@ -451,28 +319,6 @@ expect(order.total).toBeGreaterThan(0);
await expect(page.locator('.order-confirmation')).toContainText(order.id); 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 ## Advanced Patterns
### HAR Recording for Offline Testing ### HAR Recording for Offline Testing
@ -635,36 +481,6 @@ test('dashboard loads data', async ({ page }) => {
- Validates UI matches API (catch frontend bugs) - Validates UI matches API (catch frontend bugs)
- Works in any environment (local, CI, staging) - 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 ### Form Submission
**Traditional (Flaky):** **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 ## Common Misconceptions
### "I Already Use waitForSelector" ### "I Already Use waitForSelector"
@ -758,57 +545,29 @@ await page.waitForSelector('.success'); // Then validate UI
### "Too Much Boilerplate" ### "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 ```typescript
test('test 1', async ({ page }) => { // Create reusable fixture
const promise = page.waitForResponse( export const test = base.extend({
resp => resp.url().includes('/api/submit') && resp.ok() 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 }) => { // Use in tests
const promise = page.waitForResponse( test('test', async ({ page, waitForApi }) => {
resp => resp.url().includes('/api/load') && resp.ok() const promise = waitForApi('/api/submit'); // Get promise
); await page.click('button'); // Trigger action
await page.click('button'); await promise; // Wait for response
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
}); });
``` ```
**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 ## Technical Implementation
For detailed network-first patterns, see the knowledge base: 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 - [How to Run NFR Assessment](/docs/how-to/workflows/run-nfr-assess.md) - NFR risk assessment
**Use-Case Guides:** **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 ## Reference

View File

@ -107,7 +107,7 @@ test('flaky test', async ({ page }) => {
}); });
``` ```
**Good Example (Vanilla Playwright):** **Good Example:**
```typescript ```typescript
test('deterministic test', async ({ page }) => { test('deterministic test', async ({ page }) => {
const responsePromise = page.waitForResponse( const responsePromise = page.waitForResponse(
@ -126,43 +126,12 @@ test('deterministic test', async ({ page }) => {
}); });
``` ```
**With Playwright Utils (Even Cleaner):** **Why it works:**
```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:**
- Waits for actual event (network response) - Waits for actual event (network response)
- No conditionals (behavior is deterministic) - No conditionals (behavior is deterministic)
- Assertions fail loudly (no silent failures) - Assertions fail loudly (no silent failures)
- Same result every run (deterministic) - 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) ### 2. Isolation (No Dependencies)
**Rule:** Test runs independently, no shared state. **Rule:** Test runs independently, no shared state.
@ -183,7 +152,7 @@ test('create user', async ({ apiRequest }) => {
const { body } = await apiRequest({ const { body } = await apiRequest({
method: 'POST', method: 'POST',
path: '/api/users', 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 userId = body.id; // Store in global
}); });
@ -193,7 +162,7 @@ test('update user', async ({ apiRequest }) => {
await apiRequest({ await apiRequest({
method: 'PATCH', method: 'PATCH',
path: `/api/users/${userId}`, path: `/api/users/${userId}`,
body: { name: 'Updated' } body: { name: 'Updated' } // 'body' not 'data'
}); });
// No cleanup - leaves user in database // No cleanup - leaves user in database
}); });
@ -244,7 +213,7 @@ test('should update user profile', async ({ apiRequest }) => {
const { status: createStatus, body: user } = await apiRequest({ const { status: createStatus, body: user } = await apiRequest({
method: 'POST', method: 'POST',
path: '/api/users', path: '/api/users',
body: { email: testEmail, name: faker.person.fullName() } body: { email: testEmail, name: faker.person.fullName() } // 'body' not 'data'
}); });
expect(createStatus).toBe(201); expect(createStatus).toBe(201);
@ -253,7 +222,7 @@ test('should update user profile', async ({ apiRequest }) => {
const { status, body: updated } = await apiRequest({ const { status, body: updated } = await apiRequest({
method: 'PATCH', method: 'PATCH',
path: `/api/users/${user.id}`, path: `/api/users/${user.id}`,
body: { name: 'Updated Name' } body: { name: 'Updated Name' } // 'body' not 'data'
}); });
expect(status).toBe(200); expect(status).toBe(200);
@ -443,7 +412,7 @@ test('slow test', async ({ page }) => {
**Total time:** 3+ minutes (95 seconds wasted on hard waits) **Total time:** 3+ minutes (95 seconds wasted on hard waits)
**Good Example (Vanilla Playwright):** **Good Example:**
```typescript ```typescript
// ✅ Fast test (< 10 seconds) // ✅ Fast test (< 10 seconds)
test('fast test', async ({ page }) => { 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) **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's Quality Scoring
TEA reviews tests against these standards in `*test-review`: TEA reviews tests against these standards in `*test-review`:
@ -894,7 +821,7 @@ For detailed test quality patterns, see:
**Use-Case Guides:** **Use-Case Guides:**
- [Using TEA with Existing Tests](/docs/how-to/brownfield/use-tea-with-existing-tests.md) - Improve legacy quality - [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 ## 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 ```typescript
import { test } from '@seontechnologies/playwright-utils/fixtures'; import { test } from '@seontechnologies/playwright-utils/network-error-monitor/fixtures';
import { expect } from '@playwright/test';
test('checkout completes', async ({ page, interceptNetworkCall }) => { // That's it! Just import the fixture - monitoring is automatic
// Use interceptNetworkCall for cleaner network interception test('checkout completes', async ({ page }) => {
const checkoutCall = interceptNetworkCall({ const checkoutPromise = page.waitForResponse(
method: 'POST', resp => resp.url().includes('/api/checkout') && resp.ok()
url: '**/api/checkout' );
});
await page.click('button[name="checkout"]'); 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'); expect(order.status).toBe('confirmed');
// Validate UI
await expect(page.locator('.confirmation')).toBeVisible(); 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:** **Playwright Utils Benefits:**
- `interceptNetworkCall` for cleaner network interception - Auto-enabled by fixture import (zero code changes)
- Automatic JSON parsing (`responseJson` ready to use) - Catches silent backend errors (500, 503, 504)
- No manual `await response.json()` - Test fails even if UI shows cached/stale success message
- Glob pattern matching (`**/api/checkout`) - Structured error report in test output
- Cleaner, more maintainable code - No manual error checking needed
**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).
**Priority 3: P1 Requirements** **Priority 3: P1 Requirements**
``` ```
@ -359,7 +353,7 @@ test.skip('flaky test - needs fixing', async ({ page }) => {
# Quarantined Tests # Quarantined Tests
| Test | Reason | Owner | Target Fix Date | | Test | Reason | Owner | Target Fix Date |
| ------------------- | -------------------------- | -------- | --------------- | |------|--------|-------|----------------|
| checkout.spec.ts:45 | Hard wait causes flakiness | QA Team | 2026-01-20 | | 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 | | profile.spec.ts:28 | Conditional flow control | Dev Team | 2026-01-25 |
``` ```
@ -405,7 +399,7 @@ Same process
# Test Suite Status # Test Suite Status
| Directory | Tests | Quality Score | Status | Notes | | Directory | Tests | Quality Score | Status | Notes |
| ------------------ | ----- | ------------- | ------------- | -------------- | |-----------|-------|---------------|--------|-------|
| tests/auth/ | 15 | 85/100 | ✅ Modernized | Week 1 cleanup | | tests/auth/ | 15 | 85/100 | ✅ Modernized | Week 1 cleanup |
| tests/api/ | 32 | 78/100 | ⚠️ In Progress | Week 2 | | tests/api/ | 32 | 78/100 | ⚠️ In Progress | Week 2 |
| tests/e2e/ | 28 | 62/100 | ❌ Legacy | Week 3 planned | | tests/e2e/ | 28 | 62/100 | ❌ Legacy | Week 3 planned |
@ -471,26 +465,15 @@ Incremental changes = lower risk
**Solution:** **Solution:**
``` ```
1. Configure parallel execution (shard tests across workers) 1. Run *ci to add selective testing
2. Add selective testing (run only affected tests on PR) 2. Run only affected tests on PR
3. Run full suite nightly only 3. Run full suite nightly
4. Optimize slow tests (remove hard waits, improve selectors) 4. Parallelize with sharding
Before: 4 hours sequential Before: 4 hours sequential
After: 15 minutes with sharding + selective testing 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" ### "We Have Tests But They Always Fail"
**Problem:** Tests are so flaky they're ignored. **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 *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 ## Related Guides
**Workflow 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 ## When to Use This
**For UI Testing:**
- Want exploratory mode in `*test-design` (browser-based UI discovery) - 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) - Want healing mode in `*automate` (fix tests with visual debugging)
- Debugging complex UI issues
- Need accurate selectors from actual DOM - 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:** **Don't use if:**
- You're new to TEA (adds complexity)
- You don't have MCP servers configured - You don't have MCP servers configured
- Your tests work fine without it
- You're testing APIs only (no UI)
## Prerequisites ## 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. 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 ```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 ```yaml
# _bmad/bmm/config.yaml
tea_use_mcp_enhancements: true 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 ## How MCP Enhances TEA Workflows
@ -152,14 +162,16 @@ I'll design tests for these interactions."
**Without MCP:** **Without MCP:**
- TEA generates selectors from best practices - 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):** **With MCP:**
TEA verifies selectors with live browser:
**For UI Tests:**
``` ```
[TEA navigates to /login with live browser] "Let me verify the login form selectors"
[Inspects actual form fields]
[TEA navigates to /login]
[Inspects form fields]
"I see: "I see:
- Email input has label 'Email Address' (not 'Email') - 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." I'll use these exact selectors."
``` ```
**For API Tests:** **Generated test:**
``` ```typescript
[TEA analyzes trace files from test runs] await page.getByLabel('Email Address').fill('test@example.com');
[Inspects network requests/responses] await page.getByLabel('Your Password').fill('password');
await page.getByRole('button', { name: 'Sign In' }).click();
"I see the API returns: // Selectors verified against actual DOM
- POST /api/login → 200 with { token, userId }
- Response time: 150ms
- Required headers: Content-Type, Authorization
I'll validate these in tests."
``` ```
**Benefits:** **Benefits:**
- UI: Accurate selectors from real DOM - Accurate selectors from real DOM
- API: Validated request/response patterns from trace - Tests work on first run
- Both: Tests work on first run - No trial-and-error selector debugging
### *automate: Healing + Recording Modes ### *automate: Healing Mode
**Without MCP:** **Without MCP:**
- TEA analyzes test code only - TEA analyzes test code only
- Suggests fixes based on static analysis - Suggests fixes based on static analysis
- Generates tests from documentation/code - Can't verify fixes work
**With MCP:** **With MCP:**
TEA uses visual debugging:
**Healing Mode (UI + API):**
``` ```
"This test is failing. Let me debug with trace viewer"
[TEA opens trace file] [TEA opens trace file]
[Analyzes screenshots + network tab] [Analyzes screenshots]
[Identifies selector changed]
UI failures: "Button selector changed from 'Save' to 'Save Changes'" "The button selector changed from 'Save' to 'Save Changes'
API failures: "Response structure changed, expected {id} got {userId}" I'll update the test and verify it works"
[TEA makes fixes] [TEA makes fix]
[Verifies with trace analysis] [Runs test with MCP]
``` [Confirms test passes]
**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]
``` ```
**Benefits:** **Benefits:**
- Visual debugging + trace analysis (not just UI) - Visual debugging during healing
- Verified selectors (UI) + network patterns (API) - Verified fixes (not guesses)
- Tests verified against actual application behavior - Faster resolution
## Usage Examples ## Usage Examples
@ -289,6 +290,43 @@ Fixing selector and verifying...
Updated test with corrected selector. 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 ## Troubleshooting
### MCP Servers Not Running ### MCP Servers Not Running
@ -395,6 +433,107 @@ tea_use_mcp_enhancements: true
tea_use_mcp_enhancements: false 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 ## Related Guides
**Getting Started:** **Getting Started:**

View File

@ -62,7 +62,7 @@ Edit `_bmad/bmm/config.yaml`:
tea_use_playwright_utils: true 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 ### Step 3: Verify Installation
@ -175,16 +175,13 @@ Reviews against playwright-utils best practices:
### *ci Workflow ### *ci Workflow
**Without Playwright Utils:** **Without Playwright Utils:**
- Parallel sharding Basic CI configuration
- Burn-in loops (basic shell scripts)
- CI triggers (PR, push, schedule)
- Artifact collection
**With Playwright Utils:** **With Playwright Utils:**
Enhanced with smart testing: Enhanced CI with:
- Burn-in utility (git diff-based, volume control) - Burn-in utility for smart test selection
- Selective testing (skip config/docs/types changes) - Selective testing based on git diff
- Test prioritization by file changes - Test prioritization
## Available Utilities ## Available Utilities
@ -192,18 +189,6 @@ Enhanced with smart testing:
Typed HTTP client with schema validation. 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:** **Usage:**
```typescript ```typescript
import { test } from '@seontechnologies/playwright-utils/api-request/fixtures'; import { test } from '@seontechnologies/playwright-utils/api-request/fixtures';
@ -221,7 +206,7 @@ test('should create user', async ({ apiRequest }) => {
method: 'POST', method: 'POST',
path: '/api/users', // Note: 'path' not 'url' path: '/api/users', // Note: 'path' not 'url'
body: { name: 'Test User', email: 'test@example.com' } // Note: 'body' not 'data' 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(status).toBe(201);
expect(body.id).toBeDefined(); expect(body.id).toBeDefined();
@ -239,17 +224,6 @@ test('should create user', async ({ apiRequest }) => {
Authentication session management with token persistence. 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:** **Usage:**
```typescript ```typescript
import { test } from '@seontechnologies/playwright-utils/auth-session/fixtures'; 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. 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:** **Usage:**
```typescript ```typescript
import { test } from '@seontechnologies/playwright-utils/network-recorder/fixtures'; 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. 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:** **Usage:**
```typescript ```typescript
import { test } from '@seontechnologies/playwright-utils/fixtures'; 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). 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:** **Usage:**
```typescript ```typescript
import { test } from '@seontechnologies/playwright-utils/fixtures'; 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. 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:** **Usage:**
```typescript ```typescript
import { log } from '@seontechnologies/playwright-utils'; import { log } from '@seontechnologies/playwright-utils';
@ -466,24 +396,13 @@ test('should login', async ({ page }) => {
- Direct import (no fixture needed for basic usage) - Direct import (no fixture needed for basic usage)
- Structured logs in test reports - Structured logs in test reports
- `.step()` shows in Playwright UI - `.step()` shows in Playwright UI
- Logs objects seamlessly (no special handling needed) - Supports object logging with `.debug()`
- Trace test execution - Trace test execution
### file-utils ### file-utils
Read and validate CSV, PDF, XLSX, ZIP files. 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:** **Usage:**
```typescript ```typescript
import { handleDownload, readCSV } from '@seontechnologies/playwright-utils/file-utils'; 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. 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:** **Usage:**
```typescript ```typescript
// scripts/burn-in-changed.ts // scripts/burn-in-changed.ts
@ -582,7 +490,6 @@ export default config;
``` ```
**Benefits:** **Benefits:**
- **Ensure flake-free tests upfront** - Never deal with test flake again
- Smart filtering (skip config, types, docs changes) - Smart filtering (skip config, types, docs changes)
- Volume control (run percentage of affected tests) - Volume control (run percentage of affected tests)
- Git diff-based test selection - Git diff-based test selection
@ -592,17 +499,6 @@ export default config;
Automatically detect HTTP 4xx/5xx errors during tests. 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:** **Usage:**
```typescript ```typescript
import { test } from '@seontechnologies/playwright-utils/network-error-monitor/fixtures'; import { test } from '@seontechnologies/playwright-utils/network-error-monitor/fixtures';
@ -644,76 +540,98 @@ test.describe('error handling',
**Benefits:** **Benefits:**
- Auto-enabled (zero setup) - Auto-enabled (zero setup)
- Catches silent backend failures (500, 503, 504) - Catches silent backend failures
- **Prevents domino effect** (limits cascading failures from one bad endpoint) - Opt-out with annotations
- Opt-out with annotations for validation tests - Structured error reporting
- Structured error reporting (JSON artifacts)
## Fixture Composition ## Fixture Composition
**Option 1: Use Package's Combined Fixtures (Simplest)** Combine utilities using `mergeTests`:
**Option 1: Use Combined Fixtures (Simplest)**
```typescript ```typescript
// Import all utilities at once // Import all utilities at once
import { test } from '@seontechnologies/playwright-utils/fixtures'; import { test } from '@seontechnologies/playwright-utils/fixtures';
import { log } from '@seontechnologies/playwright-utils'; import { log } from '@seontechnologies/playwright-utils';
import { expect } from '@playwright/test'; import { expect } from '@playwright/test';
test('api test', async ({ apiRequest, interceptNetworkCall }) => { test('full test', async ({ apiRequest, authToken, interceptNetworkCall }) => {
await log.info('Fetching users'); await log.info('Starting test'); // log is direct import
const { status, body } = await apiRequest({ const { status, body } = await apiRequest({
method: 'GET', method: 'GET',
path: '/api/users' path: '/api/data',
headers: { Authorization: `Bearer ${authToken}` }
}); });
await log.info('Data fetched', body);
expect(status).toBe(200); 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 ```typescript
import { test as base, mergeTests } from '@playwright/test'; import { test as base } from '@playwright/test';
import { test as apiRequest } from '@seontechnologies/playwright-utils/api-request/fixtures'; import { mergeTests } from '@playwright/test';
import { test as interceptNetworkCall } from '@seontechnologies/playwright-utils/intercept-network-call/fixtures'; import { test as apiRequestFixture } from '@seontechnologies/playwright-utils/api-request/fixtures';
import { test as networkErrorMonitor } from '@seontechnologies/playwright-utils/network-error-monitor/fixtures'; import { test as recurseFixture } from '@seontechnologies/playwright-utils/recurse/fixtures';
import { log } from '@seontechnologies/playwright-utils'; import { log } from '@seontechnologies/playwright-utils';
// Merge only what you need // Merge only the fixtures you need
export const test = mergeTests( export const test = mergeTests(
base, apiRequestFixture,
apiRequest, recurseFixture
interceptNetworkCall,
networkErrorMonitor
); );
export const expect = base.expect; export { expect } from '@playwright/test';
export { log };
```
**File 2: tests/api/users.spec.ts** // Use merged utilities in tests
```typescript test('selective test', async ({ apiRequest, recurse }) => {
import { test, expect, log } from '../support/merged-fixtures'; await log.info('Starting test'); // log is direct import, not fixture
test('api test', async ({ apiRequest, interceptNetworkCall }) => {
await log.info('Fetching users');
const { status, body } = await apiRequest({ const { status, body } = await apiRequest({
method: 'GET', method: 'GET',
path: '/api/users' path: '/api/data'
}); });
await log.info('Data fetched', body);
expect(status).toBe(200); expect(status).toBe(200);
}); });
``` ```
**Contrast:** **Note:** `log` is a direct utility (not a fixture), so import it separately.
- Option 1: All utilities available, zero setup
- Option 2: Pick utilities you need, one central file
**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 ## Troubleshooting
@ -780,6 +698,47 @@ expect(status).toBe(200);
## Migration Guide ## 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 ## Related Guides
**Getting Started:** **Getting Started:**
@ -796,7 +755,6 @@ expect(status).toBe(200);
## Understanding the Concepts ## 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 - [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 - [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 - [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) - E2E tests (browser-based, full user journey)
- API tests (backend only, faster) - API tests (backend only, faster)
- Component tests (UI components in isolation) - 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 ### Component Testing by Framework
TEA generates component tests using framework-appropriate tools: TEA generates component tests using framework-appropriate tools:
| Your Framework | Component Testing Tool | | Your Framework | Component Testing Tool |
| -------------- | ------------------------------------------- | |----------------|----------------------|
| **Cypress** | Cypress Component Testing (*.cy.tsx) | | **Cypress** | Cypress Component Testing (*.cy.tsx) |
| **Playwright** | Vitest + React Testing Library (*.test.tsx) | | **Playwright** | Vitest + React Testing Library (*.test.tsx) |
@ -188,7 +190,7 @@ test.describe('Profile API', () => {
const { status, body } = await apiRequest({ const { status, body } = await apiRequest({
method: 'PATCH', method: 'PATCH',
path: '/api/profile', path: '/api/profile',
body: { body: { // 'body' not 'data'
name: 'Updated Name', name: 'Updated Name',
email: 'updated@example.com' email: 'updated@example.com'
} }
@ -203,7 +205,7 @@ test.describe('Profile API', () => {
const { status, body } = await apiRequest({ const { status, body } = await apiRequest({
method: 'PATCH', method: 'PATCH',
path: '/api/profile', path: '/api/profile',
body: { email: 'invalid-email' } body: { email: 'invalid-email' } // 'body' not 'data'
}); });
expect(status).toBe(400); expect(status).toBe(400);
@ -224,28 +226,52 @@ test.describe('Profile API', () => {
```typescript ```typescript
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
test('should edit and save profile', async ({ page }) => { test.describe('Profile Page', () => {
test.beforeEach(async ({ page }) => {
// Login first // Login first
await page.goto('/login'); await page.goto('/login');
await page.getByLabel('Email').fill('test@example.com'); await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Password').fill('password123'); await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Sign in' }).click(); await page.getByRole('button', { name: 'Sign in' }).click();
});
// Navigate to profile test('should display current profile information', async ({ page }) => {
await page.goto('/profile'); 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(); await page.getByRole('button', { name: 'Edit Profile' }).click();
// Modify fields
await page.getByLabel('Name').fill('Updated Name'); await page.getByLabel('Name').fill('Updated Name');
await page.getByLabel('Email').fill('updated@example.com');
// Save
await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('button', { name: 'Save' }).click();
// Verify success // 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 #### Implementation Checklist
TEA also provides an 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 *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 ### Focus on P0/P1 Scenarios
@ -413,6 +444,43 @@ TEA generates deterministic tests by default:
Don't modify these patterns - they prevent flakiness! 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 ## Related Guides
- [How to Run Test Design](/docs/how-to/workflows/run-test-design.md) - Plan before generating - [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 ## 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 - [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 - [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 - [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({ const { status, body } = await apiRequest({
method: 'PATCH', method: 'PATCH',
path: '/api/profile', path: '/api/profile',
body: { name: 'Updated Name', bio: 'Test bio' }, body: { name: 'Updated Name', bio: 'Test bio' }, // 'body' not 'data'
headers: { Authorization: `Bearer ${authToken}` } headers: { Authorization: `Bearer ${authToken}` }
}).validateSchema(ProfileSchema); // Chained validation }).validateSchema(ProfileSchema); // Chained validation
@ -233,7 +233,7 @@ testWithAuth.describe('Profile API', () => {
const { status, body } = await apiRequest({ const { status, body } = await apiRequest({
method: 'PATCH', method: 'PATCH',
path: '/api/profile', path: '/api/profile',
body: { email: 'invalid-email' }, body: { email: 'invalid-email' }, // 'body' not 'data'
headers: { Authorization: `Bearer ${authToken}` } headers: { Authorization: `Bearer ${authToken}` }
}); });
@ -250,31 +250,58 @@ testWithAuth.describe('Profile API', () => {
- Automatic retry for 5xx errors - Automatic retry for 5xx errors
- Less boilerplate (no manual `await response.json()` everywhere) - Less boilerplate (no manual `await response.json()` everywhere)
#### E2E Tests (`tests/e2e/profile.spec.ts`): #### E2E Tests (`tests/e2e/profile-workflow.spec.ts`):
```typescript ```typescript
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
test('should edit profile', async ({ page }) => { test.describe('Profile Management Workflow', () => {
test.beforeEach(async ({ page }) => {
// Login // Login
await page.goto('/login'); await page.goto('/login');
await page.getByLabel('Email').fill('test@example.com'); await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Password').fill('password123'); await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Sign in' }).click(); 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'); 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.getByRole('button', { name: 'Edit Profile' }).click();
await page.getByLabel('Name').fill('New Name'); await page.getByLabel('Name').fill('New Name');
await page.getByRole('button', { name: 'Save' }).click(); await page.getByRole('button', { name: 'Save' }).click();
// Verify success // Verify success
await expect(page.getByText('Profile updated')).toBeVisible(); 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`): #### Fixtures (`tests/support/fixtures/profile.ts`):
**Vanilla Playwright:** **Vanilla Playwright:**
@ -478,7 +505,7 @@ Compare against:
TEA supports component testing using framework-appropriate tools: TEA supports component testing using framework-appropriate tools:
| Your Framework | Component Testing Tool | Tests Location | | Your Framework | Component Testing Tool | Tests Location |
| -------------- | ------------------------------ | ----------------------------------------- | |----------------|----------------------|----------------|
| **Cypress** | Cypress Component Testing | `tests/component/` | | **Cypress** | Cypress Component Testing | `tests/component/` |
| **Playwright** | Vitest + React Testing Library | `tests/component/` or `src/**/*.test.tsx` | | **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. 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 When prompted, select "healing mode" to:
- **Recording mode:** Verify selectors with live browser, capture network requests - 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 ### Generate Tests Incrementally
@ -624,11 +662,21 @@ We already have these tests:
Generate tests for scenarios NOT covered by those files 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 ## Related Guides
@ -638,7 +686,6 @@ Setup: Answer "Yes" to MCPs in BMad installer + configure MCP servers in your ID
## Understanding the Concepts ## 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 - [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 - [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 - [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 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 - [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 ## Understanding the Concepts

View File

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

View File

@ -290,84 +290,137 @@ burn-in:
- if: $CI_PIPELINE_SOURCE == "merge_request_event" - 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 ```json
{ {
"scripts": { "scripts": {
"test": "playwright test", "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:** **Selective Testing Script** (`scripts/test-changed.sh`):
- Runs every test 5 times
- Fails if any iteration fails
- Detects flakiness before merge
**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 ```typescript
// scripts/burn-in-changed.ts
import { runBurnIn } from '@seontechnologies/playwright-utils/burn-in'; import { runBurnIn } from '@seontechnologies/playwright-utils/burn-in';
async function main() {
await runBurnIn({ await runBurnIn({
configPath: 'playwright.burn-in.config.ts', configPath: 'playwright.burn-in.config.ts',
baseBranch: 'main' baseBranch: 'main'
}); });
}
main().catch(console.error);
``` ```
**playwright.burn-in.config.ts:**
```typescript ```typescript
// playwright.burn-in.config.ts
import type { BurnInConfig } from '@seontechnologies/playwright-utils/burn-in'; import type { BurnInConfig } from '@seontechnologies/playwright-utils/burn-in';
const config: BurnInConfig = { const config: BurnInConfig = {
skipBurnInPatterns: ['**/config/**', '**/*.md', '**/*types*'], skipBurnInPatterns: ['**/config/**', '**/*.md', '**/*types*'],
burnInTestPercentage: 0.3, burnInTestPercentage: 0.3, // Run 30% of affected tests
burnIn: { repeatEach: 5, retries: 0 } burnIn: { repeatEach: 5, retries: 1 }
}; };
export default config; export default config;
``` ```
**package.json:** **Benefits over shell script:**
```json - Only runs tests affected by git changes (faster)
{ - Smart filtering (skips config, docs, types)
"scripts": { - Volume control (run percentage, not all tests)
"test:burn-in": "tsx scripts/burn-in-changed.ts"
}
}
```
**How it works:** **Example:** Changed 1 file → runs 3 affected tests 5 times = 15 runs (not 500 tests × 5 = 2500 runs)
- 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.
### 6. Configure Secrets ### 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 **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 **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):** **Example (Enable):**
```yaml ```yaml
@ -354,9 +364,9 @@ tea_use_playwright_utils: true
tea_use_mcp_enhancements: false tea_use_mcp_enhancements: false
``` ```
**Individual config (typically gitignored):** **Individual config (gitignored):**
```yaml ```yaml
# _bmad/bmm/config.yaml (user adds to .gitignore) # _bmad/bmm/config.yaml (gitignored)
user_name: John Doe user_name: John Doe
user_skill_level: expert user_skill_level: expert
tea_use_mcp_enhancements: true # Individual preference tea_use_mcp_enhancements: true # Individual preference
@ -397,7 +407,7 @@ _bmad/bmm/config.yaml.example # Template for team
package.json # Dependencies package.json # Dependencies
``` ```
**Recommended for .gitignore:** **Gitignore:**
``` ```
_bmad/bmm/config.yaml # User-specific values _bmad/bmm/config.yaml # User-specific values
.env # Secrets .env # Secrets
@ -410,7 +420,8 @@ _bmad/bmm/config.yaml # User-specific values
```markdown ```markdown
## Setup ## Setup
1. Install BMad 1. Install BMad:
npx bmad-method@alpha install
2. Copy config template: 2. Copy config template:
cp _bmad/bmm/config.yaml.example _bmad/bmm/config.yaml cp _bmad/bmm/config.yaml.example _bmad/bmm/config.yaml
@ -547,48 +558,48 @@ npx playwright install
## Configuration Examples ## Configuration Examples
### Recommended Setup (Full Stack) ### Minimal Setup (Defaults)
```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)
```yaml ```yaml
# _bmad/bmm/config.yaml # _bmad/bmm/config.yaml
project_name: my-project project_name: my-project
user_skill_level: intermediate
output_folder: _bmad-output output_folder: _bmad-output
tea_use_playwright_utils: false tea_use_playwright_utils: false
tea_use_mcp_enhancements: false tea_use_mcp_enhancements: false
``` ```
**Best for:** **Best for:**
- First-time TEA users (keep it simple initially) - New projects
- Quick experiments - Learning TEA
- Learning basics before adding integrations - 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 # apps/api/_bmad/bmm/config.yaml
project_name: api-service project_name: api-service
output_folder: ../../_bmad-output/api 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 implementation_artifacts: _bmad-output/implementation-artifacts
project_knowledge: docs project_knowledge: docs
# TEA Configuration (Recommended: Enable both for full stack) # TEA Configuration
tea_use_playwright_utils: true # Recommended - production-ready utilities tea_use_playwright_utils: false # Set true if using @seontechnologies/playwright-utils
tea_use_mcp_enhancements: true # Recommended - live browser verification tea_use_mcp_enhancements: false # Set true if MCP servers configured in IDE
# Languages # Languages
communication_language: english 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 ## See Also
### How-To Guides ### How-To Guides

View File

@ -167,10 +167,11 @@ Feature flag testing, contract testing, and API testing patterns.
### Playwright-Utils Integration ### 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 | | 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 | | [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 | | [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 | | [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 | | [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 | | [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 | | [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` **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) - `tags` - Searchable tags (semicolon-separated)
- `fragment_file` - Relative path to fragment markdown file - `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 ## 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 ## Related
- [TEA Overview](/docs/explanation/features/tea-overview.md) - How knowledge base fits in TEA - [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 Method
Install BMad (see installation guide for latest command). ```bash
npx bmad-method@alpha install
```
When prompted: When prompted:
- **Select modules:** Choose "BMM: BMad Method" (press Space, then Enter) - **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({ const { status, body: todo } = await apiRequest({
method: 'POST', method: 'POST',
path: '/api/todos', path: '/api/todos',
body: { title: 'Complete tutorial' } body: { title: 'Complete tutorial' } // 'body' not 'data'
}); });
expect(status).toBe(201); 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): **Explanation** (understanding-oriented):
- [TEA Overview](/docs/explanation/features/tea-overview.md) - Complete TEA capabilities - [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 - [Risk-Based Testing](/docs/explanation/tea/risk-based-testing.md) - How risk scoring works
**Reference** (quick lookup): **Reference** (quick lookup):

175
package-lock.json generated
View File

@ -19,6 +19,7 @@
"fs-extra": "^11.3.0", "fs-extra": "^11.3.0",
"glob": "^11.0.3", "glob": "^11.0.3",
"ignore": "^7.0.5", "ignore": "^7.0.5",
"inquirer": "^9.3.8",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"ora": "^5.4.1", "ora": "^5.4.1",
"semver": "^7.6.3", "semver": "^7.6.3",
@ -33,7 +34,6 @@
"devDependencies": { "devDependencies": {
"@astrojs/sitemap": "^3.6.0", "@astrojs/sitemap": "^3.6.0",
"@astrojs/starlight": "^0.37.0", "@astrojs/starlight": "^0.37.0",
"@clack/prompts": "^0.11.0",
"@eslint/js": "^9.33.0", "@eslint/js": "^9.33.0",
"archiver": "^7.0.1", "archiver": "^7.0.1",
"astro": "^5.16.0", "astro": "^5.16.0",
@ -755,29 +755,6 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@clack/core": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@clack/core/-/core-0.5.0.tgz",
"integrity": "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==",
"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": { "node_modules/@colors/colors": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
@ -2020,6 +1997,36 @@
"url": "https://opencollective.com/libvips" "url": "https://opencollective.com/libvips"
} }
}, },
"node_modules/@inquirer/external-editor": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz",
"integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==",
"license": "MIT",
"dependencies": {
"chardet": "^2.1.1",
"iconv-lite": "^0.7.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@types/node": ">=18"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/@inquirer/figures": {
"version": "1.0.15",
"resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz",
"integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@isaacs/balanced-match": { "node_modules/@isaacs/balanced-match": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
@ -3633,7 +3640,7 @@
"version": "25.0.3", "version": "25.0.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~7.16.0" "undici-types": "~7.16.0"
@ -4021,7 +4028,6 @@
"version": "4.3.2", "version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
"integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"type-fest": "^0.21.3" "type-fest": "^0.21.3"
@ -4037,7 +4043,6 @@
"version": "0.21.3", "version": "0.21.3",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
"dev": true,
"license": "(MIT OR CC0-1.0)", "license": "(MIT OR CC0-1.0)",
"engines": { "engines": {
"node": ">=10" "node": ">=10"
@ -5591,6 +5596,12 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/chardet": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz",
"integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==",
"license": "MIT"
},
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@ -5771,6 +5782,15 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1" "url": "https://github.com/chalk/strip-ansi?sponsor=1"
} }
}, },
"node_modules/cli-width": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
"integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
"license": "ISC",
"engines": {
"node": ">= 12"
}
},
"node_modules/cliui": { "node_modules/cliui": {
"version": "8.0.1", "version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@ -8243,6 +8263,22 @@
"@babel/runtime": "^7.23.2" "@babel/runtime": "^7.23.2"
} }
}, },
"node_modules/iconv-lite": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz",
"integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/ieee754": { "node_modules/ieee754": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@ -8378,6 +8414,43 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/inquirer": {
"version": "9.3.8",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.3.8.tgz",
"integrity": "sha512-pFGGdaHrmRKMh4WoDDSowddgjT1Vkl90atobmTeSmcPGdYiwikch/m/Ef5wRaiamHejtw0cUUMMerzDUXCci2w==",
"license": "MIT",
"dependencies": {
"@inquirer/external-editor": "^1.0.2",
"@inquirer/figures": "^1.0.3",
"ansi-escapes": "^4.3.2",
"cli-width": "^4.1.0",
"mute-stream": "1.0.0",
"ora": "^5.4.1",
"run-async": "^3.0.0",
"rxjs": "^7.8.1",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^6.2.0",
"yoctocolors-cjs": "^2.1.2"
},
"engines": {
"node": ">=18"
}
},
"node_modules/inquirer/node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/iron-webcrypto": { "node_modules/iron-webcrypto": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz",
@ -11496,6 +11569,15 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/mute-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz",
"integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==",
"license": "ISC",
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/nano-spawn": { "node_modules/nano-spawn": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz", "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz",
@ -13218,6 +13300,15 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/run-async": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz",
"integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==",
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/run-parallel": { "node_modules/run-parallel": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@ -13242,6 +13333,15 @@
"queue-microtask": "^1.2.2" "queue-microtask": "^1.2.2"
} }
}, },
"node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/safe-buffer": { "node_modules/safe-buffer": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -13262,6 +13362,12 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/sax": { "node_modules/sax": {
"version": "1.4.3", "version": "1.4.3",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz",
@ -14135,7 +14241,6 @@
"version": "2.8.1", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/type-check": { "node_modules/type-check": {
@ -14220,7 +14325,7 @@
"version": "7.16.0", "version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/unicode-properties": { "node_modules/unicode-properties": {
@ -15153,6 +15258,18 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/yoctocolors-cjs": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz",
"integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zip-stream": { "node_modules/zip-stream": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz",

View File

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

View File

@ -3,7 +3,7 @@ const path = require('node:path');
const fs = require('node:fs'); const fs = require('node:fs');
// Fix for stdin issues when running through npm on Windows // Fix for stdin issues when running through npm on Windows
// Ensures keyboard interaction works properly with CLI prompts // Ensures keyboard interaction works properly with inquirer prompts
if (process.stdin.isTTY) { if (process.stdin.isTTY) {
try { try {
process.stdin.resume(); process.stdin.resume();

View File

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

View File

@ -4,7 +4,15 @@ const yaml = require('yaml');
const chalk = require('chalk'); const chalk = require('chalk');
const { getProjectRoot, getModulePath } = require('../../../lib/project-root'); const { getProjectRoot, getModulePath } = require('../../../lib/project-root');
const { CLIUtils } = require('../../../lib/cli-utils'); const { CLIUtils } = require('../../../lib/cli-utils');
const prompts = require('../../../lib/prompts');
// Lazy-load inquirer (ESM module) to avoid ERR_REQUIRE_ESM
let _inquirer = null;
async function getInquirer() {
if (!_inquirer) {
_inquirer = (await import('inquirer')).default;
}
return _inquirer;
}
class ConfigCollector { class ConfigCollector {
constructor() { constructor() {
@ -175,6 +183,7 @@ class ConfigCollector {
* @returns {boolean} True if new fields were prompted, false if all fields existed * @returns {boolean} True if new fields were prompted, false if all fields existed
*/ */
async collectModuleConfigQuick(moduleName, projectDir, silentMode = true) { async collectModuleConfigQuick(moduleName, projectDir, silentMode = true) {
const inquirer = await getInquirer();
this.currentProjectDir = projectDir; this.currentProjectDir = projectDir;
// Load existing config if not already loaded // Load existing config if not already loaded
@ -350,7 +359,7 @@ class ConfigCollector {
// Only show header if we actually have questions // Only show header if we actually have questions
CLIUtils.displayModuleConfigHeader(moduleName, moduleConfig.header, moduleConfig.subheader); CLIUtils.displayModuleConfigHeader(moduleName, moduleConfig.header, moduleConfig.subheader);
console.log(); // Line break before questions console.log(); // Line break before questions
const promptedAnswers = await prompts.prompt(questions); const promptedAnswers = await inquirer.prompt(questions);
// Merge prompted answers with static answers // Merge prompted answers with static answers
Object.assign(allAnswers, promptedAnswers); Object.assign(allAnswers, promptedAnswers);
@ -493,6 +502,7 @@ class ConfigCollector {
* @param {boolean} skipCompletion - Skip showing completion message (for early core collection) * @param {boolean} skipCompletion - Skip showing completion message (for early core collection)
*/ */
async collectModuleConfig(moduleName, projectDir, skipLoadExisting = false, skipCompletion = false) { async collectModuleConfig(moduleName, projectDir, skipLoadExisting = false, skipCompletion = false) {
const inquirer = await getInquirer();
this.currentProjectDir = projectDir; this.currentProjectDir = projectDir;
// Load existing config if needed and not already loaded // Load existing config if needed and not already loaded
if (!skipLoadExisting && !this.existingConfig) { if (!skipLoadExisting && !this.existingConfig) {
@ -587,7 +597,7 @@ class ConfigCollector {
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName)); console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
let customize = true; let customize = true;
if (moduleName !== 'core') { if (moduleName !== 'core') {
const customizeAnswer = await prompts.prompt([ const customizeAnswer = await inquirer.prompt([
{ {
type: 'confirm', type: 'confirm',
name: 'customize', name: 'customize',
@ -604,7 +614,7 @@ class ConfigCollector {
if (questionsWithoutDefaults.length > 0) { if (questionsWithoutDefaults.length > 0) {
console.log(chalk.dim(`\n Asking required questions for ${moduleName.toUpperCase()}...`)); console.log(chalk.dim(`\n Asking required questions for ${moduleName.toUpperCase()}...`));
const promptedAnswers = await prompts.prompt(questionsWithoutDefaults); const promptedAnswers = await inquirer.prompt(questionsWithoutDefaults);
Object.assign(allAnswers, promptedAnswers); Object.assign(allAnswers, promptedAnswers);
} }
@ -618,7 +628,7 @@ class ConfigCollector {
allAnswers[question.name] = question.default; allAnswers[question.name] = question.default;
} }
} else { } else {
const promptedAnswers = await prompts.prompt(questions); const promptedAnswers = await inquirer.prompt(questions);
Object.assign(allAnswers, promptedAnswers); Object.assign(allAnswers, promptedAnswers);
} }
} }
@ -740,7 +750,7 @@ class ConfigCollector {
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName)); console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
// Ask user if they want to accept defaults or customize on the next line // Ask user if they want to accept defaults or customize on the next line
const { customize } = await prompts.prompt([ const { customize } = await inquirer.prompt([
{ {
type: 'confirm', type: 'confirm',
name: 'customize', name: 'customize',
@ -835,7 +845,7 @@ class ConfigCollector {
} }
/** /**
* Build a prompt question from a config item * Build an inquirer question from a config item
* @param {string} moduleName - Module name * @param {string} moduleName - Module name
* @param {string} key - Config key * @param {string} key - Config key
* @param {Object} item - Config item definition * @param {Object} item - Config item definition
@ -997,7 +1007,7 @@ class ConfigCollector {
message: message, message: message,
}; };
// Set default - if it's dynamic, use a function that the prompt will evaluate with current answers // Set default - if it's dynamic, use a function that inquirer will evaluate with current answers
// But if we have an existing value, always use that instead // But if we have an existing value, always use that instead
if (existingValue !== null && existingValue !== undefined && questionType !== 'list') { if (existingValue !== null && existingValue !== undefined && questionType !== 'list') {
question.default = existingValue; question.default = existingValue;

View File

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

View File

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

View File

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

View File

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

View File

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

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