Compare commits

..

4 Commits

Author SHA1 Message Date
Murat K Ozcan d2c8095d0b
Merge c83da03621 into 993d02b8b3 2026-01-14 18:23:17 -06:00
VJSai 993d02b8b3
Enhance security policy documentation (#1312)
Expanded the security policy to include supported versions, reporting guidelines, response timelines, security scope, and best practices for users.

Co-authored-by: Alex Verkhovsky <alexey.verkhovsky@gmail.com>
2026-01-14 16:27:52 -06:00
Davor Racic 5cb5606ba3
fix(cli): replace inquirer with @clack/prompts for Windows compatibility (#1316)
* fix(cli): replace inquirer with @clack/prompts for Windows compatibility

- Add new prompts.js wrapper around @clack/prompts to fix Windows arrow
  key navigation issues (libuv #852)
- Fix validation logic in github-copilot.js that always returned true
- Add support for primitive choice values (string/number) in select/multiselect
- Add 'when' property support for conditional questions in prompt()
- Update all IDE installers to use new prompts module

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(cli): address code review feedback for prompts migration

- Move @clack/prompts from devDependencies to dependencies (critical)
- Remove unused inquirer dependency
- Fix potential crash in multiselect when initialValues is undefined
- Add async validator detection with explicit error message
- Extract validateCustomContentPathSync method in ui.js
- Extract promptInstallLocation methods in claude-code.js and antigravity.js
- Fix moduleId -> missing.id in installer.js remove flow
- Update multiselect to support native clack API (options/initialValues)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: update comments to reference @clack/prompts instead of inquirer

- Update bmad-cli.js comment about CLI prompts
- Update config-collector.js JSDoc comments
- Rename inquirer variable to choiceUtils in ui.js
- Update JSDoc returns and calls documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(cli): add spacing between prompts and installation progress

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(cli): add multiselect usage hints for inexperienced users

Add inline navigation hints to all multiselect prompts showing
(↑/↓ navigate, SPACE select, ENTER confirm) to help users
unfamiliar with terminal multiselect controls.

Also restore detailed warning when no tools are selected,
explaining that SPACE must be pressed to select items.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(cli): restore IDE grouping using groupMultiselect

Replace flat multiselect with native @clack/prompts groupMultiselect
component to restore visual grouping of IDE/tool options:
- "Previously Configured" - pre-selected IDEs from existing install
- "Recommended Tools" - starred preferred options
- "Additional Tools" - other available options

This restores the grouped UX that was lost during the Inquirer.js
to @clack/prompts migration.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 16:25:35 -06:00
murat c83da03621 docs: refined the docs 2026-01-14 12:51:28 -06:00
33 changed files with 2028 additions and 3154 deletions

85
SECURITY.md Normal file
View File

@ -0,0 +1,85 @@
# 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**: AI generation verified with live browser (accurate selectors from real DOM) | | `*atdd` | Failing acceptance tests + implementation checklist | TDD red phase + optional recording mode | **+ Recording**: UI selectors verified with live browser; API tests benefit from trace analysis |
| `*automate` | Prioritized specs, fixtures, README/script updates, DoD summary | Optional healing/recording, avoid duplicate coverage | **+ Healing**: Pattern fixes enhanced with visual debugging + **+ Recording**: AI verified with live browser | | `*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) |
| `*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) - Why TEA exists, problem statement - [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
## 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 (`npx bmad-method@alpha install`) 1. Install BMad Method (see installation guide)
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/workflows/run-tea-for-enterprise.md) - Enterprise integration - [Running TEA for Enterprise](/docs/how-to/enterprise/use-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', // Pure function uses 'url' (not 'path') url: '/api/profile',
data: { name: 'New Name' }, // Pure function uses 'data' (not 'body') data: { name: 'New Name' },
headers: { Authorization: `Bearer ${authToken}` } headers: { Authorization: `Bearer ${authToken}` }
}); });

View File

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

View File

@ -125,6 +125,40 @@ 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.
@ -196,6 +230,7 @@ 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:
@ -219,6 +254,37 @@ 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`:
@ -252,6 +318,7 @@ 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());
@ -259,8 +326,23 @@ 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(
@ -270,8 +352,21 @@ 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(
@ -284,8 +379,24 @@ 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([
@ -298,8 +409,29 @@ 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(
@ -319,6 +451,28 @@ 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
@ -481,6 +635,36 @@ 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):**
@ -513,6 +697,35 @@ 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"
@ -545,29 +758,57 @@ await page.waitForSelector('.success'); // Then validate UI
### "Too Much Boilerplate" ### "Too Much Boilerplate"
**Solution:** Extract to fixtures (see Fixture Architecture) **Problem:** `waitForResponse` is verbose, repeated in every test.
**Solution:** Use Playwright Utils `interceptNetworkCall` - built-in fixture that reduces boilerplate.
**Vanilla Playwright (Repetitive):**
```typescript ```typescript
// Create reusable fixture test('test 1', async ({ page }) => {
export const test = base.extend({ const promise = page.waitForResponse(
waitForApi: async ({ page }, use) => { resp => resp.url().includes('/api/submit') && resp.ok()
await use((urlPattern: string) => { );
// Returns promise immediately (doesn't await) await page.click('button');
return page.waitForResponse( await promise;
resp => resp.url().includes(urlPattern) && resp.ok()
);
});
}
}); });
// Use in tests test('test 2', async ({ page }) => {
test('test', async ({ page, waitForApi }) => { const promise = page.waitForResponse(
const promise = waitForApi('/api/submit'); // Get promise resp => resp.url().includes('/api/load') && resp.ok()
await page.click('button'); // Trigger action );
await promise; // Wait for response await page.click('button');
await promise;
});
// Repeated pattern in every test
```
**With Playwright Utils (Cleaner):**
```typescript
import { test } from '@seontechnologies/playwright-utils/fixtures';
test('test 1', async ({ page, interceptNetworkCall }) => {
const submitCall = interceptNetworkCall({ url: '**/api/submit' });
await page.click('button');
const { status, responseJson } = await submitCall;
expect(status).toBe(200);
});
test('test 2', async ({ page, interceptNetworkCall }) => {
const loadCall = interceptNetworkCall({ url: '**/api/load' });
await page.click('button');
const { responseJson } = await loadCall;
// Automatic JSON parsing, cleaner API
}); });
``` ```
**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/workflows/run-tea-for-enterprise.md) - Enterprise risk management - [Running TEA for Enterprise](/docs/how-to/enterprise/use-tea-for-enterprise.md) - Enterprise risk management
## Reference ## Reference

View File

@ -107,7 +107,7 @@ test('flaky test', async ({ page }) => {
}); });
``` ```
**Good Example:** **Good Example (Vanilla Playwright):**
```typescript ```typescript
test('deterministic test', async ({ page }) => { test('deterministic test', async ({ page }) => {
const responsePromise = page.waitForResponse( const responsePromise = page.waitForResponse(
@ -126,12 +126,43 @@ test('deterministic test', async ({ page }) => {
}); });
``` ```
**Why it works:** **With Playwright Utils (Even Cleaner):**
```typescript
import { test } from '@seontechnologies/playwright-utils/fixtures';
import { expect } from '@playwright/test';
test('deterministic test', async ({ page, interceptNetworkCall }) => {
const submitCall = interceptNetworkCall({
method: 'POST',
url: '**/api/submit'
});
await page.click('button');
// Wait for actual response (automatic JSON parsing)
const { status, responseJson } = await submitCall;
expect(status).toBe(200);
// Modal should ALWAYS show (make it deterministic)
await expect(page.locator('.modal')).toBeVisible();
await page.click('.dismiss');
// Explicit assertion (fails if not visible)
await expect(page.locator('.success')).toBeVisible();
});
```
**Why both work:**
- 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.
@ -152,7 +183,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' } // 'body' not 'data' (hard-coded) body: { email: 'test@example.com' } (hard-coded)
}); });
userId = body.id; // Store in global userId = body.id; // Store in global
}); });
@ -162,7 +193,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' not 'data' body: { name: 'Updated' }
}); });
// No cleanup - leaves user in database // No cleanup - leaves user in database
}); });
@ -213,7 +244,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' not 'data' body: { email: testEmail, name: faker.person.fullName() }
}); });
expect(createStatus).toBe(201); expect(createStatus).toBe(201);
@ -222,7 +253,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' not 'data' body: { name: 'Updated Name' }
}); });
expect(status).toBe(200); expect(status).toBe(200);
@ -412,7 +443,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:** **Good Example (Vanilla Playwright):**
```typescript ```typescript
// ✅ Fast test (< 10 seconds) // ✅ Fast test (< 10 seconds)
test('fast test', async ({ page }) => { test('fast test', async ({ page }) => {
@ -436,8 +467,50 @@ 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`:
@ -821,7 +894,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/workflows/run-tea-for-enterprise.md) - Enterprise quality thresholds - [Running TEA for Enterprise](/docs/how-to/enterprise/use-tea-for-enterprise.md) - Enterprise quality thresholds
## Reference ## Reference

View File

@ -150,34 +150,40 @@ test('checkout completes', async ({ page }) => {
}); });
``` ```
**After (With Playwright Utils + Auto Error Detection):** **After (With Playwright Utils - Cleaner API):**
```typescript ```typescript
import { test } from '@seontechnologies/playwright-utils/network-error-monitor/fixtures'; import { test } from '@seontechnologies/playwright-utils/fixtures';
import { expect } from '@playwright/test';
// That's it! Just import the fixture - monitoring is automatic test('checkout completes', async ({ page, interceptNetworkCall }) => {
test('checkout completes', async ({ page }) => { // Use interceptNetworkCall for cleaner network interception
const checkoutPromise = page.waitForResponse( const checkoutCall = interceptNetworkCall({
resp => resp.url().includes('/api/checkout') && resp.ok() method: 'POST',
); 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');
await expect(page.locator('.confirmation')).toBeVisible();
// Zero setup - automatically fails if ANY 4xx/5xx occurred // Validate UI
// Error message: "Network errors detected: POST 500 /api/payment" await expect(page.locator('.confirmation')).toBeVisible();
}); });
``` ```
**Playwright Utils Benefits:** **Playwright Utils Benefits:**
- Auto-enabled by fixture import (zero code changes) - `interceptNetworkCall` for cleaner network interception
- Catches silent backend errors (500, 503, 504) - Automatic JSON parsing (`responseJson` ready to use)
- Test fails even if UI shows cached/stale success message - No manual `await response.json()`
- Structured error report in test output - Glob pattern matching (`**/api/checkout`)
- No manual error checking needed - Cleaner, more maintainable code
**For automatic error detection,** use `network-error-monitor` fixture separately. See [Integrate Playwright Utils](/docs/how-to/customization/integrate-playwright-utils.md#network-error-monitor).
**Priority 3: P1 Requirements** **Priority 3: P1 Requirements**
``` ```
@ -352,10 +358,10 @@ test.skip('flaky test - needs fixing', async ({ page }) => {
```markdown ```markdown
# 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 |
``` ```
**Fix systematically:** **Fix systematically:**
@ -398,12 +404,12 @@ Same process
```markdown ```markdown
# 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 |
| tests/integration/ | 12 | 45/100 | ❌ Legacy | Week 4 planned | | tests/integration/ | 12 | 45/100 | ❌ Legacy | Week 4 planned |
**Legend:** **Legend:**
- ✅ Modernized: Quality >80, no critical issues - ✅ Modernized: Quality >80, no critical issues
@ -465,15 +471,26 @@ Incremental changes = lower risk
**Solution:** **Solution:**
``` ```
1. Run *ci to add selective testing 1. Configure parallel execution (shard tests across workers)
2. Run only affected tests on PR 2. Add selective testing (run only affected tests on PR)
3. Run full suite nightly 3. Run full suite nightly only
4. Parallelize with sharding 4. Optimize slow tests (remove hard waits, improve selectors)
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.
@ -530,43 +547,6 @@ 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,17 +18,25 @@ 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` (verify selectors with live browser) - Want recording mode in `*atdd` or `*automate` (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
@ -71,13 +79,11 @@ 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.
## Installation ## Setup
### Step 1: Configure MCP Servers in IDE ### 1. Configure MCP Servers
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. Add to your IDE's MCP configuration:
**MCP Configuration:**
```json ```json
{ {
@ -94,36 +100,20 @@ Add this configuration to your IDE's MCP settings. See [TEA Overview](/docs/expl
} }
``` ```
### Step 2: Install Playwright Browsers See [TEA Overview](/docs/explanation/features/tea-overview.md#playwright-mcp-enhancements) for IDE-specific config locations.
```bash ### 2. Enable in BMAD
npx playwright install
```
### Step 3: Enable in TEA Config Answer "Yes" when prompted during installation, or set in config:
Edit `_bmad/bmm/config.yaml`:
```yaml ```yaml
# _bmad/bmm/config.yaml
tea_use_mcp_enhancements: true tea_use_mcp_enhancements: true
``` ```
### Step 4: Restart IDE ### 3. Verify MCPs Running
Restart your IDE to load MCP server configuration. Ensure your MCP servers are running in your IDE.
### 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
@ -162,16 +152,14 @@ I'll design tests for these interactions."
**Without MCP:** **Without MCP:**
- TEA generates selectors from best practices - TEA generates selectors from best practices
- May use `getByRole()` that doesn't match actual app - TEA infers API patterns from documentation
- Selectors might need adjustment
**With MCP:** **With MCP (Recording Mode):**
TEA verifies selectors with live browser:
**For UI Tests:**
``` ```
"Let me verify the login form selectors" [TEA navigates to /login with live browser]
[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')
@ -181,47 +169,58 @@ TEA verifies selectors with live browser:
I'll use these exact selectors." I'll use these exact selectors."
``` ```
**Generated test:** **For API Tests:**
```typescript ```
await page.getByLabel('Email Address').fill('test@example.com'); [TEA analyzes trace files from test runs]
await page.getByLabel('Your Password').fill('password'); [Inspects network requests/responses]
await page.getByRole('button', { name: 'Sign In' }).click();
// Selectors verified against actual DOM "I see the API returns:
- POST /api/login → 200 with { token, userId }
- Response time: 150ms
- Required headers: Content-Type, Authorization
I'll validate these in tests."
``` ```
**Benefits:** **Benefits:**
- Accurate selectors from real DOM - UI: Accurate selectors from real DOM
- Tests work on first run - API: Validated request/response patterns from trace
- No trial-and-error selector debugging - Both: Tests work on first run
### *automate: Healing Mode ### *automate: Healing + Recording Modes
**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
- Can't verify fixes work - Generates tests from documentation/code
**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] [Analyzes screenshots + network tab]
[Identifies selector changed]
"The button selector changed from 'Save' to 'Save Changes' UI failures: "Button selector changed from 'Save' to 'Save Changes'"
I'll update the test and verify it works" API failures: "Response structure changed, expected {id} got {userId}"
[TEA makes fix] [TEA makes fixes]
[Runs test with MCP] [Verifies with trace analysis]
[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 during healing - Visual debugging + trace analysis (not just UI)
- Verified fixes (not guesses) - Verified selectors (UI) + network patterns (API)
- Faster resolution - Tests verified against actual application behavior
## Usage Examples ## Usage Examples
@ -290,43 +289,6 @@ 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
@ -433,107 +395,6 @@ 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 installation (`npx bmad-method@alpha install`), it's already set. **Note:** If you enabled this during BMad installation, it's already set.
### Step 3: Verify Installation ### Step 3: Verify Installation
@ -175,13 +175,16 @@ Reviews against playwright-utils best practices:
### *ci Workflow ### *ci Workflow
**Without Playwright Utils:** **Without Playwright Utils:**
Basic CI configuration - Parallel sharding
- Burn-in loops (basic shell scripts)
- CI triggers (PR, push, schedule)
- Artifact collection
**With Playwright Utils:** **With Playwright Utils:**
Enhanced CI with: Enhanced with smart testing:
- Burn-in utility for smart test selection - Burn-in utility (git diff-based, volume control)
- Selective testing based on git diff - Selective testing (skip config/docs/types changes)
- Test prioritization - Test prioritization by file changes
## Available Utilities ## Available Utilities
@ -189,6 +192,18 @@ Enhanced CI with:
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';
@ -206,7 +221,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); // Note: chained method }).validateSchema(UserSchema); // Chained method (can await separately if needed)
expect(status).toBe(201); expect(status).toBe(201);
expect(body.id).toBeDefined(); expect(body.id).toBeDefined();
@ -224,6 +239,17 @@ 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';
@ -262,6 +288,17 @@ 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';
@ -301,6 +338,17 @@ 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';
@ -337,6 +385,17 @@ 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';
@ -373,6 +432,17 @@ 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';
@ -396,13 +466,24 @@ 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
- Supports object logging with `.debug()` - Logs objects seamlessly (no special handling needed)
- 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';
@ -444,6 +525,17 @@ 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
@ -490,6 +582,7 @@ 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
@ -499,6 +592,17 @@ 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';
@ -540,98 +644,76 @@ test.describe('error handling',
**Benefits:** **Benefits:**
- Auto-enabled (zero setup) - Auto-enabled (zero setup)
- Catches silent backend failures - Catches silent backend failures (500, 503, 504)
- Opt-out with annotations - **Prevents domino effect** (limits cascading failures from one bad endpoint)
- Structured error reporting - Opt-out with annotations for validation tests
- Structured error reporting (JSON artifacts)
## Fixture Composition ## Fixture Composition
Combine utilities using `mergeTests`: **Option 1: Use Package's Combined Fixtures (Simplest)**
**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('full test', async ({ apiRequest, authToken, interceptNetworkCall }) => { test('api test', async ({ apiRequest, interceptNetworkCall }) => {
await log.info('Starting test'); // log is direct import await log.info('Fetching users');
const { status, body } = await apiRequest({ const { status, body } = await apiRequest({
method: 'GET', method: 'GET',
path: '/api/data', path: '/api/users'
headers: { Authorization: `Bearer ${authToken}` }
}); });
await log.info('Data fetched', body);
expect(status).toBe(200); expect(status).toBe(200);
}); });
``` ```
**Note:** `log` is imported directly (not a fixture). `authToken` requires auth-session provider setup. **Option 2: Create Custom Merged Fixtures (Selective)**
**Option 2: Merge Individual Fixtures (Selective)** **File 1: support/merged-fixtures.ts**
```typescript ```typescript
import { test as base } from '@playwright/test'; import { test as base, mergeTests } from '@playwright/test';
import { mergeTests } from '@playwright/test'; import { test as apiRequest } from '@seontechnologies/playwright-utils/api-request/fixtures';
import { test as apiRequestFixture } from '@seontechnologies/playwright-utils/api-request/fixtures'; import { test as interceptNetworkCall } from '@seontechnologies/playwright-utils/intercept-network-call/fixtures';
import { test as recurseFixture } from '@seontechnologies/playwright-utils/recurse/fixtures'; import { test as networkErrorMonitor } from '@seontechnologies/playwright-utils/network-error-monitor/fixtures';
import { log } from '@seontechnologies/playwright-utils'; import { log } from '@seontechnologies/playwright-utils';
// Merge only the fixtures you need // Merge only what you need
export const test = mergeTests( export const test = mergeTests(
apiRequestFixture, base,
recurseFixture apiRequest,
interceptNetworkCall,
networkErrorMonitor
); );
export { expect } from '@playwright/test'; export const expect = base.expect;
export { log };
```
// Use merged utilities in tests **File 2: tests/api/users.spec.ts**
test('selective test', async ({ apiRequest, recurse }) => { ```typescript
await log.info('Starting test'); // log is direct import, not fixture import { test, expect, log } from '../support/merged-fixtures';
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/data' path: '/api/users'
}); });
await log.info('Data fetched', body);
expect(status).toBe(200); expect(status).toBe(200);
}); });
``` ```
**Note:** `log` is a direct utility (not a fixture), so import it separately. **Contrast:**
- Option 1: All utilities available, zero setup
- Option 2: Pick utilities you need, one central file
**Recommended:** Use Option 1 (combined fixtures) unless you need fine control over which utilities are included. **See working examples:** <https://github.com/seontechnologies/playwright-utils/tree/main/playwright/support>
## 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
@ -698,47 +780,6 @@ 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:**
@ -755,6 +796,7 @@ test('should access protected route', async ({ page, authToken }) => {
## 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,17 +90,15 @@ 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 - Mix of levels (see [API Tests First, E2E Later](#api-tests-first-e2e-later) tip)
**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) |
**Example response:** **Example response:**
@ -190,7 +188,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' not 'data' body: {
name: 'Updated Name', name: 'Updated Name',
email: 'updated@example.com' email: 'updated@example.com'
} }
@ -205,7 +203,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' not 'data' body: { email: 'invalid-email' }
}); });
expect(status).toBe(400); expect(status).toBe(400);
@ -226,52 +224,28 @@ test.describe('Profile API', () => {
```typescript ```typescript
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
test.describe('Profile Page', () => { test('should edit and save profile', async ({ 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();
});
test('should display current profile information', async ({ page }) => { // Navigate to profile
await page.goto('/profile'); await page.goto('/profile');
await expect(page.getByText('test@example.com')).toBeVisible(); // Edit profile
await expect(page.getByText('Test User')).toBeVisible(); await page.getByRole('button', { name: 'Edit Profile' }).click();
}); await page.getByLabel('Name').fill('Updated Name');
await page.getByRole('button', { name: 'Save' }).click();
test('should edit and save profile', async ({ page }) => { // Verify success
await page.goto('/profile'); await expect(page.getByText('Profile updated')).toBeVisible();
// Click edit
await page.getByRole('button', { name: 'Edit Profile' }).click();
// Modify fields
await page.getByLabel('Name').fill('Updated Name');
await page.getByLabel('Email').fill('updated@example.com');
// Save
await page.getByRole('button', { name: 'Save' }).click();
// Verify success
await expect(page.getByText('Profile updated 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:
@ -400,18 +374,13 @@ Run `*test-design` before `*atdd` for better results:
*atdd # Generate tests based on design *atdd # Generate tests based on design
``` ```
### Recording Mode Note ### MCP Enhancements (Optional)
**Recording mode is NOT typically used with ATDD** because ATDD generates tests for features that don't exist yet (no UI to record against). If you have MCP servers configured (`tea_use_mcp_enhancements: true`), TEA can use them during `*atdd`.
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). **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.
**Recording mode is only applicable for ATDD in the rare case where:** See [Enable MCP Enhancements](/docs/how-to/customization/enable-tea-mcp-enhancements.md) for setup.
- 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
@ -444,43 +413,6 @@ 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
@ -489,6 +421,7 @@ Avoid CSS selectors
## 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' not 'data' body: { name: 'Updated Name', bio: 'Test bio' },
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' not 'data' body: { email: 'invalid-email' },
headers: { Authorization: `Bearer ${authToken}` } headers: { Authorization: `Bearer ${authToken}` }
}); });
@ -250,58 +250,31 @@ 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-workflow.spec.ts`): #### E2E Tests (`tests/e2e/profile.spec.ts`):
```typescript ```typescript
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
test.describe('Profile Management Workflow', () => { test('should edit profile', async ({ page }) => {
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();
// Wait for login to complete // Edit profile
await expect(page).toHaveURL(/\/dashboard/); await page.goto('/profile');
}); await page.getByRole('button', { name: 'Edit Profile' }).click();
await page.getByLabel('Name').fill('New Name');
await page.getByRole('button', { name: 'Save' }).click();
test('should view and edit profile', async ({ page }) => { // Verify success
// Navigate to profile await expect(page.getByText('Profile updated')).toBeVisible();
await page.goto('/profile');
// Verify profile displays
await expect(page.getByText('test@example.com')).toBeVisible();
// Edit profile
await page.getByRole('button', { name: 'Edit Profile' }).click();
await page.getByLabel('Name').fill('New Name');
await page.getByRole('button', { name: 'Save' }).click();
// Verify success
await expect(page.getByText('Profile updated')).toBeVisible();
await expect(page.getByText('New Name')).toBeVisible();
});
test('should show validation errors', async ({ page }) => {
await page.goto('/profile');
await page.getByRole('button', { name: 'Edit Profile' }).click();
// Enter invalid email
await page.getByLabel('Email').fill('invalid');
await page.getByRole('button', { name: 'Save' }).click();
// Verify error shown
await expect(page.getByText('Invalid email format')).toBeVisible();
// Profile should not be updated
await page.reload();
await expect(page.getByText('test@example.com')).toBeVisible();
});
}); });
``` ```
TEA generates additional tests for validation, edge cases, etc. based on priorities.
#### Fixtures (`tests/support/fixtures/profile.ts`): #### Fixtures (`tests/support/fixtures/profile.ts`):
**Vanilla Playwright:** **Vanilla Playwright:**
@ -504,9 +477,9 @@ 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` |
**Note:** Component tests use separate tooling from E2E tests: **Note:** Component tests use separate tooling from E2E tests:
@ -568,25 +541,14 @@ 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.
### Use Healing Mode (Optional) ### MCP Enhancements (Optional)
If MCP enhancements enabled (`tea_use_mcp_enhancements: true`): If you have MCP servers configured (`tea_use_mcp_enhancements: true`), TEA can use them during `*automate` for:
When prompted, select "healing mode" to: - **Healing mode:** Fix broken selectors, update assertions, enhance with trace analysis
- Fix broken selectors in existing tests - **Recording mode:** Verify selectors with live browser, capture network requests
- Update outdated assertions
- Enhance with trace viewer insights
See [Enable MCP Enhancements](/docs/how-to/customization/enable-tea-mcp-enhancements.md) No prompts - TEA uses MCPs automatically when available. See [Enable MCP Enhancements](/docs/how-to/customization/enable-tea-mcp-enhancements.md) for setup.
### 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
@ -662,21 +624,11 @@ We already have these tests:
Generate tests for scenarios NOT covered by those files Generate tests for scenarios NOT covered by those files
``` ```
### Selectors Are Fragile ### MCP Enhancements for Better Selectors
**Problem:** E2E tests use brittle CSS selectors. If you have MCP servers configured, TEA verifies selectors against live browser. Otherwise, TEA generates accessible selectors (`getByRole`, `getByLabel`) by default.
**Solution:** Request accessible 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).
```
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
@ -686,6 +638,7 @@ Or use MCP recording mode for verified selectors.
## 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/workflows/run-tea-for-enterprise.md) - Enterprise workflow - [Run TEA for Enterprise](/docs/how-to/enterprise/use-tea-for-enterprise.md) - Enterprise workflow
## Understanding the Concepts ## Understanding the Concepts

View File

@ -62,12 +62,12 @@ 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 |
| **Multiple** | All of the above | Comprehensive analysis | | **Multiple** | All of the above | Comprehensive analysis |
**Example Response:** **Example Response:**
``` ```
@ -113,21 +113,21 @@ 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% |
| **No Coverage** | 1 | 7% | | **No Coverage** | 1 | 7% |
### 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% ⚠️ |
| **P3** | 1 | 0 | 0% ✅ (acceptable) | | **P3** | 1 | 0 | 0% ✅ (acceptable) |
--- ---
@ -223,10 +223,10 @@ 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 |
**Estimated Effort:** 3 hours **Estimated Effort:** 3 hours
**Owner:** QA team **Owner:** QA team
@ -234,9 +234,9 @@ 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
**Owner:** QA team **Owner:** QA team
@ -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' not 'data' body: { bio: 'Updated bio' },
headers: { Authorization: `Bearer ${authToken}` } headers: { Authorization: `Bearer ${authToken}` }
}); });
@ -442,12 +442,12 @@ 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) |
| **P3** | 20% | 0% | ✅ PASS (low priority) | | **P3** | 20% | 0% | ✅ PASS (low priority) |
**Rationale:** **Rationale:**
- All critical path (P0) requirements fully tested - All critical path (P0) requirements fully tested
@ -456,11 +456,11 @@ 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 | ✅ |
## Risks and Mitigations ## Risks and Mitigations
@ -501,14 +501,14 @@ 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** |
| Any | <80% | Any | **FAIL** | | Any | <80% | Any | **FAIL** |
| Any | Any | <80% | **FAIL** | | Any | Any | <80% | **FAIL** |
| Any | Any | Any | **WAIVED** ⏭️ (with approval) | | Any | Any | Any | **WAIVED** ⏭️ (with approval) |
**Detailed Rules:** **Detailed Rules:**
- **PASS:** P0=100%, P1≥90%, Overall≥80% - **PASS:** P0=100%, P1≥90%, Overall≥80%
@ -683,12 +683,12 @@ Track improvement over time:
```markdown ```markdown
## 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 |
| 2026-01-20 | Epic 3 | 100% | 88 | Ready! | | 2026-01-20 | Epic 3 | 100% | 88 | Ready! |
``` ```
### Set Coverage Targets by Priority ### Set Coverage Targets by Priority

View File

@ -290,137 +290,84 @@ burn-in:
- if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_PIPELINE_SOURCE == "merge_request_event"
``` ```
#### Helper Scripts #### Burn-In Testing
TEA generates shell scripts for CI and local development. **Option 1: Classic Burn-In (Playwright Built-In)**
**Test Scripts** (`package.json`):
```json ```json
{ {
"scripts": { "scripts": {
"test": "playwright test", "test": "playwright test",
"test:headed": "playwright test --headed", "test:burn-in": "playwright test --repeat-each=5 --retries=0"
"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"
} }
} }
``` ```
**Selective Testing Script** (`scripts/test-changed.sh`): **How it works:**
- Runs every test 5 times
- Fails if any iteration fails
- Detects flakiness before merge
```bash **Use when:** Small test suite, want to run everything multiple times
#!/bin/bash
# Run only tests for changed files
CHANGED_FILES=$(git diff --name-only origin/main...HEAD) ---
if echo "$CHANGED_FILES" | grep -q "src/.*\.ts$"; then **Option 2: Smart Burn-In (Playwright Utils)**
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
```
**Burn-In Script** (`scripts/burn-in.sh`): If `tea_use_playwright_utils: true`:
```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, // Run 30% of affected tests burnInTestPercentage: 0.3,
burnIn: { repeatEach: 5, retries: 1 } burnIn: { repeatEach: 5, retries: 0 }
}; };
export default config; export default config;
``` ```
**Benefits over shell script:** **package.json:**
- Only runs tests affected by git changes (faster) ```json
- Smart filtering (skips config, docs, types) {
- Volume control (run percentage, not all tests) "scripts": {
"test:burn-in": "tsx scripts/burn-in-changed.ts"
}
}
```
**Example:** Changed 1 file → runs 3 affected tests 5 times = 15 runs (not 500 tests × 5 = 2500 runs) **How it works:**
- Git diff analysis (only affected tests)
- Smart filtering (skip configs, docs, types)
- Volume control (run 30% of affected tests)
- Each test runs 5 times
**Use when:** Large test suite, want intelligent selection
---
**Comparison:**
| Feature | Classic Burn-In | Smart Burn-In (PW-Utils) |
|---------|----------------|--------------------------|
| Changed 1 file | Runs all 500 tests × 5 = 2500 runs | Runs 3 affected tests × 5 = 15 runs |
| Config change | Runs all tests | Skips (no tests affected) |
| Type change | Runs all tests | Skips (no runtime impact) |
| Setup | Zero config | Requires config file |
**Recommendation:** Start with classic (simple), upgrade to smart (faster) when suite grows.
### 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:** `npx bmad-method@alpha install` command **Created By:** BMad installer
**Status:** Gitignored (not committed to repository) **Status:** Typically gitignored (user-specific values)
**Usage:** Edit this file to change TEA behavior in your project **Usage:** Edit this file to change TEA behavior in your project
@ -155,17 +155,7 @@ Would you like to enable MCP enhancements in Test Architect?
} }
``` ```
**Configuration Location (IDE-Specific):** **Configuration:** Refer to your AI agent's documentation for MCP server setup instructions.
**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
@ -364,9 +354,9 @@ tea_use_playwright_utils: true
tea_use_mcp_enhancements: false tea_use_mcp_enhancements: false
``` ```
**Individual config (gitignored):** **Individual config (typically gitignored):**
```yaml ```yaml
# _bmad/bmm/config.yaml (gitignored) # _bmad/bmm/config.yaml (user adds to .gitignore)
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
@ -407,7 +397,7 @@ _bmad/bmm/config.yaml.example # Template for team
package.json # Dependencies package.json # Dependencies
``` ```
**Gitignore:** **Recommended for .gitignore:**
``` ```
_bmad/bmm/config.yaml # User-specific values _bmad/bmm/config.yaml # User-specific values
.env # Secrets .env # Secrets
@ -420,8 +410,7 @@ _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
@ -558,48 +547,48 @@ npx playwright install
## Configuration Examples ## Configuration Examples
### Minimal Setup (Defaults) ### Recommended Setup (Full Stack)
```yaml
# _bmad/bmm/config.yaml
project_name: my-project
user_skill_level: beginner # or intermediate/expert
output_folder: _bmad-output
tea_use_playwright_utils: true # Recommended
tea_use_mcp_enhancements: true # Recommended
```
**Why recommended:**
- Playwright Utils: Production-ready fixtures and utilities
- MCP enhancements: Live browser verification, visual debugging
- Together: The three-part stack (see [Testing as Engineering](/docs/explanation/philosophy/testing-as-engineering.md))
**Prerequisites:**
```bash
npm install -D @seontechnologies/playwright-utils
# Configure MCP servers in IDE (see Enable MCP Enhancements guide)
```
**Best for:** Everyone (beginners learn good patterns from day one)
---
### Minimal Setup (Learning Only)
```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:**
- New projects - First-time TEA users (keep it simple initially)
- Learning TEA - Quick experiments
- Simple testing needs - Learning basics before adding integrations
--- **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
--- ---
@ -622,7 +611,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 # API tests don't need it tea_use_playwright_utils: false # Using vanilla Playwright only
``` ```
--- ---
@ -642,9 +631,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 # TEA Configuration (Recommended: Enable both for full stack)
tea_use_playwright_utils: false # Set true if using @seontechnologies/playwright-utils tea_use_playwright_utils: true # Recommended - production-ready utilities
tea_use_mcp_enhancements: false # Set true if MCP servers configured in IDE tea_use_mcp_enhancements: true # Recommended - live browser verification
# Languages # Languages
communication_language: english communication_language: english
@ -668,74 +657,6 @@ 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,11 +167,10 @@ Feature flag testing, contract testing, and API testing patterns.
### Playwright-Utils Integration ### Playwright-Utils Integration
Patterns for using `@seontechnologies/playwright-utils` package (11 utilities). Patterns for using `@seontechnologies/playwright-utils` package (9 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 |
@ -181,9 +180,8 @@ Patterns for using `@seontechnologies/playwright-utils` package (11 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:** All 11 playwright-utils fragments are in the same `knowledge/` directory as other fragments. **Note:** `fixtures-composition` is listed under Architecture & Fixtures (general Playwright `mergeTests` pattern, applies to all fixtures).
**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`
@ -211,51 +209,11 @@ 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 Locations **Fragment Location:** `src/modules/bmm/testarch/knowledge/` (all 33 fragments in single directory)
**Knowledge Base Directory:** **Manifest:** `src/modules/bmm/testarch/tea-index.csv`
```
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
@ -371,207 +329,6 @@ 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,9 +51,7 @@ You've just explored the features we'll test!
### Install BMad Method ### Install BMad Method
```bash Install BMad (see installation guide for latest command).
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)
@ -272,7 +270,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' not 'data' body: { title: 'Complete tutorial' }
}); });
expect(status).toBe(201); expect(status).toBe(201);
@ -393,7 +391,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) - Design philosophy - [Testing as Engineering](/docs/explanation/philosophy/testing-as-engineering.md) - **Why TEA exists** (problem + solution)
- [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,7 +19,6 @@
"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",
@ -34,6 +33,7 @@
"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,6 +755,29 @@
"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",
@ -1997,36 +2020,6 @@
"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",
@ -3640,7 +3633,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==",
"devOptional": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~7.16.0" "undici-types": "~7.16.0"
@ -4028,6 +4021,7 @@
"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"
@ -4043,6 +4037,7 @@
"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"
@ -5596,12 +5591,6 @@
"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",
@ -5782,15 +5771,6 @@
"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",
@ -8263,22 +8243,6 @@
"@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",
@ -8414,43 +8378,6 @@
"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",
@ -11569,15 +11496,6 @@
"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",
@ -13300,15 +13218,6 @@
"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",
@ -13333,15 +13242,6 @@
"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",
@ -13362,12 +13262,6 @@
], ],
"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",
@ -14241,6 +14135,7 @@
"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": {
@ -14325,7 +14220,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==",
"devOptional": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/unicode-properties": { "node_modules/unicode-properties": {
@ -15258,18 +15153,6 @@
"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,6 +68,7 @@
] ]
}, },
"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",
@ -78,7 +79,6 @@
"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 inquirer prompts // Ensures keyboard interaction works properly with CLI prompts
if (process.stdin.isTTY) { if (process.stdin.isTTY) {
try { try {
process.stdin.resume(); process.stdin.resume();

View File

@ -71,14 +71,10 @@ 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 { default: inquirer } = await import('inquirer'); const prompts = require('../lib/prompts');
await inquirer.prompt([ await prompts.text({
{ message: chalk.green('Press Enter to start AgentVibes installer...'),
type: 'input', });
name: 'continue',
message: chalk.green('Press Enter to start AgentVibes installer...'),
},
]);
console.log(''); console.log('');

View File

@ -4,15 +4,7 @@ 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() {
@ -183,7 +175,6 @@ 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
@ -359,7 +350,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 inquirer.prompt(questions); const promptedAnswers = await prompts.prompt(questions);
// Merge prompted answers with static answers // Merge prompted answers with static answers
Object.assign(allAnswers, promptedAnswers); Object.assign(allAnswers, promptedAnswers);
@ -502,7 +493,6 @@ 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) {
@ -597,7 +587,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 inquirer.prompt([ const customizeAnswer = await prompts.prompt([
{ {
type: 'confirm', type: 'confirm',
name: 'customize', name: 'customize',
@ -614,7 +604,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 inquirer.prompt(questionsWithoutDefaults); const promptedAnswers = await prompts.prompt(questionsWithoutDefaults);
Object.assign(allAnswers, promptedAnswers); Object.assign(allAnswers, promptedAnswers);
} }
@ -628,7 +618,7 @@ class ConfigCollector {
allAnswers[question.name] = question.default; allAnswers[question.name] = question.default;
} }
} else { } else {
const promptedAnswers = await inquirer.prompt(questions); const promptedAnswers = await prompts.prompt(questions);
Object.assign(allAnswers, promptedAnswers); Object.assign(allAnswers, promptedAnswers);
} }
} }
@ -750,7 +740,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 inquirer.prompt([ const { customize } = await prompts.prompt([
{ {
type: 'confirm', type: 'confirm',
name: 'customize', name: 'customize',
@ -845,7 +835,7 @@ class ConfigCollector {
} }
/** /**
* Build an inquirer question from a config item * Build a prompt 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
@ -1007,7 +997,7 @@ class ConfigCollector {
message: message, message: message,
}; };
// Set default - if it's dynamic, use a function that inquirer will evaluate with current answers // Set default - if it's dynamic, use a function that the prompt 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,6 +16,7 @@ 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';
@ -758,6 +759,9 @@ 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 {
@ -2139,15 +2143,11 @@ class Installer {
* Private: Prompt for update action * Private: Prompt for update action
*/ */
async promptUpdateAction() { async promptUpdateAction() {
const { default: inquirer } = await import('inquirer'); const action = await prompts.select({
return await inquirer.prompt([ message: 'What would you like to do?',
{ choices: [{ name: 'Update existing installation', value: 'update' }],
type: 'list', });
name: 'action', return { action };
message: 'What would you like to do?',
choices: [{ name: 'Update existing installation', value: 'update' }],
},
]);
} }
/** /**
@ -2156,8 +2156,6 @@ 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)));
@ -2172,26 +2170,22 @@ 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 inquirer.prompt([ const proceed = await prompts.select({
{ message: 'What would you like to do?',
type: 'list', choices: [
name: 'proceed', {
message: 'What would you like to do?', name: 'Exit and clean up manually (recommended)',
choices: [ value: 'exit',
{ hint: 'Exit installation',
name: 'Exit and clean up manually (recommended)', },
value: 'exit', {
short: 'Exit installation', name: 'Continue with installation anyway',
}, value: 'continue',
{ hint: 'Continue',
name: 'Continue with installation anyway', },
value: 'continue', ],
short: 'Continue', default: 'exit',
}, });
],
default: 'exit',
},
]);
if (proceed === 'exit') { if (proceed === 'exit') {
console.log(''); console.log('');
@ -2437,7 +2431,6 @@ 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;
@ -2451,12 +2444,12 @@ class Installer {
{ {
name: 'Keep installed (will not be processed)', name: 'Keep installed (will not be processed)',
value: 'keep', value: 'keep',
short: 'Keep', hint: 'Keep',
}, },
{ {
name: 'Specify new source location', name: 'Specify new source location',
value: 'update', value: 'update',
short: 'Update', hint: 'Update',
}, },
]; ];
@ -2465,47 +2458,40 @@ class Installer {
choices.push({ choices.push({
name: '⚠️ REMOVE module completely (destructive!)', name: '⚠️ REMOVE module completely (destructive!)',
value: 'remove', value: 'remove',
short: 'Remove', hint: 'Remove',
}); });
} }
const { action } = await inquirer.prompt([ const action = await prompts.select({
{ message: `How would you like to handle "${missing.name}"?`,
type: 'list', choices,
name: 'action', });
message: `How would you like to handle "${missing.name}"?`,
choices,
},
]);
switch (action) { switch (action) {
case 'update': { case 'update': {
const { newSourcePath } = await inquirer.prompt([ // Use sync validation because @clack/prompts doesn't support async validate
{ const newSourcePath = await prompts.text({
type: 'input', message: 'Enter the new path to the custom module:',
name: 'newSourcePath', default: missing.sourcePath,
message: 'Enter the new path to the custom module:', validate: (input) => {
default: missing.sourcePath, if (!input || input.trim() === '') {
validate: async (input) => { return 'Please enter a path';
if (!input || input.trim() === '') { }
return 'Please enter a path'; const expandedPath = path.resolve(input.trim());
} if (!fs.pathExistsSync(expandedPath)) {
const expandedPath = path.resolve(input.trim()); return 'Path does not exist';
if (!(await fs.pathExists(expandedPath))) { }
return 'Path does not exist'; // Check if it looks like a valid module
} const moduleYamlPath = path.join(expandedPath, 'module.yaml');
// Check if it looks like a valid module const agentsPath = path.join(expandedPath, 'agents');
const moduleYamlPath = path.join(expandedPath, 'module.yaml'); const workflowsPath = path.join(expandedPath, 'workflows');
const agentsPath = path.join(expandedPath, 'agents');
const workflowsPath = path.join(expandedPath, 'workflows');
if (!(await fs.pathExists(moduleYamlPath)) && !(await fs.pathExists(agentsPath)) && !(await fs.pathExists(workflowsPath))) { if (!fs.pathExistsSync(moduleYamlPath) && !fs.pathExistsSync(agentsPath) && !fs.pathExistsSync(workflowsPath)) {
return 'Path does not appear to contain a valid custom module'; return 'Path does not appear to contain a valid custom module';
} }
return true; return; // clack expects undefined for valid input
},
}, },
]); });
// Update the source in manifest // Update the source in manifest
const resolvedPath = path.resolve(newSourcePath.trim()); const resolvedPath = path.resolve(newSourcePath.trim());
@ -2531,46 +2517,38 @@ 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 { confirm } = await inquirer.prompt([ const confirmDelete = await prompts.confirm({
{ message: chalk.red.bold('Are you absolutely sure you want to delete this module?'),
type: 'confirm', default: false,
name: 'confirm', });
message: chalk.red.bold('Are you absolutely sure you want to delete this module?'),
default: false,
},
]);
if (confirm) { if (confirmDelete) {
const { typedConfirm } = await inquirer.prompt([ const typedConfirm = await prompts.text({
{ message: chalk.red.bold('Type "DELETE" to confirm permanent deletion:'),
type: 'input', validate: (input) => {
name: 'typedConfirm', if (input !== 'DELETE') {
message: chalk.red.bold('Type "DELETE" to confirm permanent deletion:'), return chalk.red('You must type "DELETE" exactly to proceed');
validate: (input) => { }
if (input !== 'DELETE') { return; // clack expects undefined for valid input
return chalk.red('You must type "DELETE" exactly to proceed');
}
return true;
},
}, },
]); });
if (typedConfirm === 'DELETE') { if (typedConfirm === 'DELETE') {
// Remove the module from filesystem and manifest // Remove the module from filesystem and manifest
const modulePath = path.join(bmadDir, moduleId); const modulePath = path.join(bmadDir, missing.id);
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, moduleId); await this.manifest.removeModule(bmadDir, missing.id);
await this.manifest.removeCustomModule(bmadDir, moduleId); await this.manifest.removeCustomModule(bmadDir, missing.id);
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(moduleId)) { if (installedModules && installedModules.includes(missing.id)) {
const index = installedModules.indexOf(moduleId); const index = installedModules.indexOf(missing.id);
if (index !== -1) { if (index !== -1) {
installedModules.splice(index, 1); installedModules.splice(index, 1);
} }
@ -2591,7 +2569,7 @@ class Installer {
} }
case 'keep': { case 'keep': {
keptCount++; keptCount++;
keptModulesWithoutSources.push(moduleId); keptModulesWithoutSources.push(missing.id);
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,6 +13,7 @@ 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
@ -26,6 +27,21 @@ 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
@ -57,21 +73,7 @@ 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') {
// Ask for installation location config.installLocation = 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',
},
]);
config.installLocation = locationAnswer.location;
} }
} }
} catch (error) { } catch (error) {
@ -297,20 +299,7 @@ class AntigravitySetup extends BaseIdeSetup {
choices = await this.promptSubagentInstallation(config.subagents); choices = await this.promptSubagentInstallation(config.subagents);
if (choices.install !== 'none') { if (choices.install !== 'none') {
const { default: inquirer } = await import('inquirer'); location = await this._promptInstallLocation();
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;
} }
} }
@ -334,22 +323,16 @@ 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 inquirer.prompt([ const install = await prompts.select({
{ message: 'Would you like to install Antigravity subagents for enhanced functionality?',
type: 'list', choices: [
name: 'install', { name: 'Yes, install all subagents', value: 'all' },
message: 'Would you like to install Antigravity subagents for enhanced functionality?', { name: 'Yes, let me choose specific subagents', value: 'selective' },
choices: [ { name: 'No, skip subagent installation', value: 'none' },
{ name: 'Yes, install all subagents', value: 'all' }, ],
{ name: 'Yes, let me choose specific subagents', value: 'selective' }, default: 'all',
{ name: 'No, skip subagent installation', value: 'none' }, });
],
default: 'all',
},
]);
if (install === 'selective') { if (install === 'selective') {
// Show list of available subagents with descriptions // Show list of available subagents with descriptions
@ -361,18 +344,14 @@ class AntigravitySetup extends BaseIdeSetup {
'document-reviewer.md': 'Document quality review', 'document-reviewer.md': 'Document quality review',
}; };
const { selected } = await inquirer.prompt([ const selected = await prompts.multiselect({
{ message: `Select subagents to install ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
type: 'checkbox', choices: subagentConfig.files.map((file) => ({
name: 'selected', name: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
message: 'Select subagents to install:', value: file,
choices: subagentConfig.files.map((file) => ({ checked: true,
name: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`, })),
value: file, });
checked: true,
})),
},
]);
return { install: 'selective', selected }; return { install: 'selective', selected };
} }

View File

@ -13,6 +13,7 @@ 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
@ -25,6 +26,21 @@ 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
@ -56,21 +72,7 @@ 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') {
// Ask for installation location config.installLocation = 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',
},
]);
config.installLocation = locationAnswer.location;
} }
} }
} catch (error) { } catch (error) {
@ -305,20 +307,7 @@ class ClaudeCodeSetup extends BaseIdeSetup {
choices = await this.promptSubagentInstallation(config.subagents); choices = await this.promptSubagentInstallation(config.subagents);
if (choices.install !== 'none') { if (choices.install !== 'none') {
const { default: inquirer } = await import('inquirer'); location = await this.promptInstallLocation();
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;
} }
} }
@ -342,22 +331,16 @@ 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 inquirer.prompt([ const install = await prompts.select({
{ message: 'Would you like to install Claude Code subagents for enhanced functionality?',
type: 'list', choices: [
name: 'install', { name: 'Yes, install all subagents', value: 'all' },
message: 'Would you like to install Claude Code subagents for enhanced functionality?', { name: 'Yes, let me choose specific subagents', value: 'selective' },
choices: [ { name: 'No, skip subagent installation', value: 'none' },
{ name: 'Yes, install all subagents', value: 'all' }, ],
{ name: 'Yes, let me choose specific subagents', value: 'selective' }, default: 'all',
{ name: 'No, skip subagent installation', value: 'none' }, });
],
default: 'all',
},
]);
if (install === 'selective') { if (install === 'selective') {
// Show list of available subagents with descriptions // Show list of available subagents with descriptions
@ -369,18 +352,14 @@ class ClaudeCodeSetup extends BaseIdeSetup {
'document-reviewer.md': 'Document quality review', 'document-reviewer.md': 'Document quality review',
}; };
const { selected } = await inquirer.prompt([ const selected = await prompts.multiselect({
{ message: `Select subagents to install ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
type: 'checkbox', options: subagentConfig.files.map((file) => ({
name: 'selected', label: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
message: 'Select subagents to install:', value: file,
choices: subagentConfig.files.map((file) => ({ })),
name: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`, initialValues: subagentConfig.files,
value: file, });
checked: true,
})),
},
]);
return { install: 'selective', selected }; return { install: 'selective', selected };
} }

View File

@ -6,6 +6,7 @@ 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)
@ -21,32 +22,24 @@ 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) {
const { location } = await inquirer.prompt([ installLocation = await prompts.select({
{ message: 'Where would you like to install Codex CLI prompts?',
type: 'list', choices: [
name: 'location', {
message: 'Where would you like to install Codex CLI prompts?', name: 'Global - Simple for single project ' + '(~/.codex/prompts, but references THIS project only)',
choices: [ value: 'global',
{ },
name: 'Global - Simple for single project ' + '(~/.codex/prompts, but references THIS project only)', {
value: 'global', name: `Project-specific - Recommended for real work (requires CODEX_HOME=<project-dir>${path.sep}.codex)`,
}, value: 'project',
{ },
name: `Project-specific - Recommended for real work (requires CODEX_HOME=<project-dir>${path.sep}.codex)`, ],
value: 'project', default: 'global',
}, });
],
default: 'global',
},
]);
installLocation = location;
// Display detailed instructions for the chosen option // Display detailed instructions for the chosen option
console.log(''); console.log('');
@ -57,16 +50,10 @@ class CodexSetup extends BaseIdeSetup {
} }
// Confirm the choice // Confirm the choice
const { proceed } = await inquirer.prompt([ confirmed = await prompts.confirm({
{ message: 'Proceed with this installation option?',
type: 'confirm', default: true,
name: 'proceed', });
message: 'Proceed with this installation option?',
default: true,
},
]);
confirmed = proceed;
if (!confirmed) { if (!confirmed) {
console.log(chalk.yellow("\n Let's choose a different installation option.\n")); console.log(chalk.yellow("\n Let's choose a different installation option.\n"));

View File

@ -2,6 +2,7 @@ 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
@ -21,29 +22,23 @@ 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'));
const response = await inquirer.prompt([ config.vsCodeConfig = await prompts.select({
{ message: 'How would you like to configure VS Code settings?',
type: 'list', choices: [
name: 'configChoice', { name: 'Use recommended defaults (fastest)', value: 'defaults' },
message: 'How would you like to configure VS Code settings?', { name: 'Configure each setting manually', value: 'manual' },
choices: [ { name: 'Skip settings configuration', value: 'skip' },
{ name: 'Use recommended defaults (fastest)', value: 'defaults' }, ],
{ name: 'Configure each setting manually', value: 'manual' }, default: 'defaults',
{ name: 'Skip settings configuration', value: 'skip' }, });
],
default: 'defaults',
},
]);
config.vsCodeConfig = response.configChoice;
if (response.configChoice === 'manual') { if (config.vsCodeConfig === 'manual') {
config.manualSettings = await inquirer.prompt([ config.manualSettings = await prompts.prompt([
{ {
type: 'input', type: 'input',
name: 'maxRequests', name: 'maxRequests',
@ -52,7 +47,8 @@ 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';
return (num >= 1 && num <= 50) || 'Enter 1-50'; if (num < 1 || num > 50) return 'Enter a number between 1-50';
return true;
}, },
}, },
{ {

432
tools/cli/lib/prompts.js Normal file
View File

@ -0,0 +1,432 @@
/**
* @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,16 +4,21 @@ 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');
// Lazy-load inquirer (ESM module) to avoid ERR_REQUIRE_ESM // Separator class for visual grouping in select/multiselect prompts
let _inquirer = null; // Note: @clack/prompts doesn't support separators natively, they are filtered out
async function getInquirer() { class Separator {
if (!_inquirer) { constructor(text = '────────') {
_inquirer = (await import('inquirer')).default; this.line = text;
this.name = text;
} }
return _inquirer; type = 'separator';
} }
// Separator for choice lists (compatible interface)
const choiceUtils = { Separator };
/** /**
* UI utilities for the installer * UI utilities for the installer
*/ */
@ -23,7 +28,6 @@ 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
@ -113,26 +117,20 @@ class UI {
console.log(chalk.yellow('─'.repeat(80))); console.log(chalk.yellow('─'.repeat(80)));
console.log(''); console.log('');
const { proceed } = await inquirer.prompt([ const proceed = await prompts.select({
{ message: 'What would you like to do?',
type: 'list', choices: [
name: 'proceed', {
message: 'What would you like to do?', name: 'Cancel and do a fresh install (recommended)',
choices: [ value: 'cancel',
{ },
name: 'Cancel and do a fresh install (recommended)', {
value: 'cancel', name: 'Proceed anyway (will attempt update, potentially may fail or have unstable behavior)',
short: 'Cancel installation', value: 'proceed',
}, },
{ ],
name: 'Proceed anyway (will attempt update, potentially may fail or have unstable behavior)', default: 'cancel',
value: 'proceed', });
short: 'Proceed with update',
},
],
default: 'cancel',
},
]);
if (proceed === 'cancel') { if (proceed === 'cancel') {
console.log(''); console.log('');
@ -188,14 +186,10 @@ 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 inquirer.prompt([ const enableTts = await prompts.confirm({
{ message: 'Claude Code supports TTS (Text-to-Speech). Would you like to enable it?',
type: 'confirm', default: false,
name: 'enableTts', });
message: 'Claude Code supports TTS (Text-to-Speech). Would you like to enable it?',
default: false,
},
]);
if (enableTts) { if (enableTts) {
agentVibesConfig = { enabled: true, alreadyInstalled: false }; agentVibesConfig = { enabled: true, alreadyInstalled: false };
@ -250,18 +244,11 @@ class UI {
// Common actions // Common actions
choices.push({ name: 'Modify BMAD Installation', value: 'update' }); choices.push({ name: 'Modify BMAD Installation', value: 'update' });
const promptResult = await inquirer.prompt([ actionType = await prompts.select({
{ message: 'What would you like to do?',
type: 'list', choices: choices,
name: 'actionType', default: choices[0].value,
message: 'What would you like to do?', });
choices: choices,
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') {
@ -290,14 +277,10 @@ 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 inquirer.prompt([ const changeModuleSelection = await prompts.confirm({
{ message: 'Modify official module selection (BMad Method, BMad Builder, Creative Innovation Suite)?',
type: 'confirm', default: false,
name: 'changeModuleSelection', });
message: 'Modify official module selection (BMad Method, BMad Builder, Creative Innovation Suite)?',
default: false,
},
]);
let selectedModules = []; let selectedModules = [];
if (changeModuleSelection) { if (changeModuleSelection) {
@ -310,14 +293,10 @@ class UI {
// After module selection, ask about custom modules // After module selection, ask about custom modules
console.log(''); console.log('');
const { changeCustomModules } = await inquirer.prompt([ const changeCustomModules = await prompts.confirm({
{ message: 'Modify custom module selection (add, update, or remove custom modules/agents/workflows)?',
type: 'confirm', default: false,
name: 'changeCustomModules', });
message: 'Modify custom module selection (add, update, or remove custom modules/agents/workflows)?',
default: false,
},
]);
let customModuleResult = { selectedCustomModules: [], customContentConfig: { hasCustomContent: false } }; let customModuleResult = { selectedCustomModules: [], customContentConfig: { hasCustomContent: false } };
if (changeCustomModules) { if (changeCustomModules) {
@ -352,15 +331,10 @@ class UI {
let enableTts = false; let enableTts = false;
if (hasClaudeCode) { if (hasClaudeCode) {
const { enableTts: enable } = await inquirer.prompt([ enableTts = await prompts.confirm({
{ message: 'Claude Code supports TTS (Text-to-Speech). Would you like to enable it?',
type: 'confirm', default: false,
name: 'enableTts', });
message: 'Claude Code supports TTS (Text-to-Speech). Would you like to enable it?',
default: false,
},
]);
enableTts = enable;
} }
// Core config with existing defaults (ask after TTS) // Core config with existing defaults (ask after TTS)
@ -385,14 +359,10 @@ 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 inquirer.prompt([ const wantsOfficialModules = await prompts.confirm({
{ message: 'Will you be installing any official BMad modules (BMad Method, BMad Builder, Creative Innovation Suite)?',
type: 'confirm', default: true,
name: 'wantsOfficialModules', });
message: 'Will you be installing any official BMad modules (BMad Method, BMad Builder, Creative Innovation Suite)?',
default: true,
},
]);
let selectedOfficialModules = []; let selectedOfficialModules = [];
if (wantsOfficialModules) { if (wantsOfficialModules) {
@ -401,14 +371,10 @@ class UI {
} }
// Ask about custom content // Ask about custom content
const { wantsCustomContent } = await inquirer.prompt([ const wantsCustomContent = await prompts.confirm({
{ message: 'Would you like to install a local custom module (this includes custom agents and workflows also)?',
type: 'confirm', default: false,
name: 'wantsCustomContent', });
message: 'Would you like to install a local custom module (this includes custom agents and workflows also)?',
default: false,
},
]);
if (wantsCustomContent) { if (wantsCustomContent) {
customContentConfig = await this.promptCustomContentSource(); customContentConfig = await this.promptCustomContentSource();
@ -459,7 +425,6 @@ 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');
@ -477,13 +442,14 @@ class UI {
const preferredIdes = ideManager.getPreferredIdes(); const preferredIdes = ideManager.getPreferredIdes();
const otherIdes = ideManager.getOtherIdes(); const otherIdes = ideManager.getOtherIdes();
// Build IDE choices array with separators // Build grouped options object for groupMultiselect
const ideChoices = []; const groupedOptions = {};
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) {
ideChoices.push(new inquirer.Separator('── Previously Configured ──')); const configuredGroup = [];
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') {
@ -496,81 +462,71 @@ class UI {
const ide = preferredIde || otherIde; const ide = preferredIde || otherIde;
if (ide) { if (ide) {
ideChoices.push({ configuredGroup.push({
name: `${ide.name}`, label: `${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) {
ideChoices.push(new inquirer.Separator('── Recommended Tools ──')); groupedOptions['Recommended Tools'] = remainingPreferred.map((ide) => {
for (const ide of remainingPreferred) {
ideChoices.push({
name: `${ide.name}`,
value: ide.value,
checked: false,
});
processedIdes.add(ide.value); processedIdes.add(ide.value);
} return {
label: `${ide.name}`,
value: 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) {
ideChoices.push(new inquirer.Separator('── Additional Tools ──')); groupedOptions['Additional Tools'] = remainingOther.map((ide) => ({
for (const ide of remainingOther) { label: ide.name,
ideChoices.push({ value: ide.value,
name: ide.name, }));
value: ide.value,
checked: false,
});
}
} }
let answers; let selectedIdes = [];
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) {
answers = await inquirer.prompt([ selectedIdes = await prompts.groupMultiselect({
{ message: `Select tools to configure ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
type: 'checkbox', options: groupedOptions,
name: 'ides', initialValues: initialValues.length > 0 ? initialValues : undefined,
message: 'Select tools to configure:', required: false,
choices: ideChoices, });
pageSize: 30,
},
]);
// If tools were selected, we're done // If tools were selected, we're done
if (answers.ides && answers.ides.length > 0) { if (selectedIdes && selectedIdes.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 SPACEBAR to select items, then ENTER to confirm.')); console.log(chalk.red(' You must press SPACE 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 inquirer.prompt([ const goBack = await prompts.confirm({
{ message: chalk.yellow('Would you like to go back and select at least one tool?'),
type: 'confirm', default: true,
name: 'goBack', });
message: chalk.yellow('Would you like to go back and select at least one tool?'),
default: true,
},
]);
if (goBack) { if (goBack) {
// Re-display a message before looping back // Re-display a message before looping back
@ -582,8 +538,8 @@ class UI {
} }
return { return {
ides: answers.ides || [], ides: selectedIdes || [],
skipIde: !answers.ides || answers.ides.length === 0, skipIde: !selectedIdes || selectedIdes.length === 0,
}; };
} }
@ -592,23 +548,17 @@ class UI {
* @returns {Object} Update configuration * @returns {Object} Update configuration
*/ */
async promptUpdate() { async promptUpdate() {
const inquirer = await getInquirer(); const backupFirst = await prompts.confirm({
const answers = await inquirer.prompt([ message: 'Create backup before updating?',
{ default: true,
type: 'confirm', });
name: 'backupFirst',
message: 'Create backup before updating?',
default: true,
},
{
type: 'confirm',
name: 'preserveCustomizations',
message: 'Preserve local customizations?',
default: true,
},
]);
return answers; const preserveCustomizations = await prompts.confirm({
message: 'Preserve local customizations?',
default: true,
});
return { backupFirst, preserveCustomizations };
} }
/** /**
@ -617,27 +567,17 @@ 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 inquirer.prompt([ const selectedModules = await prompts.multiselect({
{ message: `Select modules to add ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
type: 'checkbox', choices,
name: 'selectedModules', required: true,
message: 'Select modules to add:', });
choices,
validate: (answer) => {
if (answer.length === 0) {
return 'You must choose at least one module.';
}
return true;
},
},
]);
return selectedModules; return selectedModules;
} }
@ -649,17 +589,10 @@ class UI {
* @returns {boolean} User confirmation * @returns {boolean} User confirmation
*/ */
async confirm(message, defaultValue = false) { async confirm(message, defaultValue = false) {
const inquirer = await getInquirer(); return await prompts.confirm({
const { confirmed } = await inquirer.prompt([ message,
{ default: defaultValue,
type: 'confirm', });
name: 'confirmed',
message,
default: defaultValue,
},
]);
return confirmed;
} }
/** /**
@ -753,10 +686,9 @@ 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 inquirer * @returns {Array} Module choices for prompt
*/ */
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;
@ -811,9 +743,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 inquirer.Separator('── Custom Content ──'), new choiceUtils.Separator('── Custom Content ──'),
...allCustomModules, ...allCustomModules,
new inquirer.Separator('── Official Content ──'), new choiceUtils.Separator('── Official Content ──'),
); );
} }
@ -837,44 +769,43 @@ class UI {
* @returns {Array} Selected module IDs * @returns {Array} Selected module IDs
*/ */
async selectModules(moduleChoices, defaultSelections = []) { async selectModules(moduleChoices, defaultSelections = []) {
const inquirer = await getInquirer(); // Mark choices as checked based on defaultSelections
const moduleAnswer = await inquirer.prompt([ const choicesWithDefaults = moduleChoices.map((choice) => ({
{ ...choice,
type: 'checkbox', checked: defaultSelections.includes(choice.value),
name: 'modules', }));
message: 'Select modules to install:',
choices: moduleChoices,
default: defaultSelections,
},
]);
const selected = moduleAnswer.modules || []; const selected = await prompts.multiselect({
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 inquirer * @returns {Object} Directory answer from prompt
*/ */
async promptForDirectory() { async promptForDirectory() {
const inquirer = await getInquirer(); // Use sync validation because @clack/prompts doesn't support async validate
return await inquirer.prompt([ const directory = await prompts.text({
{ message: 'Installation directory:',
type: 'input', default: process.cwd(),
name: 'directory', placeholder: process.cwd(),
message: `Installation directory:`, validate: (input) => this.validateDirectorySync(input),
default: process.cwd(), });
validate: async (input) => this.validateDirectory(input),
filter: (input) => { // Apply filter logic
// If empty, use the default let filteredDir = directory;
if (!input || input.trim() === '') { if (!filteredDir || filteredDir.trim() === '') {
return process.cwd(); filteredDir = process.cwd();
} } else {
return this.expandUserPath(input); filteredDir = this.expandUserPath(filteredDir);
}, }
},
]); return { directory: filteredDir };
} }
/** /**
@ -915,45 +846,92 @@ 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 confirmAnswer = await inquirer.prompt([ const proceed = await prompts.confirm({
{ message: 'Install to this directory?',
type: 'confirm', default: true,
name: 'proceed', });
message: `Install to this directory?`,
default: true,
},
]);
if (!confirmAnswer.proceed) { if (!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 confirmAnswer.proceed; return proceed;
} else { } else {
// Ask for confirmation to create the directory // Ask for confirmation to create the directory
const createConfirm = await inquirer.prompt([ const create = await prompts.confirm({
{ message: `The directory '${directory}' doesn't exist. Would you like to create it?`,
type: 'confirm', default: false,
name: 'create', });
message: `The directory '${directory}' doesn't exist. Would you like to create it?`,
default: false,
},
]);
if (!createConfirm.create) { if (!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 createConfirm.create; return create;
} }
} }
/** /**
* Validate directory path for installation * Validate directory path for installation (sync version for clack prompts)
* @param {string} input - User input path
* @returns {string|undefined} Error message or undefined if valid
*/
validateDirectorySync(input) {
// Allow empty input to use the default
if (!input || input.trim() === '') {
return; // Empty means use default, undefined = valid for clack
}
let expandedPath;
try {
expandedPath = this.expandUserPath(input.trim());
} catch (error) {
return error.message;
}
// Check if the path exists
const pathExists = fs.pathExistsSync(expandedPath);
if (!pathExists) {
// Find the first existing parent directory
const existingParent = this.findExistingParentSync(expandedPath);
if (!existingParent) {
return 'Cannot create directory: no existing parent directory found';
}
// Check if the existing parent is writable
try {
fs.accessSync(existingParent, fs.constants.W_OK);
// Path doesn't exist but can be created - will prompt for confirmation later
return;
} catch {
// Provide a detailed error message explaining both issues
return `Directory '${expandedPath}' does not exist and cannot be created: parent directory '${existingParent}' is not writable`;
}
}
// If it exists, validate it's a directory and writable
const stat = fs.statSync(expandedPath);
if (!stat.isDirectory()) {
return `Path exists but is not a directory: ${expandedPath}`;
}
// Check write permissions
try {
fs.accessSync(expandedPath, fs.constants.W_OK);
} catch {
return `Directory is not writable: ${expandedPath}`;
}
return;
}
/**
* Validate directory path for installation (async version)
* @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
*/ */
@ -1009,7 +987,28 @@ class UI {
} }
/** /**
* Find the first existing parent directory * Find the first existing parent directory (sync version)
* @param {string} targetPath - The path to check
* @returns {string|null} The first existing parent directory, or null if none found
*/
findExistingParentSync(targetPath) {
let currentPath = path.resolve(targetPath);
// Walk up the directory tree until we find an existing directory
while (currentPath !== path.dirname(currentPath)) {
// Stop at root
const parent = path.dirname(currentPath);
if (fs.pathExistsSync(parent)) {
return parent;
}
currentPath = parent;
}
return null; // No existing parent found (shouldn't happen in practice)
}
/**
* Find the first existing parent directory (async version)
* @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
*/ */
@ -1071,7 +1070,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(), inquirer.prompt(), chalk.green/yellow/dim() * @calls checkAgentVibesInstalled(), prompts.select(), 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)
@ -1102,7 +1101,6 @@ 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
@ -1114,23 +1112,19 @@ class UI {
console.log(chalk.dim(' AgentVibes not detected')); console.log(chalk.dim(' AgentVibes not detected'));
} }
const answers = await inquirer.prompt([ const enableTts = await prompts.confirm({
{ message: 'Enable Agents to Speak Out loud (powered by Agent Vibes? Claude Code only currently)',
type: 'confirm', default: false,
name: 'enableTts', });
message: 'Enable Agents to Speak Out loud (powered by Agent Vibes? Claude Code only currently)',
default: false, // Default to yes - recommended for best experience
},
]);
if (answers.enableTts && !agentVibesInstalled) { if (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: answers.enableTts, enabled: enableTts,
alreadyInstalled: agentVibesInstalled, alreadyInstalled: agentVibesInstalled,
}; };
} }
@ -1248,30 +1242,75 @@ 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 inquirer.prompt([ const action = await prompts.select({
{ message: 'Would you like to:',
type: 'list', choices: [
name: 'action', { name: 'Add another custom module', value: 'add' },
message: 'Would you like to:', { name: 'Continue with installation', value: 'continue' },
choices: [ ],
{ name: 'Add another custom module', value: 'add' }, default: 'continue',
{ name: 'Continue with installation', value: 'continue' }, });
],
default: 'continue',
},
]);
if (action === 'continue') { if (action === 'continue') {
break; break;
@ -1282,57 +1321,11 @@ class UI {
let isValid = false; let isValid = false;
while (!isValid) { while (!isValid) {
const { path: inputPath } = await inquirer.prompt([ // Use sync validation because @clack/prompts doesn't support async validate
{ const inputPath = await prompts.text({
type: 'input', message: 'Enter the path to your custom content folder (or press Enter to cancel):',
name: 'path', validate: (input) => this.validateCustomContentPathSync(input),
message: 'Enter the path to your custom content folder (or press Enter to cancel):', });
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() === '') {
@ -1364,14 +1357,10 @@ 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 inquirer.prompt([ const shouldInstall = await prompts.confirm({
{ message: `Install ${customContentConfig.sources.length} custom module(s) now?`,
type: 'confirm', default: true,
name: 'shouldInstall', });
message: `Install ${customContentConfig.sources.length} custom module(s) now?`,
default: true,
},
]);
if (shouldInstall) { if (shouldInstall) {
customContentConfig.selected = true; customContentConfig.selected = true;
@ -1391,7 +1380,6 @@ 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);
@ -1451,16 +1439,11 @@ 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 inquirer.prompt([ const customAction = await prompts.select({
{ message: cachedCustomModules.length > 0 ? 'What would you like to do with custom modules?' : 'Would you like to add custom modules?',
type: 'list', choices: choices,
name: 'customAction', default: cachedCustomModules.length > 0 ? 'keep' : 'add',
message: });
cachedCustomModules.length > 0 ? 'What would you like to do with custom modules?' : 'Would you like to add custom modules?',
choices: choices,
default: cachedCustomModules.length > 0 ? 'keep' : 'add',
},
]);
switch (customAction) { switch (customAction) {
case 'keep': { case 'keep': {
@ -1472,21 +1455,18 @@ class UI {
case 'select': { case 'select': {
// Let user choose which to keep // Let user choose which to keep
const choices = cachedCustomModules.map((m) => ({ const selectChoices = 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 inquirer.prompt([ const keepModules = await prompts.multiselect({
{ message: `Select custom modules to keep ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
type: 'checkbox', choices: selectChoices,
name: 'keepModules', required: false,
message: 'Select custom modules to keep:', });
choices: choices, result.selectedCustomModules = keepModules || [];
default: cachedCustomModules.filter((m) => m.checked).map((m) => m.id),
},
]);
result.selectedCustomModules = keepModules;
break; break;
} }
@ -1586,7 +1566,6 @@ 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)
@ -1627,26 +1606,20 @@ class UI {
console.log(chalk.yellow('─'.repeat(80))); console.log(chalk.yellow('─'.repeat(80)));
console.log(''); console.log('');
const { proceed } = await inquirer.prompt([ const proceed = await prompts.select({
{ message: 'What would you like to do?',
type: 'list', choices: [
name: 'proceed', {
message: 'What would you like to do?', name: 'Proceed with update anyway (may have issues)',
choices: [ value: 'proceed',
{ },
name: 'Proceed with update anyway (may have issues)', {
value: 'proceed', name: 'Cancel (recommended - do a fresh install instead)',
short: 'Proceed with update', value: 'cancel',
}, },
{ ],
name: 'Cancel (recommended - do a fresh install instead)', default: 'cancel',
value: 'cancel', });
short: 'Cancel installation',
},
],
default: 'cancel',
},
]);
if (proceed === 'cancel') { if (proceed === 'cancel') {
console.log(''); console.log('');