gentyr 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/antipattern-hunter.md +176 -0
- package/.claude/agents/code-reviewer.md +205 -0
- package/.claude/agents/code-writer.md +154 -0
- package/.claude/agents/deputy-cto.md +309 -0
- package/.claude/agents/feedback-agent.md +101 -0
- package/.claude/agents/investigator.md +136 -0
- package/.claude/agents/product-manager.md +97 -0
- package/.claude/agents/project-manager.md +116 -0
- package/.claude/agents/repo-hygiene-expert.md +626 -0
- package/.claude/agents/secret-manager.md +324 -0
- package/.claude/agents/test-writer.md +354 -0
- package/.claude/commands/configure-personas.md +144 -0
- package/.claude/commands/cto-report.md +36 -0
- package/.claude/commands/demo.md +89 -0
- package/.claude/commands/deputy-cto.md +345 -0
- package/.claude/commands/hotfix.md +31 -0
- package/.claude/commands/overdrive-gentyr.md +167 -0
- package/.claude/commands/product-manager.md +32 -0
- package/.claude/commands/push-migrations.md +86 -0
- package/.claude/commands/push-secrets.md +97 -0
- package/.claude/commands/services.json.example +30 -0
- package/.claude/commands/setup-gentyr.md +396 -0
- package/.claude/commands/show.md +42 -0
- package/.claude/commands/spawn-tasks.md +79 -0
- package/.claude/commands/toggle-automation-gentyr.md +75 -0
- package/.claude/commands/toggle-product-manager.md +19 -0
- package/.claude/commands/triage.md +69 -0
- package/.claude/hooks/README.md +686 -0
- package/.claude/hooks/__tests__/README.md +129 -0
- package/.claude/hooks/agent-tracker.js +434 -0
- package/.claude/hooks/antipattern-hunter-hook.js +401 -0
- package/.claude/hooks/api-key-watcher.js +289 -0
- package/.claude/hooks/block-no-verify.js +301 -0
- package/.claude/hooks/bypass-approval-hook.js +313 -0
- package/.claude/hooks/compliance-checker.js +1309 -0
- package/.claude/hooks/config-reader.js +143 -0
- package/.claude/hooks/credential-file-guard.js +1139 -0
- package/.claude/hooks/credential-health-check.js +168 -0
- package/.claude/hooks/credential-sync-hook.js +79 -0
- package/.claude/hooks/cto-notification-hook.js +656 -0
- package/.claude/hooks/feedback-launcher.js +424 -0
- package/.claude/hooks/feedback-orchestrator.js +367 -0
- package/.claude/hooks/gentyr-splash.js +47 -0
- package/.claude/hooks/gentyr-sync.js +389 -0
- package/.claude/hooks/hourly-automation.js +3340 -0
- package/.claude/hooks/key-sync.js +899 -0
- package/.claude/hooks/lib/approval-utils.js +731 -0
- package/.claude/hooks/lib/feature-branch-helper.js +102 -0
- package/.claude/hooks/lib/worktree-manager.js +330 -0
- package/.claude/hooks/mapping-validator.js +285 -0
- package/.claude/hooks/plan-executor.js +398 -0
- package/.claude/hooks/playwright-cli-guard.js +104 -0
- package/.claude/hooks/playwright-health-check.js +71 -0
- package/.claude/hooks/pre-commit-review.js +725 -0
- package/.claude/hooks/prompts/local-spec-enforcement.md +310 -0
- package/.claude/hooks/prompts/mapping-fix.md +92 -0
- package/.claude/hooks/prompts/mapping-review.md +140 -0
- package/.claude/hooks/prompts/schema-mapper.md +185 -0
- package/.claude/hooks/prompts/spec-enforcement.md +233 -0
- package/.claude/hooks/protected-action-approval-hook.js +336 -0
- package/.claude/hooks/protected-action-gate.js +562 -0
- package/.claude/hooks/protected-actions.json +208 -0
- package/.claude/hooks/protected-actions.json.template +122 -0
- package/.claude/hooks/quota-monitor.js +490 -0
- package/.claude/hooks/reporters/jest-failure-reporter.js +401 -0
- package/.claude/hooks/reporters/playwright-failure-reporter.js +446 -0
- package/.claude/hooks/reporters/vitest-failure-reporter.js +443 -0
- package/.claude/hooks/schema-mapper-hook.js +544 -0
- package/.claude/hooks/secret-leak-detector.js +216 -0
- package/.claude/hooks/session-reviver.js +514 -0
- package/.claude/hooks/slash-command-prefetch.js +1145 -0
- package/.claude/hooks/stale-work-detector.js +205 -0
- package/.claude/hooks/stop-continue-hook.js +414 -0
- package/.claude/hooks/todo-maintenance.js +522 -0
- package/.claude/hooks/todo-processing-prompt.md +75 -0
- package/.claude/hooks/usage-optimizer.js +791 -0
- package/.claude/mcp/README.md +246 -0
- package/.claude/settings.json.template +168 -0
- package/.mcp.json.template +207 -0
- package/CLAUDE.md +340 -0
- package/CLAUDE.md.gentyr-section +89 -0
- package/LICENSE +21 -0
- package/README.md +297 -0
- package/cli/commands/init.js +471 -0
- package/cli/commands/migrate.js +132 -0
- package/cli/commands/protect.js +271 -0
- package/cli/commands/scaffold.js +48 -0
- package/cli/commands/status.js +133 -0
- package/cli/commands/sync.js +101 -0
- package/cli/commands/uninstall.js +207 -0
- package/cli/index.js +111 -0
- package/cli/lib/config-gen.js +214 -0
- package/cli/lib/resolve-framework.js +97 -0
- package/cli/lib/state.js +140 -0
- package/cli/lib/symlinks.js +260 -0
- package/docs/AUTOMATION-SYSTEMS.md +484 -0
- package/docs/BINARY-PATCHING.md +212 -0
- package/docs/CHANGELOG.md +2830 -0
- package/docs/CREDENTIAL-DETECTION.md +151 -0
- package/docs/CTO-DASHBOARD.md +476 -0
- package/docs/DEPLOYMENT-FLOW.md +477 -0
- package/docs/DEVELOPER.md +116 -0
- package/docs/Executive.md +372 -0
- package/docs/SECRET-PATHS.md +77 -0
- package/docs/SETUP-GUIDE.md +419 -0
- package/docs/STACK.md +109 -0
- package/docs/TESTING.md +440 -0
- package/docs/assets/claude-logo.svg +3 -0
- package/docs/sessions/2026-01-24-spec-suite-implementation.md +190 -0
- package/docs/sessions/2026-02-15-feedback-e2e-audit.md +484 -0
- package/docs/sessions/2026-02-20-credential-rotation-experiments.md +340 -0
- package/docs/sessions/TEST-COVERAGE-REPORT-2026-02-20.md +168 -0
- package/docs/shared/EPHEMERAL-STATE-FILES.md +115 -0
- package/docs/shared/PROTECTION-SYSTEM.md +341 -0
- package/husky/post-commit +10 -0
- package/husky/pre-commit +40 -0
- package/husky/pre-push +94 -0
- package/package.json +43 -0
- package/packages/cto-dashboard/package-lock.json +3510 -0
- package/packages/cto-dashboard/package.json +41 -0
- package/packages/cto-dashboard/pnpm-lock.yaml +2168 -0
- package/packages/mcp-servers/dist/__testUtils__/fixtures.d.ts +220 -0
- package/packages/mcp-servers/dist/__testUtils__/fixtures.d.ts.map +1 -0
- package/packages/mcp-servers/dist/__testUtils__/fixtures.js +376 -0
- package/packages/mcp-servers/dist/__testUtils__/fixtures.js.map +1 -0
- package/packages/mcp-servers/dist/__testUtils__/index.d.ts +121 -0
- package/packages/mcp-servers/dist/__testUtils__/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/__testUtils__/index.js +180 -0
- package/packages/mcp-servers/dist/__testUtils__/index.js.map +1 -0
- package/packages/mcp-servers/dist/__testUtils__/schemas.d.ts +84 -0
- package/packages/mcp-servers/dist/__testUtils__/schemas.d.ts.map +1 -0
- package/packages/mcp-servers/dist/__testUtils__/schemas.js +309 -0
- package/packages/mcp-servers/dist/__testUtils__/schemas.js.map +1 -0
- package/packages/mcp-servers/dist/agent-reports/index.d.ts +7 -0
- package/packages/mcp-servers/dist/agent-reports/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/agent-reports/index.js +8 -0
- package/packages/mcp-servers/dist/agent-reports/index.js.map +1 -0
- package/packages/mcp-servers/dist/agent-reports/server.d.ts +22 -0
- package/packages/mcp-servers/dist/agent-reports/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/agent-reports/server.js +535 -0
- package/packages/mcp-servers/dist/agent-reports/server.js.map +1 -0
- package/packages/mcp-servers/dist/agent-reports/types.d.ts +258 -0
- package/packages/mcp-servers/dist/agent-reports/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/agent-reports/types.js +81 -0
- package/packages/mcp-servers/dist/agent-reports/types.js.map +1 -0
- package/packages/mcp-servers/dist/agent-tracker/index.d.ts +5 -0
- package/packages/mcp-servers/dist/agent-tracker/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/agent-tracker/index.js +5 -0
- package/packages/mcp-servers/dist/agent-tracker/index.js.map +1 -0
- package/packages/mcp-servers/dist/agent-tracker/server.d.ts +12 -0
- package/packages/mcp-servers/dist/agent-tracker/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/agent-tracker/server.js +919 -0
- package/packages/mcp-servers/dist/agent-tracker/server.js.map +1 -0
- package/packages/mcp-servers/dist/agent-tracker/types.d.ts +328 -0
- package/packages/mcp-servers/dist/agent-tracker/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/agent-tracker/types.js +128 -0
- package/packages/mcp-servers/dist/agent-tracker/types.js.map +1 -0
- package/packages/mcp-servers/dist/chrome-bridge/browser-tips.d.ts +27 -0
- package/packages/mcp-servers/dist/chrome-bridge/browser-tips.d.ts.map +1 -0
- package/packages/mcp-servers/dist/chrome-bridge/browser-tips.js +167 -0
- package/packages/mcp-servers/dist/chrome-bridge/browser-tips.js.map +1 -0
- package/packages/mcp-servers/dist/chrome-bridge/index.d.ts +6 -0
- package/packages/mcp-servers/dist/chrome-bridge/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/chrome-bridge/index.js +6 -0
- package/packages/mcp-servers/dist/chrome-bridge/index.js.map +1 -0
- package/packages/mcp-servers/dist/chrome-bridge/server.d.ts +13 -0
- package/packages/mcp-servers/dist/chrome-bridge/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/chrome-bridge/server.js +959 -0
- package/packages/mcp-servers/dist/chrome-bridge/server.js.map +1 -0
- package/packages/mcp-servers/dist/chrome-bridge/types.d.ts +41 -0
- package/packages/mcp-servers/dist/chrome-bridge/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/chrome-bridge/types.js +8 -0
- package/packages/mcp-servers/dist/chrome-bridge/types.js.map +1 -0
- package/packages/mcp-servers/dist/cloudflare/index.d.ts +8 -0
- package/packages/mcp-servers/dist/cloudflare/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/cloudflare/index.js +8 -0
- package/packages/mcp-servers/dist/cloudflare/index.js.map +1 -0
- package/packages/mcp-servers/dist/cloudflare/server.d.ts +16 -0
- package/packages/mcp-servers/dist/cloudflare/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/cloudflare/server.js +253 -0
- package/packages/mcp-servers/dist/cloudflare/server.js.map +1 -0
- package/packages/mcp-servers/dist/cloudflare/types.d.ts +141 -0
- package/packages/mcp-servers/dist/cloudflare/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/cloudflare/types.js +53 -0
- package/packages/mcp-servers/dist/cloudflare/types.js.map +1 -0
- package/packages/mcp-servers/dist/codecov/index.d.ts +7 -0
- package/packages/mcp-servers/dist/codecov/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/codecov/index.js +7 -0
- package/packages/mcp-servers/dist/codecov/index.js.map +1 -0
- package/packages/mcp-servers/dist/codecov/server.d.ts +21 -0
- package/packages/mcp-servers/dist/codecov/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/codecov/server.js +376 -0
- package/packages/mcp-servers/dist/codecov/server.js.map +1 -0
- package/packages/mcp-servers/dist/codecov/types.d.ts +269 -0
- package/packages/mcp-servers/dist/codecov/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/codecov/types.js +128 -0
- package/packages/mcp-servers/dist/codecov/types.js.map +1 -0
- package/packages/mcp-servers/dist/cto-report/index.d.ts +9 -0
- package/packages/mcp-servers/dist/cto-report/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/cto-report/index.js +9 -0
- package/packages/mcp-servers/dist/cto-report/index.js.map +1 -0
- package/packages/mcp-servers/dist/cto-report/server.d.ts +14 -0
- package/packages/mcp-servers/dist/cto-report/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/cto-report/server.js +859 -0
- package/packages/mcp-servers/dist/cto-report/server.js.map +1 -0
- package/packages/mcp-servers/dist/cto-report/types.d.ts +213 -0
- package/packages/mcp-servers/dist/cto-report/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/cto-report/types.js +29 -0
- package/packages/mcp-servers/dist/cto-report/types.js.map +1 -0
- package/packages/mcp-servers/dist/cto-reports/index.d.ts +7 -0
- package/packages/mcp-servers/dist/cto-reports/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/cto-reports/index.js +8 -0
- package/packages/mcp-servers/dist/cto-reports/index.js.map +1 -0
- package/packages/mcp-servers/dist/cto-reports/server.d.ts +20 -0
- package/packages/mcp-servers/dist/cto-reports/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/cto-reports/server.js +538 -0
- package/packages/mcp-servers/dist/cto-reports/server.js.map +1 -0
- package/packages/mcp-servers/dist/cto-reports/types.d.ts +236 -0
- package/packages/mcp-servers/dist/cto-reports/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/cto-reports/types.js +77 -0
- package/packages/mcp-servers/dist/cto-reports/types.js.map +1 -0
- package/packages/mcp-servers/dist/deputy-cto/index.d.ts +7 -0
- package/packages/mcp-servers/dist/deputy-cto/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/deputy-cto/index.js +8 -0
- package/packages/mcp-servers/dist/deputy-cto/index.js.map +1 -0
- package/packages/mcp-servers/dist/deputy-cto/server.d.ts +23 -0
- package/packages/mcp-servers/dist/deputy-cto/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/deputy-cto/server.js +1700 -0
- package/packages/mcp-servers/dist/deputy-cto/server.js.map +1 -0
- package/packages/mcp-servers/dist/deputy-cto/types.d.ts +439 -0
- package/packages/mcp-servers/dist/deputy-cto/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/deputy-cto/types.js +102 -0
- package/packages/mcp-servers/dist/deputy-cto/types.js.map +1 -0
- package/packages/mcp-servers/dist/elastic-logs/index.d.ts +5 -0
- package/packages/mcp-servers/dist/elastic-logs/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/elastic-logs/index.js +5 -0
- package/packages/mcp-servers/dist/elastic-logs/index.js.map +1 -0
- package/packages/mcp-servers/dist/elastic-logs/server.d.ts +18 -0
- package/packages/mcp-servers/dist/elastic-logs/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/elastic-logs/server.js +259 -0
- package/packages/mcp-servers/dist/elastic-logs/server.js.map +1 -0
- package/packages/mcp-servers/dist/elastic-logs/types.d.ts +107 -0
- package/packages/mcp-servers/dist/elastic-logs/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/elastic-logs/types.js +31 -0
- package/packages/mcp-servers/dist/elastic-logs/types.js.map +1 -0
- package/packages/mcp-servers/dist/feedback-explorer/index.d.ts +2 -0
- package/packages/mcp-servers/dist/feedback-explorer/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/feedback-explorer/index.js +2 -0
- package/packages/mcp-servers/dist/feedback-explorer/index.js.map +1 -0
- package/packages/mcp-servers/dist/feedback-explorer/server.d.ts +21 -0
- package/packages/mcp-servers/dist/feedback-explorer/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/feedback-explorer/server.js +580 -0
- package/packages/mcp-servers/dist/feedback-explorer/server.js.map +1 -0
- package/packages/mcp-servers/dist/feedback-explorer/types.d.ts +331 -0
- package/packages/mcp-servers/dist/feedback-explorer/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/feedback-explorer/types.js +40 -0
- package/packages/mcp-servers/dist/feedback-explorer/types.js.map +1 -0
- package/packages/mcp-servers/dist/feedback-reporter/index.d.ts +9 -0
- package/packages/mcp-servers/dist/feedback-reporter/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/feedback-reporter/index.js +9 -0
- package/packages/mcp-servers/dist/feedback-reporter/index.js.map +1 -0
- package/packages/mcp-servers/dist/feedback-reporter/server.d.ts +36 -0
- package/packages/mcp-servers/dist/feedback-reporter/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/feedback-reporter/server.js +392 -0
- package/packages/mcp-servers/dist/feedback-reporter/server.js.map +1 -0
- package/packages/mcp-servers/dist/feedback-reporter/types.d.ts +152 -0
- package/packages/mcp-servers/dist/feedback-reporter/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/feedback-reporter/types.js +67 -0
- package/packages/mcp-servers/dist/feedback-reporter/types.js.map +1 -0
- package/packages/mcp-servers/dist/github/index.d.ts +7 -0
- package/packages/mcp-servers/dist/github/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/github/index.js +7 -0
- package/packages/mcp-servers/dist/github/index.js.map +1 -0
- package/packages/mcp-servers/dist/github/server.d.ts +15 -0
- package/packages/mcp-servers/dist/github/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/github/server.js +686 -0
- package/packages/mcp-servers/dist/github/server.js.map +1 -0
- package/packages/mcp-servers/dist/github/types.d.ts +660 -0
- package/packages/mcp-servers/dist/github/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/github/types.js +209 -0
- package/packages/mcp-servers/dist/github/types.js.map +1 -0
- package/packages/mcp-servers/dist/index.d.ts +30 -0
- package/packages/mcp-servers/dist/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/index.js +32 -0
- package/packages/mcp-servers/dist/index.js.map +1 -0
- package/packages/mcp-servers/dist/makerkit-docs/index.d.ts +5 -0
- package/packages/mcp-servers/dist/makerkit-docs/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/makerkit-docs/index.js +5 -0
- package/packages/mcp-servers/dist/makerkit-docs/index.js.map +1 -0
- package/packages/mcp-servers/dist/makerkit-docs/server.d.ts +15 -0
- package/packages/mcp-servers/dist/makerkit-docs/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/makerkit-docs/server.js +252 -0
- package/packages/mcp-servers/dist/makerkit-docs/server.js.map +1 -0
- package/packages/mcp-servers/dist/makerkit-docs/types.d.ts +74 -0
- package/packages/mcp-servers/dist/makerkit-docs/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/makerkit-docs/types.js +20 -0
- package/packages/mcp-servers/dist/makerkit-docs/types.js.map +1 -0
- package/packages/mcp-servers/dist/onepassword/index.d.ts +2 -0
- package/packages/mcp-servers/dist/onepassword/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/onepassword/index.js +2 -0
- package/packages/mcp-servers/dist/onepassword/index.js.map +1 -0
- package/packages/mcp-servers/dist/onepassword/server.d.ts +2 -0
- package/packages/mcp-servers/dist/onepassword/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/onepassword/server.js +159 -0
- package/packages/mcp-servers/dist/onepassword/server.js.map +1 -0
- package/packages/mcp-servers/dist/onepassword/types.d.ts +55 -0
- package/packages/mcp-servers/dist/onepassword/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/onepassword/types.js +22 -0
- package/packages/mcp-servers/dist/onepassword/types.js.map +1 -0
- package/packages/mcp-servers/dist/playwright/helpers.d.ts +20 -0
- package/packages/mcp-servers/dist/playwright/helpers.d.ts.map +1 -0
- package/packages/mcp-servers/dist/playwright/helpers.js +31 -0
- package/packages/mcp-servers/dist/playwright/helpers.js.map +1 -0
- package/packages/mcp-servers/dist/playwright/index.d.ts +5 -0
- package/packages/mcp-servers/dist/playwright/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/playwright/index.js +5 -0
- package/packages/mcp-servers/dist/playwright/index.js.map +1 -0
- package/packages/mcp-servers/dist/playwright/server.d.ts +13 -0
- package/packages/mcp-servers/dist/playwright/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/playwright/server.js +1201 -0
- package/packages/mcp-servers/dist/playwright/server.js.map +1 -0
- package/packages/mcp-servers/dist/playwright/types.d.ts +216 -0
- package/packages/mcp-servers/dist/playwright/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/playwright/types.js +172 -0
- package/packages/mcp-servers/dist/playwright/types.js.map +1 -0
- package/packages/mcp-servers/dist/playwright-feedback/browser-manager.d.ts +39 -0
- package/packages/mcp-servers/dist/playwright-feedback/browser-manager.d.ts.map +1 -0
- package/packages/mcp-servers/dist/playwright-feedback/browser-manager.js +71 -0
- package/packages/mcp-servers/dist/playwright-feedback/browser-manager.js.map +1 -0
- package/packages/mcp-servers/dist/playwright-feedback/index.d.ts +5 -0
- package/packages/mcp-servers/dist/playwright-feedback/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/playwright-feedback/index.js +5 -0
- package/packages/mcp-servers/dist/playwright-feedback/index.js.map +1 -0
- package/packages/mcp-servers/dist/playwright-feedback/server.d.ts +34 -0
- package/packages/mcp-servers/dist/playwright-feedback/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/playwright-feedback/server.js +538 -0
- package/packages/mcp-servers/dist/playwright-feedback/server.js.map +1 -0
- package/packages/mcp-servers/dist/playwright-feedback/types.d.ts +305 -0
- package/packages/mcp-servers/dist/playwright-feedback/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/playwright-feedback/types.js +123 -0
- package/packages/mcp-servers/dist/playwright-feedback/types.js.map +1 -0
- package/packages/mcp-servers/dist/product-manager/server.d.ts +17 -0
- package/packages/mcp-servers/dist/product-manager/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/product-manager/server.js +690 -0
- package/packages/mcp-servers/dist/product-manager/server.js.map +1 -0
- package/packages/mcp-servers/dist/product-manager/types.d.ts +286 -0
- package/packages/mcp-servers/dist/product-manager/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/product-manager/types.js +99 -0
- package/packages/mcp-servers/dist/product-manager/types.js.map +1 -0
- package/packages/mcp-servers/dist/programmatic-feedback/index.d.ts +7 -0
- package/packages/mcp-servers/dist/programmatic-feedback/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/programmatic-feedback/index.js +7 -0
- package/packages/mcp-servers/dist/programmatic-feedback/index.js.map +1 -0
- package/packages/mcp-servers/dist/programmatic-feedback/sandbox.d.ts +19 -0
- package/packages/mcp-servers/dist/programmatic-feedback/sandbox.d.ts.map +1 -0
- package/packages/mcp-servers/dist/programmatic-feedback/sandbox.js +174 -0
- package/packages/mcp-servers/dist/programmatic-feedback/sandbox.js.map +1 -0
- package/packages/mcp-servers/dist/programmatic-feedback/server.d.ts +35 -0
- package/packages/mcp-servers/dist/programmatic-feedback/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/programmatic-feedback/server.js +465 -0
- package/packages/mcp-servers/dist/programmatic-feedback/server.js.map +1 -0
- package/packages/mcp-servers/dist/programmatic-feedback/types.d.ts +127 -0
- package/packages/mcp-servers/dist/programmatic-feedback/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/programmatic-feedback/types.js +80 -0
- package/packages/mcp-servers/dist/programmatic-feedback/types.js.map +1 -0
- package/packages/mcp-servers/dist/render/index.d.ts +8 -0
- package/packages/mcp-servers/dist/render/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/render/index.js +8 -0
- package/packages/mcp-servers/dist/render/index.js.map +1 -0
- package/packages/mcp-servers/dist/render/server.d.ts +15 -0
- package/packages/mcp-servers/dist/render/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/render/server.js +428 -0
- package/packages/mcp-servers/dist/render/server.js.map +1 -0
- package/packages/mcp-servers/dist/render/types.d.ts +273 -0
- package/packages/mcp-servers/dist/render/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/render/types.js +102 -0
- package/packages/mcp-servers/dist/render/types.js.map +1 -0
- package/packages/mcp-servers/dist/resend/index.d.ts +7 -0
- package/packages/mcp-servers/dist/resend/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/resend/index.js +7 -0
- package/packages/mcp-servers/dist/resend/index.js.map +1 -0
- package/packages/mcp-servers/dist/resend/server.d.ts +15 -0
- package/packages/mcp-servers/dist/resend/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/resend/server.js +298 -0
- package/packages/mcp-servers/dist/resend/server.js.map +1 -0
- package/packages/mcp-servers/dist/resend/types.d.ts +222 -0
- package/packages/mcp-servers/dist/resend/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/resend/types.js +58 -0
- package/packages/mcp-servers/dist/resend/types.js.map +1 -0
- package/packages/mcp-servers/dist/review-queue/index.d.ts +6 -0
- package/packages/mcp-servers/dist/review-queue/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/review-queue/index.js +6 -0
- package/packages/mcp-servers/dist/review-queue/index.js.map +1 -0
- package/packages/mcp-servers/dist/review-queue/server.d.ts +17 -0
- package/packages/mcp-servers/dist/review-queue/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/review-queue/server.js +348 -0
- package/packages/mcp-servers/dist/review-queue/server.js.map +1 -0
- package/packages/mcp-servers/dist/review-queue/types.d.ts +162 -0
- package/packages/mcp-servers/dist/review-queue/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/review-queue/types.js +56 -0
- package/packages/mcp-servers/dist/review-queue/types.js.map +1 -0
- package/packages/mcp-servers/dist/secret-sync/server.d.ts +19 -0
- package/packages/mcp-servers/dist/secret-sync/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/secret-sync/server.js +1139 -0
- package/packages/mcp-servers/dist/secret-sync/server.js.map +1 -0
- package/packages/mcp-servers/dist/secret-sync/types.d.ts +442 -0
- package/packages/mcp-servers/dist/secret-sync/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/secret-sync/types.js +113 -0
- package/packages/mcp-servers/dist/secret-sync/types.js.map +1 -0
- package/packages/mcp-servers/dist/session-events/index.d.ts +5 -0
- package/packages/mcp-servers/dist/session-events/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/session-events/index.js +5 -0
- package/packages/mcp-servers/dist/session-events/index.js.map +1 -0
- package/packages/mcp-servers/dist/session-events/server.d.ts +11 -0
- package/packages/mcp-servers/dist/session-events/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/session-events/server.js +290 -0
- package/packages/mcp-servers/dist/session-events/server.js.map +1 -0
- package/packages/mcp-servers/dist/session-events/types.d.ts +213 -0
- package/packages/mcp-servers/dist/session-events/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/session-events/types.js +69 -0
- package/packages/mcp-servers/dist/session-events/types.js.map +1 -0
- package/packages/mcp-servers/dist/session-restart/index.d.ts +9 -0
- package/packages/mcp-servers/dist/session-restart/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/session-restart/index.js +9 -0
- package/packages/mcp-servers/dist/session-restart/index.js.map +1 -0
- package/packages/mcp-servers/dist/session-restart/server.d.ts +20 -0
- package/packages/mcp-servers/dist/session-restart/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/session-restart/server.js +411 -0
- package/packages/mcp-servers/dist/session-restart/server.js.map +1 -0
- package/packages/mcp-servers/dist/session-restart/types.d.ts +26 -0
- package/packages/mcp-servers/dist/session-restart/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/session-restart/types.js +16 -0
- package/packages/mcp-servers/dist/session-restart/types.js.map +1 -0
- package/packages/mcp-servers/dist/setup-helper/index.d.ts +5 -0
- package/packages/mcp-servers/dist/setup-helper/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/setup-helper/index.js +5 -0
- package/packages/mcp-servers/dist/setup-helper/index.js.map +1 -0
- package/packages/mcp-servers/dist/setup-helper/server.d.ts +14 -0
- package/packages/mcp-servers/dist/setup-helper/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/setup-helper/server.js +454 -0
- package/packages/mcp-servers/dist/setup-helper/server.js.map +1 -0
- package/packages/mcp-servers/dist/setup-helper/types.d.ts +81 -0
- package/packages/mcp-servers/dist/setup-helper/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/setup-helper/types.js +41 -0
- package/packages/mcp-servers/dist/setup-helper/types.js.map +1 -0
- package/packages/mcp-servers/dist/shared/audited-server.d.ts +31 -0
- package/packages/mcp-servers/dist/shared/audited-server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/shared/audited-server.js +126 -0
- package/packages/mcp-servers/dist/shared/audited-server.js.map +1 -0
- package/packages/mcp-servers/dist/shared/constants.d.ts +26 -0
- package/packages/mcp-servers/dist/shared/constants.d.ts.map +1 -0
- package/packages/mcp-servers/dist/shared/constants.js +41 -0
- package/packages/mcp-servers/dist/shared/constants.js.map +1 -0
- package/packages/mcp-servers/dist/shared/index.d.ts +6 -0
- package/packages/mcp-servers/dist/shared/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/shared/index.js +6 -0
- package/packages/mcp-servers/dist/shared/index.js.map +1 -0
- package/packages/mcp-servers/dist/shared/readonly-db.d.ts +11 -0
- package/packages/mcp-servers/dist/shared/readonly-db.d.ts.map +1 -0
- package/packages/mcp-servers/dist/shared/readonly-db.js +47 -0
- package/packages/mcp-servers/dist/shared/readonly-db.js.map +1 -0
- package/packages/mcp-servers/dist/shared/resolve-framework.d.ts +20 -0
- package/packages/mcp-servers/dist/shared/resolve-framework.d.ts.map +1 -0
- package/packages/mcp-servers/dist/shared/resolve-framework.js +65 -0
- package/packages/mcp-servers/dist/shared/resolve-framework.js.map +1 -0
- package/packages/mcp-servers/dist/shared/server.d.ts +86 -0
- package/packages/mcp-servers/dist/shared/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/shared/server.js +291 -0
- package/packages/mcp-servers/dist/shared/server.js.map +1 -0
- package/packages/mcp-servers/dist/shared/types.d.ts +113 -0
- package/packages/mcp-servers/dist/shared/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/shared/types.js +36 -0
- package/packages/mcp-servers/dist/shared/types.js.map +1 -0
- package/packages/mcp-servers/dist/show/server.d.ts +12 -0
- package/packages/mcp-servers/dist/show/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/show/server.js +97 -0
- package/packages/mcp-servers/dist/show/server.js.map +1 -0
- package/packages/mcp-servers/dist/show/types.d.ts +19 -0
- package/packages/mcp-servers/dist/show/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/show/types.js +32 -0
- package/packages/mcp-servers/dist/show/types.js.map +1 -0
- package/packages/mcp-servers/dist/specs-browser/index.d.ts +5 -0
- package/packages/mcp-servers/dist/specs-browser/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/specs-browser/index.js +5 -0
- package/packages/mcp-servers/dist/specs-browser/index.js.map +1 -0
- package/packages/mcp-servers/dist/specs-browser/server.d.ts +13 -0
- package/packages/mcp-servers/dist/specs-browser/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/specs-browser/server.js +692 -0
- package/packages/mcp-servers/dist/specs-browser/server.js.map +1 -0
- package/packages/mcp-servers/dist/specs-browser/types.d.ts +337 -0
- package/packages/mcp-servers/dist/specs-browser/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/specs-browser/types.js +134 -0
- package/packages/mcp-servers/dist/specs-browser/types.js.map +1 -0
- package/packages/mcp-servers/dist/supabase/index.d.ts +10 -0
- package/packages/mcp-servers/dist/supabase/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/supabase/index.js +10 -0
- package/packages/mcp-servers/dist/supabase/index.js.map +1 -0
- package/packages/mcp-servers/dist/supabase/server.d.ts +20 -0
- package/packages/mcp-servers/dist/supabase/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/supabase/server.js +451 -0
- package/packages/mcp-servers/dist/supabase/server.js.map +1 -0
- package/packages/mcp-servers/dist/supabase/types.d.ts +196 -0
- package/packages/mcp-servers/dist/supabase/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/supabase/types.js +76 -0
- package/packages/mcp-servers/dist/supabase/types.js.map +1 -0
- package/packages/mcp-servers/dist/todo-db/index.d.ts +5 -0
- package/packages/mcp-servers/dist/todo-db/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/todo-db/index.js +5 -0
- package/packages/mcp-servers/dist/todo-db/index.js.map +1 -0
- package/packages/mcp-servers/dist/todo-db/server.d.ts +13 -0
- package/packages/mcp-servers/dist/todo-db/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/todo-db/server.js +649 -0
- package/packages/mcp-servers/dist/todo-db/server.js.map +1 -0
- package/packages/mcp-servers/dist/todo-db/types.d.ts +225 -0
- package/packages/mcp-servers/dist/todo-db/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/todo-db/types.js +69 -0
- package/packages/mcp-servers/dist/todo-db/types.js.map +1 -0
- package/packages/mcp-servers/dist/user-feedback/index.d.ts +7 -0
- package/packages/mcp-servers/dist/user-feedback/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/user-feedback/index.js +8 -0
- package/packages/mcp-servers/dist/user-feedback/index.js.map +1 -0
- package/packages/mcp-servers/dist/user-feedback/server.d.ts +25 -0
- package/packages/mcp-servers/dist/user-feedback/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/user-feedback/server.js +914 -0
- package/packages/mcp-servers/dist/user-feedback/server.js.map +1 -0
- package/packages/mcp-servers/dist/user-feedback/types.d.ts +415 -0
- package/packages/mcp-servers/dist/user-feedback/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/user-feedback/types.js +132 -0
- package/packages/mcp-servers/dist/user-feedback/types.js.map +1 -0
- package/packages/mcp-servers/dist/vercel/index.d.ts +9 -0
- package/packages/mcp-servers/dist/vercel/index.d.ts.map +1 -0
- package/packages/mcp-servers/dist/vercel/index.js +9 -0
- package/packages/mcp-servers/dist/vercel/index.js.map +1 -0
- package/packages/mcp-servers/dist/vercel/server.d.ts +17 -0
- package/packages/mcp-servers/dist/vercel/server.d.ts.map +1 -0
- package/packages/mcp-servers/dist/vercel/server.js +265 -0
- package/packages/mcp-servers/dist/vercel/server.js.map +1 -0
- package/packages/mcp-servers/dist/vercel/types.d.ts +189 -0
- package/packages/mcp-servers/dist/vercel/types.d.ts.map +1 -0
- package/packages/mcp-servers/dist/vercel/types.js +65 -0
- package/packages/mcp-servers/dist/vercel/types.js.map +1 -0
- package/packages/mcp-servers/package-lock.json +3765 -0
- package/packages/mcp-servers/package.json +64 -0
- package/packages/mcp-servers/test/reporters/test-failure-reporter.ts +372 -0
- package/packages/mcp-servers/vitest.config.ts +27 -0
- package/scripts/__tests__/README.md +163 -0
- package/scripts/apply-credential-hardening.sh +271 -0
- package/scripts/credential-providers/manual.js +56 -0
- package/scripts/credential-providers/onepassword.js +85 -0
- package/scripts/credential-providers/provider-interface.js +104 -0
- package/scripts/encrypt-credential.js +337 -0
- package/scripts/feedback-launcher.js +338 -0
- package/scripts/feedback-orchestrator.js +373 -0
- package/scripts/fix-mcp-launcher-issues.sh +97 -0
- package/scripts/force-spawn-tasks.js +651 -0
- package/scripts/force-triage-reports.js +560 -0
- package/scripts/generate-protected-actions-spec.js +142 -0
- package/scripts/generate-proxy-certs.sh +158 -0
- package/scripts/grant-chrome-ext-permissions.sh +242 -0
- package/scripts/mcp-launcher.js +125 -0
- package/scripts/merge-settings.cjs +167 -0
- package/scripts/patch-clawd.py +844 -0
- package/scripts/patch-credential-cache.py +313 -0
- package/scripts/patches/credential-file-guard-patched.mjs +573 -0
- package/scripts/patches/credential-file-guard.js.patched +573 -0
- package/scripts/patches/verify-tokenizer.mjs +132 -0
- package/scripts/protect-framework.sh +478 -0
- package/scripts/readme-chrome.template +12 -0
- package/scripts/reap-completed-agents.js +439 -0
- package/scripts/reinstall.sh +86 -0
- package/scripts/resign-node.sh +185 -0
- package/scripts/rotation-proxy.js +656 -0
- package/scripts/rotation-stress-monitor.mjs +862 -0
- package/scripts/setup-automation-service.sh +648 -0
- package/scripts/setup-check.js +251 -0
- package/scripts/watch-claude-version.js +142 -0
- package/specs/framework/CORE-INVARIANTS.md +161 -0
- package/specs/patterns/AGENT-PATTERNS.md +223 -0
- package/specs/patterns/HOOK-PATTERNS.md +242 -0
- package/specs/patterns/MCP-SERVER-PATTERNS.md +144 -0
- package/templates/config/gitignore.template +14 -0
- package/templates/config/merge-chain-check.yml.template +51 -0
- package/templates/config/package.json.template +18 -0
- package/templates/config/pnpm-workspace.yaml +5 -0
- package/templates/config/services.json.template +18 -0
- package/templates/config/tsconfig.base.json +17 -0
- package/templates/scaffold/integrations/_template/.gitkeep +0 -0
- package/templates/scaffold/packages/logger/package.json +17 -0
- package/templates/scaffold/packages/logger/src/logger.ts +44 -0
- package/templates/scaffold/packages/shared/package.json +17 -0
- package/templates/scaffold/packages/shared/src/errors.ts +43 -0
- package/templates/scaffold/products/_product/apps/backend/package.json +21 -0
- package/templates/scaffold/products/_product/apps/backend/src/index.ts +17 -0
- package/templates/scaffold/products/_product/apps/extension/.gitkeep +0 -0
- package/templates/scaffold/products/_product/apps/web/.gitkeep +0 -0
- package/templates/scaffold/specs/global/.gitkeep +0 -0
- package/templates/scaffold/specs/local/.gitkeep +0 -0
- package/templates/scaffold/specs/reference/.gitkeep +0 -0
- package/version.json +15 -0
|
@@ -0,0 +1,1201 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Playwright E2E MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Provides MCP tools for interacting with the Playwright E2E test
|
|
6
|
+
* infrastructure: launching UI mode for demos, running tests headlessly,
|
|
7
|
+
* seeding/cleaning test data, and checking coverage status.
|
|
8
|
+
*
|
|
9
|
+
* @see specs/global/G028-playwright-e2e-testing.md
|
|
10
|
+
* @version 1.0.0
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import * as os from 'os';
|
|
15
|
+
import * as http from 'http';
|
|
16
|
+
import * as net from 'net';
|
|
17
|
+
import * as crypto from 'crypto';
|
|
18
|
+
import { spawn, execFileSync } from 'child_process';
|
|
19
|
+
import { McpServer } from '../shared/server.js';
|
|
20
|
+
import { LaunchUiModeArgsSchema, RunTestsArgsSchema, SeedDataArgsSchema, CleanupDataArgsSchema, GetReportArgsSchema, GetCoverageStatusArgsSchema, PreflightCheckArgsSchema, ListExtensionTabsArgsSchema, ScreenshotExtensionTabArgsSchema, RunAuthSetupArgsSchema, } from './types.js';
|
|
21
|
+
import { parseTestOutput, truncateOutput } from './helpers.js';
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Configuration
|
|
24
|
+
// ============================================================================
|
|
25
|
+
const PROJECT_DIR = path.resolve(process.env.CLAUDE_PROJECT_DIR || process.cwd());
|
|
26
|
+
const REPORT_DIR = path.join(PROJECT_DIR, 'playwright-report');
|
|
27
|
+
const RUN_TIMEOUT = 300_000; // 5 minutes for test runs
|
|
28
|
+
// Map standard env names to NEXT_PUBLIC_ variants for Next.js webServer.
|
|
29
|
+
// Credentials are injected by mcp-launcher.js from 1Password at runtime.
|
|
30
|
+
if (process.env.SUPABASE_URL && !process.env.NEXT_PUBLIC_SUPABASE_URL) {
|
|
31
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL = process.env.SUPABASE_URL;
|
|
32
|
+
}
|
|
33
|
+
if (process.env.SUPABASE_ANON_KEY && !process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) {
|
|
34
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Kill any processes listening on the dev server port (default 3000).
|
|
38
|
+
* Prevents zombie servers from previous runs causing port conflicts.
|
|
39
|
+
*/
|
|
40
|
+
function cleanupDevServerPort(port = 3000) {
|
|
41
|
+
try {
|
|
42
|
+
const lsofOutput = execFileSync('lsof', ['-ti', `:${port}`], {
|
|
43
|
+
encoding: 'utf8',
|
|
44
|
+
timeout: 5000,
|
|
45
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
46
|
+
}).trim();
|
|
47
|
+
if (lsofOutput) {
|
|
48
|
+
const pids = lsofOutput.split('\n').filter(Boolean);
|
|
49
|
+
for (const pid of pids) {
|
|
50
|
+
try {
|
|
51
|
+
process.kill(Number(pid), 'SIGTERM');
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Process may have already exited
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// No processes on port — nothing to clean up
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Validate that required environment variables are set and resolved.
|
|
65
|
+
* Catches broken 1Password injection (op:// references still present)
|
|
66
|
+
* and missing Supabase credentials before spawning Playwright.
|
|
67
|
+
*/
|
|
68
|
+
function validatePrerequisites() {
|
|
69
|
+
const errors = [];
|
|
70
|
+
const warnings = [];
|
|
71
|
+
const required = ['SUPABASE_URL', 'SUPABASE_ANON_KEY', 'SUPABASE_SERVICE_ROLE_KEY'];
|
|
72
|
+
for (const name of required) {
|
|
73
|
+
const value = process.env[name];
|
|
74
|
+
if (!value) {
|
|
75
|
+
errors.push(`${name} is not set`);
|
|
76
|
+
}
|
|
77
|
+
else if (value.startsWith('op://')) {
|
|
78
|
+
errors.push(`${name} contains unresolved 1Password reference (op:// prefix detected)`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const optional = ['NEXT_PUBLIC_SUPABASE_URL', 'NEXT_PUBLIC_SUPABASE_ANON_KEY'];
|
|
82
|
+
for (const name of optional) {
|
|
83
|
+
const value = process.env[name];
|
|
84
|
+
if (!value) {
|
|
85
|
+
warnings.push(`${name} is not set (will be derived from SUPABASE_* if available)`);
|
|
86
|
+
}
|
|
87
|
+
else if (value.startsWith('op://')) {
|
|
88
|
+
errors.push(`${name} contains unresolved 1Password reference (op:// prefix detected)`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
92
|
+
}
|
|
93
|
+
/** Persona descriptions for coverage reporting */
|
|
94
|
+
const PERSONA_MAP = {
|
|
95
|
+
'vendor-owner': 'SaaS Vendor (Owner)',
|
|
96
|
+
'vendor-admin': 'SaaS Vendor (Admin)',
|
|
97
|
+
'vendor-dev': 'SaaS Vendor (Developer)',
|
|
98
|
+
'vendor-viewer': 'SaaS Vendor (Viewer)',
|
|
99
|
+
'cross-persona': 'Cross-Persona Workflows',
|
|
100
|
+
'auth-flows': 'Auth Flows (No Pre-auth)',
|
|
101
|
+
'manual': 'Manual QA Scaffolds',
|
|
102
|
+
'extension': 'End Customer (Extension)',
|
|
103
|
+
'extension-manual': 'Extension Manual QA',
|
|
104
|
+
'demo': 'Unified Demo (Dashboard + Extension)',
|
|
105
|
+
};
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// Tool Implementations
|
|
108
|
+
// ============================================================================
|
|
109
|
+
/**
|
|
110
|
+
* Launch Playwright in interactive UI mode.
|
|
111
|
+
* Validates prerequisites, spawns a detached process, and monitors for early crashes.
|
|
112
|
+
*/
|
|
113
|
+
async function launchUiMode(args) {
|
|
114
|
+
const { project, base_url } = args;
|
|
115
|
+
// Pre-flight validation
|
|
116
|
+
const preflight = validatePrerequisites();
|
|
117
|
+
if (!preflight.ok) {
|
|
118
|
+
return {
|
|
119
|
+
success: false,
|
|
120
|
+
project,
|
|
121
|
+
message: `Environment validation failed:\n${preflight.errors.map(e => ` - ${e}`).join('\n')}`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const cmdArgs = ['playwright', 'test', '--project', project, '--ui'];
|
|
125
|
+
const env = { ...process.env };
|
|
126
|
+
if (base_url) {
|
|
127
|
+
env.PLAYWRIGHT_BASE_URL = base_url;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const child = spawn('npx', cmdArgs, {
|
|
131
|
+
detached: true,
|
|
132
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
133
|
+
cwd: PROJECT_DIR,
|
|
134
|
+
env,
|
|
135
|
+
});
|
|
136
|
+
// Collect stderr for crash diagnostics
|
|
137
|
+
let stderrChunks = [];
|
|
138
|
+
if (child.stderr) {
|
|
139
|
+
child.stderr.on('data', (chunk) => {
|
|
140
|
+
stderrChunks.push(chunk);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// Wait up to 3s for early crash detection
|
|
144
|
+
const earlyExit = await new Promise((resolve) => {
|
|
145
|
+
const timer = setTimeout(() => resolve(null), 3000);
|
|
146
|
+
child.on('exit', (code, signal) => {
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
resolve({ code, signal });
|
|
149
|
+
});
|
|
150
|
+
child.on('error', (err) => {
|
|
151
|
+
clearTimeout(timer);
|
|
152
|
+
resolve({ code: 1, signal: err.message });
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
if (earlyExit) {
|
|
156
|
+
// Process died within 3s — report the failure
|
|
157
|
+
const stderr = Buffer.concat(stderrChunks).toString('utf8').trim();
|
|
158
|
+
const snippet = stderr.length > 500 ? stderr.slice(0, 500) + '...' : stderr;
|
|
159
|
+
return {
|
|
160
|
+
success: false,
|
|
161
|
+
project,
|
|
162
|
+
message: `Playwright process crashed within 3s (exit code: ${earlyExit.code}, signal: ${earlyExit.signal})${snippet ? `\nstderr: ${snippet}` : ''}`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
// Still running after 3s — detach and return success
|
|
166
|
+
if (child.stderr) {
|
|
167
|
+
child.stderr.destroy();
|
|
168
|
+
}
|
|
169
|
+
child.unref();
|
|
170
|
+
const warningText = preflight.warnings.length > 0
|
|
171
|
+
? `\nWarnings:\n${preflight.warnings.map(w => ` - ${w}`).join('\n')}`
|
|
172
|
+
: '';
|
|
173
|
+
return {
|
|
174
|
+
success: true,
|
|
175
|
+
project,
|
|
176
|
+
message: `Playwright UI mode launched for project "${project}". The browser window should open shortly.${warningText}`,
|
|
177
|
+
pid: child.pid,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
182
|
+
return {
|
|
183
|
+
success: false,
|
|
184
|
+
project,
|
|
185
|
+
message: `Failed to launch Playwright UI: ${message}`,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Run E2E tests headlessly and return results.
|
|
191
|
+
*/
|
|
192
|
+
function runTests(args) {
|
|
193
|
+
// Pre-flight validation
|
|
194
|
+
const preflight = validatePrerequisites();
|
|
195
|
+
if (!preflight.ok) {
|
|
196
|
+
const projectLabel = args.project || 'default (vendor-owner + cross-persona)';
|
|
197
|
+
return {
|
|
198
|
+
success: false,
|
|
199
|
+
project: projectLabel,
|
|
200
|
+
passed: 0,
|
|
201
|
+
failed: 0,
|
|
202
|
+
skipped: 0,
|
|
203
|
+
duration: '0s',
|
|
204
|
+
output: `Environment validation failed:\n${preflight.errors.map(e => ` - ${e}`).join('\n')}`,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
// Clean up zombie dev servers from previous runs
|
|
208
|
+
cleanupDevServerPort();
|
|
209
|
+
cleanupDevServerPort(3001);
|
|
210
|
+
const cmdArgs = ['playwright', 'test'];
|
|
211
|
+
if (args.project) {
|
|
212
|
+
cmdArgs.push('--project', args.project);
|
|
213
|
+
}
|
|
214
|
+
if (args.grep) {
|
|
215
|
+
cmdArgs.push('--grep', args.grep);
|
|
216
|
+
}
|
|
217
|
+
if (args.retries !== undefined) {
|
|
218
|
+
cmdArgs.push('--retries', String(args.retries));
|
|
219
|
+
}
|
|
220
|
+
if (args.workers !== undefined) {
|
|
221
|
+
cmdArgs.push('--workers', String(args.workers));
|
|
222
|
+
}
|
|
223
|
+
// Use list reporter for parseable output
|
|
224
|
+
cmdArgs.push('--reporter', 'list');
|
|
225
|
+
const projectLabel = args.project || 'default (vendor-owner + cross-persona)';
|
|
226
|
+
try {
|
|
227
|
+
const output = execFileSync('npx', cmdArgs, {
|
|
228
|
+
cwd: PROJECT_DIR,
|
|
229
|
+
timeout: RUN_TIMEOUT,
|
|
230
|
+
encoding: 'utf8',
|
|
231
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
232
|
+
});
|
|
233
|
+
const { passed, failed, skipped, duration } = parseTestOutput(output);
|
|
234
|
+
// Guard: zero tests executed is not a success
|
|
235
|
+
if (passed === 0 && failed === 0 && skipped === 0) {
|
|
236
|
+
return {
|
|
237
|
+
success: false,
|
|
238
|
+
project: projectLabel,
|
|
239
|
+
passed: 0,
|
|
240
|
+
failed: 0,
|
|
241
|
+
skipped: 0,
|
|
242
|
+
duration,
|
|
243
|
+
output: truncateOutput(output),
|
|
244
|
+
error: 'No tests were executed. Check project filter, test file paths, or Playwright configuration.',
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
success: failed === 0,
|
|
249
|
+
project: projectLabel,
|
|
250
|
+
passed,
|
|
251
|
+
failed,
|
|
252
|
+
skipped,
|
|
253
|
+
duration,
|
|
254
|
+
output: truncateOutput(output),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
// execSync throws on non-zero exit code (test failures)
|
|
259
|
+
const execErr = err;
|
|
260
|
+
const output = (execErr.stdout || '') + (execErr.stderr || '');
|
|
261
|
+
const { passed, failed, skipped, duration } = parseTestOutput(output);
|
|
262
|
+
return {
|
|
263
|
+
success: false,
|
|
264
|
+
project: projectLabel,
|
|
265
|
+
passed,
|
|
266
|
+
failed: failed || 1, // At least 1 failure if we're in the catch
|
|
267
|
+
skipped,
|
|
268
|
+
duration,
|
|
269
|
+
output: truncateOutput(output),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Seed the E2E test database.
|
|
275
|
+
*/
|
|
276
|
+
function seedData() {
|
|
277
|
+
// Pre-flight validation
|
|
278
|
+
const preflight = validatePrerequisites();
|
|
279
|
+
if (!preflight.ok) {
|
|
280
|
+
return {
|
|
281
|
+
success: false,
|
|
282
|
+
message: `Environment validation failed:\n${preflight.errors.map(e => ` - ${e}`).join('\n')}`,
|
|
283
|
+
output: '',
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
// Clean up zombie dev servers from previous runs
|
|
287
|
+
cleanupDevServerPort();
|
|
288
|
+
cleanupDevServerPort(3001);
|
|
289
|
+
try {
|
|
290
|
+
const output = execFileSync('npx', ['playwright', 'test', '--project=seed'], {
|
|
291
|
+
cwd: PROJECT_DIR,
|
|
292
|
+
timeout: RUN_TIMEOUT,
|
|
293
|
+
encoding: 'utf8',
|
|
294
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
295
|
+
});
|
|
296
|
+
return {
|
|
297
|
+
success: true,
|
|
298
|
+
message: 'Test database seeded successfully.',
|
|
299
|
+
output: truncateOutput(output),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
const execErr = err;
|
|
304
|
+
const output = (execErr.stdout || '') + (execErr.stderr || '');
|
|
305
|
+
return {
|
|
306
|
+
success: false,
|
|
307
|
+
message: `Seeding failed: ${execErr.message || 'Unknown error'}`,
|
|
308
|
+
output: truncateOutput(output),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Clean up E2E test data.
|
|
314
|
+
*/
|
|
315
|
+
function cleanupData() {
|
|
316
|
+
// Pre-flight validation
|
|
317
|
+
const preflight = validatePrerequisites();
|
|
318
|
+
if (!preflight.ok) {
|
|
319
|
+
return {
|
|
320
|
+
success: false,
|
|
321
|
+
message: `Environment validation failed:\n${preflight.errors.map(e => ` - ${e}`).join('\n')}`,
|
|
322
|
+
output: '',
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
// Clean up zombie dev servers from previous runs
|
|
326
|
+
cleanupDevServerPort();
|
|
327
|
+
cleanupDevServerPort(3001);
|
|
328
|
+
try {
|
|
329
|
+
const output = execFileSync('npx', ['playwright', 'test', '--project=seed'], {
|
|
330
|
+
cwd: PROJECT_DIR,
|
|
331
|
+
timeout: RUN_TIMEOUT,
|
|
332
|
+
encoding: 'utf8',
|
|
333
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
334
|
+
env: { ...process.env, E2E_CLEANUP: 'true' },
|
|
335
|
+
});
|
|
336
|
+
return {
|
|
337
|
+
success: true,
|
|
338
|
+
message: 'Test data cleaned up successfully.',
|
|
339
|
+
output: truncateOutput(output),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
catch (err) {
|
|
343
|
+
const execErr = err;
|
|
344
|
+
const output = (execErr.stdout || '') + (execErr.stderr || '');
|
|
345
|
+
return {
|
|
346
|
+
success: false,
|
|
347
|
+
message: `Cleanup failed: ${execErr.message || 'Unknown error'}`,
|
|
348
|
+
output: truncateOutput(output),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Get the last Playwright test report.
|
|
354
|
+
*/
|
|
355
|
+
function getReport(args) {
|
|
356
|
+
const reportIndex = path.join(REPORT_DIR, 'index.html');
|
|
357
|
+
const exists = fs.existsSync(reportIndex);
|
|
358
|
+
if (!exists) {
|
|
359
|
+
return {
|
|
360
|
+
success: true,
|
|
361
|
+
reportPath: REPORT_DIR,
|
|
362
|
+
exists: false,
|
|
363
|
+
message: 'No test report found. Run tests first with run_tests.',
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
const stats = fs.statSync(reportIndex);
|
|
367
|
+
const lastModified = stats.mtime.toISOString();
|
|
368
|
+
let browserOpened = false;
|
|
369
|
+
let browserError = null;
|
|
370
|
+
if (args.open_browser) {
|
|
371
|
+
try {
|
|
372
|
+
spawn('npx', ['playwright', 'show-report'], {
|
|
373
|
+
detached: true,
|
|
374
|
+
stdio: 'ignore',
|
|
375
|
+
cwd: PROJECT_DIR,
|
|
376
|
+
}).unref();
|
|
377
|
+
browserOpened = true;
|
|
378
|
+
}
|
|
379
|
+
catch (err) {
|
|
380
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
381
|
+
browserError = `Failed to open report in browser: ${errMsg}`;
|
|
382
|
+
process.stderr.write(`[playwright] ${browserError}\n`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const statusMsg = args.open_browser
|
|
386
|
+
? (browserOpened
|
|
387
|
+
? `Report opened in browser. Last generated: ${lastModified}`
|
|
388
|
+
: `Report exists but browser failed to open: ${browserError}. Last generated: ${lastModified}`)
|
|
389
|
+
: `Report available at ${REPORT_DIR}. Last generated: ${lastModified}. Use open_browser: true to view.`;
|
|
390
|
+
return {
|
|
391
|
+
success: true,
|
|
392
|
+
reportPath: REPORT_DIR,
|
|
393
|
+
exists: true,
|
|
394
|
+
lastModified,
|
|
395
|
+
message: statusMsg,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Check E2E test coverage by persona and page.
|
|
400
|
+
* Reads the filesystem to determine which projects have tests.
|
|
401
|
+
*/
|
|
402
|
+
function getCoverageStatus() {
|
|
403
|
+
const personas = [];
|
|
404
|
+
let totalTests = 0;
|
|
405
|
+
let activeProjects = 0;
|
|
406
|
+
let deferredProjects = 0;
|
|
407
|
+
// Active test directories
|
|
408
|
+
const activeDirs = {
|
|
409
|
+
'vendor-owner': 'e2e/vendor',
|
|
410
|
+
'vendor-admin': 'e2e/vendor-roles',
|
|
411
|
+
'vendor-dev': 'e2e/vendor-roles',
|
|
412
|
+
'vendor-viewer': 'e2e/vendor-roles',
|
|
413
|
+
'cross-persona': 'e2e/cross-persona',
|
|
414
|
+
'auth-flows': 'e2e/auth',
|
|
415
|
+
'manual': 'e2e/manual',
|
|
416
|
+
'extension': 'e2e/extension',
|
|
417
|
+
'extension-manual': 'e2e/extension/manual',
|
|
418
|
+
'demo': 'e2e/demo',
|
|
419
|
+
};
|
|
420
|
+
for (const [project, testDir] of Object.entries(activeDirs)) {
|
|
421
|
+
const fullDir = path.join(PROJECT_DIR, testDir);
|
|
422
|
+
const testCount = countTestFiles(fullDir, project);
|
|
423
|
+
const entry = {
|
|
424
|
+
project,
|
|
425
|
+
persona: PERSONA_MAP[project] || project,
|
|
426
|
+
testDir,
|
|
427
|
+
testCount,
|
|
428
|
+
status: testCount > 0 ? 'active' : 'no-tests',
|
|
429
|
+
};
|
|
430
|
+
if (testCount > 0) {
|
|
431
|
+
activeProjects++;
|
|
432
|
+
totalTests += testCount;
|
|
433
|
+
}
|
|
434
|
+
personas.push(entry);
|
|
435
|
+
}
|
|
436
|
+
// Deferred test directories
|
|
437
|
+
const deferredDirs = {
|
|
438
|
+
'operator': 'e2e/_deferred/operator',
|
|
439
|
+
};
|
|
440
|
+
for (const [name, testDir] of Object.entries(deferredDirs)) {
|
|
441
|
+
const fullDir = path.join(PROJECT_DIR, testDir);
|
|
442
|
+
const testCount = countTestFiles(fullDir);
|
|
443
|
+
const personaLabel = 'Platform Operator';
|
|
444
|
+
personas.push({
|
|
445
|
+
project: `deferred-${name}`,
|
|
446
|
+
persona: personaLabel,
|
|
447
|
+
testDir,
|
|
448
|
+
testCount,
|
|
449
|
+
status: 'deferred',
|
|
450
|
+
});
|
|
451
|
+
if (testCount > 0) {
|
|
452
|
+
deferredProjects++;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return {
|
|
456
|
+
personas,
|
|
457
|
+
totalTests,
|
|
458
|
+
activeProjects,
|
|
459
|
+
deferredProjects,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
// ============================================================================
|
|
463
|
+
// Helpers
|
|
464
|
+
// ============================================================================
|
|
465
|
+
/**
|
|
466
|
+
* Count test files in a directory.
|
|
467
|
+
* Matches *.spec.ts for automated tests, *.manual.ts for manual scaffolds.
|
|
468
|
+
*/
|
|
469
|
+
function countTestFiles(dir, projectFilter) {
|
|
470
|
+
if (!fs.existsSync(dir))
|
|
471
|
+
return 0;
|
|
472
|
+
const files = fs.readdirSync(dir, { recursive: true });
|
|
473
|
+
return files.filter(f => {
|
|
474
|
+
const filename = String(f);
|
|
475
|
+
const isSpec = filename.endsWith('.spec.ts');
|
|
476
|
+
const isManual = filename.endsWith('.manual.ts');
|
|
477
|
+
if (!isSpec && !isManual)
|
|
478
|
+
return false;
|
|
479
|
+
// Exclude manual/ subdirectory for the extension project (counted separately as extension-manual)
|
|
480
|
+
if (projectFilter === 'extension' && filename.includes('manual/'))
|
|
481
|
+
return false;
|
|
482
|
+
// For role-specific projects, filter by matching spec file
|
|
483
|
+
if (projectFilter === 'vendor-admin')
|
|
484
|
+
return filename.includes('admin');
|
|
485
|
+
if (projectFilter === 'vendor-dev')
|
|
486
|
+
return filename.includes('developer');
|
|
487
|
+
if (projectFilter === 'vendor-viewer')
|
|
488
|
+
return filename.includes('viewer');
|
|
489
|
+
return true;
|
|
490
|
+
}).length;
|
|
491
|
+
}
|
|
492
|
+
// parseTestOutput and truncateOutput imported from ./helpers.js
|
|
493
|
+
// ============================================================================
|
|
494
|
+
// Preflight Check
|
|
495
|
+
// ============================================================================
|
|
496
|
+
/**
|
|
497
|
+
* Run a single preflight check and return a structured entry.
|
|
498
|
+
*/
|
|
499
|
+
function runCheck(name, fn) {
|
|
500
|
+
const start = Date.now();
|
|
501
|
+
try {
|
|
502
|
+
const result = fn();
|
|
503
|
+
return { name, ...result, duration_ms: Date.now() - start };
|
|
504
|
+
}
|
|
505
|
+
catch (err) {
|
|
506
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
507
|
+
return { name, status: 'fail', message: `Unexpected error: ${message}`, duration_ms: Date.now() - start };
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Comprehensive pre-flight validation before launching Playwright.
|
|
512
|
+
* Checks config, deps, browsers, test files, credentials, compilation, and dev server.
|
|
513
|
+
*/
|
|
514
|
+
async function preflightCheck(args) {
|
|
515
|
+
const startTime = Date.now();
|
|
516
|
+
const checks = [];
|
|
517
|
+
const failures = [];
|
|
518
|
+
const warnings = [];
|
|
519
|
+
const recoverySteps = [];
|
|
520
|
+
// 1. Config exists
|
|
521
|
+
checks.push(runCheck('config_exists', () => {
|
|
522
|
+
const tsConfig = path.join(PROJECT_DIR, 'playwright.config.ts');
|
|
523
|
+
const jsConfig = path.join(PROJECT_DIR, 'playwright.config.js');
|
|
524
|
+
if (fs.existsSync(tsConfig)) {
|
|
525
|
+
return { status: 'pass', message: `Found playwright.config.ts` };
|
|
526
|
+
}
|
|
527
|
+
if (fs.existsSync(jsConfig)) {
|
|
528
|
+
return { status: 'pass', message: `Found playwright.config.js` };
|
|
529
|
+
}
|
|
530
|
+
return { status: 'fail', message: 'No playwright.config.ts or playwright.config.js found' };
|
|
531
|
+
}));
|
|
532
|
+
// 2. Dependencies installed
|
|
533
|
+
checks.push(runCheck('dependencies_installed', () => {
|
|
534
|
+
const pwTestDir = path.join(PROJECT_DIR, 'node_modules', '@playwright', 'test');
|
|
535
|
+
if (fs.existsSync(pwTestDir)) {
|
|
536
|
+
return { status: 'pass', message: '@playwright/test is installed' };
|
|
537
|
+
}
|
|
538
|
+
return { status: 'fail', message: '@playwright/test not found in node_modules' };
|
|
539
|
+
}));
|
|
540
|
+
// 3. Browsers installed
|
|
541
|
+
checks.push(runCheck('browsers_installed', () => {
|
|
542
|
+
const homedir = os.homedir();
|
|
543
|
+
const cacheLocations = [
|
|
544
|
+
path.join(homedir, 'Library', 'Caches', 'ms-playwright'),
|
|
545
|
+
path.join(homedir, '.cache', 'ms-playwright'),
|
|
546
|
+
];
|
|
547
|
+
for (const cacheDir of cacheLocations) {
|
|
548
|
+
if (!fs.existsSync(cacheDir))
|
|
549
|
+
continue;
|
|
550
|
+
try {
|
|
551
|
+
const entries = fs.readdirSync(cacheDir);
|
|
552
|
+
const chromium = entries.find(e => e.startsWith('chromium-'));
|
|
553
|
+
if (chromium) {
|
|
554
|
+
return { status: 'pass', message: `Chromium found in ${cacheDir}` };
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return { status: 'fail', message: 'No Chromium browser found in Playwright cache' };
|
|
562
|
+
}));
|
|
563
|
+
// 4. Test files exist (when project specified)
|
|
564
|
+
if (args.project) {
|
|
565
|
+
checks.push(runCheck('test_files_exist', () => {
|
|
566
|
+
const activeDirs = {
|
|
567
|
+
'vendor-owner': 'e2e/vendor',
|
|
568
|
+
'vendor-admin': 'e2e/vendor-roles',
|
|
569
|
+
'vendor-dev': 'e2e/vendor-roles',
|
|
570
|
+
'vendor-viewer': 'e2e/vendor-roles',
|
|
571
|
+
'cross-persona': 'e2e/cross-persona',
|
|
572
|
+
'auth-flows': 'e2e/auth',
|
|
573
|
+
'manual': 'e2e/manual',
|
|
574
|
+
'extension': 'e2e/extension',
|
|
575
|
+
'extension-manual': 'e2e/extension/manual',
|
|
576
|
+
'demo': 'e2e/demo',
|
|
577
|
+
};
|
|
578
|
+
const testDir = activeDirs[args.project];
|
|
579
|
+
if (!testDir) {
|
|
580
|
+
return { status: 'warn', message: `No known test directory for project "${args.project}"` };
|
|
581
|
+
}
|
|
582
|
+
const fullDir = path.join(PROJECT_DIR, testDir);
|
|
583
|
+
const count = countTestFiles(fullDir, args.project);
|
|
584
|
+
if (count > 0) {
|
|
585
|
+
return { status: 'pass', message: `${count} test file(s) found in ${testDir}` };
|
|
586
|
+
}
|
|
587
|
+
return { status: 'fail', message: `No test files found in ${testDir} for project "${args.project}"` };
|
|
588
|
+
}));
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
checks.push({ name: 'test_files_exist', status: 'skip', message: 'No project specified — skipping test file check', duration_ms: 0 });
|
|
592
|
+
}
|
|
593
|
+
// 5. Credentials valid
|
|
594
|
+
checks.push(runCheck('credentials_valid', () => {
|
|
595
|
+
const preflight = validatePrerequisites();
|
|
596
|
+
if (preflight.ok) {
|
|
597
|
+
const warnMsg = preflight.warnings.length > 0
|
|
598
|
+
? ` (${preflight.warnings.length} warning(s))`
|
|
599
|
+
: '';
|
|
600
|
+
return { status: 'pass', message: `All required credentials are set${warnMsg}` };
|
|
601
|
+
}
|
|
602
|
+
return { status: 'fail', message: preflight.errors.join('; ') };
|
|
603
|
+
}));
|
|
604
|
+
// 6. Compilation succeeds (unless skipped)
|
|
605
|
+
if (!args.skip_compilation && args.project) {
|
|
606
|
+
checks.push(runCheck('compilation', () => {
|
|
607
|
+
try {
|
|
608
|
+
const listArgs = ['playwright', 'test', '--list', '--project', args.project];
|
|
609
|
+
const output = execFileSync('npx', listArgs, {
|
|
610
|
+
cwd: PROJECT_DIR,
|
|
611
|
+
timeout: 30_000,
|
|
612
|
+
encoding: 'utf8',
|
|
613
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
614
|
+
env: process.env,
|
|
615
|
+
});
|
|
616
|
+
// Check if any tests were listed
|
|
617
|
+
const lines = output.trim().split('\n').filter(l => l.trim().length > 0);
|
|
618
|
+
if (lines.length === 0) {
|
|
619
|
+
return { status: 'fail', message: 'Compilation succeeded but no tests were listed' };
|
|
620
|
+
}
|
|
621
|
+
return { status: 'pass', message: `${lines.length} test(s) listed successfully` };
|
|
622
|
+
}
|
|
623
|
+
catch (err) {
|
|
624
|
+
const execErr = err;
|
|
625
|
+
const stderr = (execErr.stderr || '').trim();
|
|
626
|
+
const snippet = stderr.length > 300 ? stderr.slice(0, 300) + '...' : stderr;
|
|
627
|
+
return { status: 'fail', message: `Compilation/list failed: ${snippet || execErr.message || 'unknown error'}` };
|
|
628
|
+
}
|
|
629
|
+
}));
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
const reason = args.skip_compilation ? 'skip_compilation=true' : 'no project specified';
|
|
633
|
+
checks.push({ name: 'compilation', status: 'skip', message: `Skipped (${reason})`, duration_ms: 0 });
|
|
634
|
+
}
|
|
635
|
+
// 7. Dev server reachable (warning only)
|
|
636
|
+
const baseUrl = args.base_url || 'http://localhost:3000';
|
|
637
|
+
const devServerCheck = await new Promise((resolve) => {
|
|
638
|
+
const start = Date.now();
|
|
639
|
+
const url = new URL(baseUrl);
|
|
640
|
+
const req = http.request({
|
|
641
|
+
hostname: url.hostname,
|
|
642
|
+
port: url.port || 80,
|
|
643
|
+
path: url.pathname,
|
|
644
|
+
method: 'HEAD',
|
|
645
|
+
timeout: 5000,
|
|
646
|
+
}, (res) => {
|
|
647
|
+
resolve({
|
|
648
|
+
name: 'dev_server',
|
|
649
|
+
status: 'pass',
|
|
650
|
+
message: `Dev server at ${baseUrl} responded with ${res.statusCode}`,
|
|
651
|
+
duration_ms: Date.now() - start,
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
req.on('timeout', () => {
|
|
655
|
+
req.destroy();
|
|
656
|
+
resolve({
|
|
657
|
+
name: 'dev_server',
|
|
658
|
+
status: 'warn',
|
|
659
|
+
message: `Dev server at ${baseUrl} did not respond within 5s (may auto-start via playwright.config.ts webServer)`,
|
|
660
|
+
duration_ms: Date.now() - start,
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
req.on('error', () => {
|
|
664
|
+
resolve({
|
|
665
|
+
name: 'dev_server',
|
|
666
|
+
status: 'warn',
|
|
667
|
+
message: `Dev server at ${baseUrl} is not reachable (may auto-start via playwright.config.ts webServer)`,
|
|
668
|
+
duration_ms: Date.now() - start,
|
|
669
|
+
});
|
|
670
|
+
});
|
|
671
|
+
req.end();
|
|
672
|
+
});
|
|
673
|
+
checks.push(devServerCheck);
|
|
674
|
+
// 8. Auth state freshness (only when a project is specified)
|
|
675
|
+
if (args.project) {
|
|
676
|
+
checks.push(runCheck('auth_state', () => {
|
|
677
|
+
const authDir = path.join(PROJECT_DIR, '.auth');
|
|
678
|
+
const primaryFile = path.join(authDir, 'vendor-owner.json');
|
|
679
|
+
if (!fs.existsSync(primaryFile)) {
|
|
680
|
+
return { status: 'fail', message: '.auth/vendor-owner.json missing — run mcp__playwright__run_auth_setup to generate it' };
|
|
681
|
+
}
|
|
682
|
+
const stat = fs.statSync(primaryFile);
|
|
683
|
+
const ageMs = Date.now() - stat.mtimeMs;
|
|
684
|
+
const ageHours = ageMs / (1000 * 60 * 60);
|
|
685
|
+
// Check cookie expiry from inside the JSON
|
|
686
|
+
try {
|
|
687
|
+
const state = JSON.parse(fs.readFileSync(primaryFile, 'utf-8'));
|
|
688
|
+
const cookies = state.cookies || [];
|
|
689
|
+
const now = Date.now() / 1000;
|
|
690
|
+
const expired = cookies.filter(c => c.expires && c.expires > 0 && c.expires < now);
|
|
691
|
+
if (expired.length > 0) {
|
|
692
|
+
return { status: 'fail', message: `Auth cookies are expired (${expired.length} expired) — run mcp__playwright__run_auth_setup` };
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
catch {
|
|
696
|
+
return { status: 'warn', message: `Could not parse .auth/vendor-owner.json (${ageHours.toFixed(1)}h old)` };
|
|
697
|
+
}
|
|
698
|
+
if (ageHours > 24) {
|
|
699
|
+
return { status: 'fail', message: `.auth files are ${ageHours.toFixed(1)}h old — run mcp__playwright__run_auth_setup` };
|
|
700
|
+
}
|
|
701
|
+
if (ageHours > 4) {
|
|
702
|
+
return { status: 'warn', message: `.auth files are ${ageHours.toFixed(1)}h old — may expire soon` };
|
|
703
|
+
}
|
|
704
|
+
return { status: 'pass', message: `.auth files are ${ageHours.toFixed(1)}h old — fresh` };
|
|
705
|
+
}));
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
checks.push({ name: 'auth_state', status: 'skip', message: 'No project specified — skipping auth state check', duration_ms: 0 });
|
|
709
|
+
}
|
|
710
|
+
// Aggregate results
|
|
711
|
+
for (const check of checks) {
|
|
712
|
+
if (check.status === 'fail') {
|
|
713
|
+
failures.push(`${check.name}: ${check.message}`);
|
|
714
|
+
}
|
|
715
|
+
else if (check.status === 'warn') {
|
|
716
|
+
warnings.push(`${check.name}: ${check.message}`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
// Generate recovery steps for failures
|
|
720
|
+
for (const check of checks) {
|
|
721
|
+
if (check.status !== 'fail')
|
|
722
|
+
continue;
|
|
723
|
+
switch (check.name) {
|
|
724
|
+
case 'config_exists':
|
|
725
|
+
recoverySteps.push('Create playwright.config.ts in the project root (see Playwright docs)');
|
|
726
|
+
break;
|
|
727
|
+
case 'dependencies_installed':
|
|
728
|
+
recoverySteps.push('Run: pnpm add -D @playwright/test');
|
|
729
|
+
break;
|
|
730
|
+
case 'browsers_installed':
|
|
731
|
+
recoverySteps.push('Run: npx playwright install chromium');
|
|
732
|
+
break;
|
|
733
|
+
case 'test_files_exist':
|
|
734
|
+
recoverySteps.push(`Create test files in the expected directory for project "${args.project}"`);
|
|
735
|
+
break;
|
|
736
|
+
case 'credentials_valid':
|
|
737
|
+
recoverySteps.push('Check 1Password credential injection — ensure MCP server has SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY');
|
|
738
|
+
break;
|
|
739
|
+
case 'compilation':
|
|
740
|
+
recoverySteps.push('Fix TypeScript compilation errors — run: npx playwright test --list to see details');
|
|
741
|
+
break;
|
|
742
|
+
case 'auth_state':
|
|
743
|
+
recoverySteps.push('Run: mcp__playwright__run_auth_setup() to refresh auth cookies');
|
|
744
|
+
break;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return {
|
|
748
|
+
ready: failures.length === 0,
|
|
749
|
+
project: args.project || null,
|
|
750
|
+
checks,
|
|
751
|
+
failures,
|
|
752
|
+
warnings,
|
|
753
|
+
recovery_steps: recoverySteps,
|
|
754
|
+
total_duration_ms: Date.now() - startTime,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
// ============================================================================
|
|
758
|
+
// Auth Setup
|
|
759
|
+
// ============================================================================
|
|
760
|
+
/**
|
|
761
|
+
* Refresh Playwright auth state by running seed + auth-setup projects.
|
|
762
|
+
* Seeds test data first, then runs the auth-setup project to generate .auth/*.json files.
|
|
763
|
+
*/
|
|
764
|
+
async function runAuthSetup(args) {
|
|
765
|
+
const startTime = Date.now();
|
|
766
|
+
const prereq = validatePrerequisites();
|
|
767
|
+
if (!prereq.ok) {
|
|
768
|
+
return {
|
|
769
|
+
success: false,
|
|
770
|
+
phases: [],
|
|
771
|
+
auth_files_refreshed: [],
|
|
772
|
+
total_duration_ms: 0,
|
|
773
|
+
error: `Credential check failed: ${prereq.errors.join('; ')}`,
|
|
774
|
+
output_summary: '',
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
const phases = [];
|
|
778
|
+
// Phase 1: Seed
|
|
779
|
+
const seedStart = Date.now();
|
|
780
|
+
try {
|
|
781
|
+
execFileSync('npx', ['playwright', 'test', '--project=seed'], {
|
|
782
|
+
cwd: PROJECT_DIR,
|
|
783
|
+
timeout: 60_000,
|
|
784
|
+
encoding: 'utf8',
|
|
785
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
786
|
+
env: process.env,
|
|
787
|
+
});
|
|
788
|
+
phases.push({ name: 'seed', success: true, message: 'Seed completed', duration_ms: Date.now() - seedStart });
|
|
789
|
+
}
|
|
790
|
+
catch (err) {
|
|
791
|
+
const execErr = err;
|
|
792
|
+
const msg = (execErr.stderr || execErr.message || 'unknown').slice(0, 300);
|
|
793
|
+
phases.push({ name: 'seed', success: false, message: `Seed failed: ${msg}`, duration_ms: Date.now() - seedStart });
|
|
794
|
+
return {
|
|
795
|
+
success: false,
|
|
796
|
+
phases,
|
|
797
|
+
auth_files_refreshed: [],
|
|
798
|
+
total_duration_ms: Date.now() - startTime,
|
|
799
|
+
error: 'Seed phase failed — auth-setup aborted',
|
|
800
|
+
output_summary: msg.slice(0, 500),
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
if (args.seed_only) {
|
|
804
|
+
return {
|
|
805
|
+
success: true,
|
|
806
|
+
phases,
|
|
807
|
+
auth_files_refreshed: [],
|
|
808
|
+
total_duration_ms: Date.now() - startTime,
|
|
809
|
+
output_summary: 'Seed only',
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
// Phase 2: Auth setup
|
|
813
|
+
const authStart = Date.now();
|
|
814
|
+
try {
|
|
815
|
+
const output = execFileSync('npx', ['playwright', 'test', '--project=auth-setup'], {
|
|
816
|
+
cwd: PROJECT_DIR,
|
|
817
|
+
timeout: 240_000, // 4 min: web server startup + 4 persona sign-ins
|
|
818
|
+
encoding: 'utf8',
|
|
819
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
820
|
+
env: process.env,
|
|
821
|
+
});
|
|
822
|
+
phases.push({ name: 'auth-setup', success: true, message: 'Auth setup completed', duration_ms: Date.now() - authStart });
|
|
823
|
+
const authDir = path.join(PROJECT_DIR, '.auth');
|
|
824
|
+
const expected = ['vendor-owner.json', 'vendor-admin.json', 'vendor-dev.json', 'vendor-viewer.json'];
|
|
825
|
+
const refreshed = expected.filter(f => fs.existsSync(path.join(authDir, f)));
|
|
826
|
+
return {
|
|
827
|
+
success: true,
|
|
828
|
+
phases,
|
|
829
|
+
auth_files_refreshed: refreshed,
|
|
830
|
+
total_duration_ms: Date.now() - startTime,
|
|
831
|
+
output_summary: truncateOutput(output, 1000),
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
catch (err) {
|
|
835
|
+
const execErr = err;
|
|
836
|
+
const msg = ((execErr.stderr || '') + (execErr.stdout || '') || execErr.message || 'unknown').slice(0, 500);
|
|
837
|
+
phases.push({ name: 'auth-setup', success: false, message: `Auth setup failed: ${msg.slice(0, 300)}`, duration_ms: Date.now() - authStart });
|
|
838
|
+
return {
|
|
839
|
+
success: false,
|
|
840
|
+
phases,
|
|
841
|
+
auth_files_refreshed: [],
|
|
842
|
+
total_duration_ms: Date.now() - startTime,
|
|
843
|
+
error: 'Auth setup phase failed',
|
|
844
|
+
output_summary: msg,
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* List all open tabs in the CDP-connected extension test browser.
|
|
850
|
+
* Requires --remote-debugging-port to be exposed in the fixture.
|
|
851
|
+
*/
|
|
852
|
+
async function listExtensionTabs(args) {
|
|
853
|
+
const { port } = args;
|
|
854
|
+
return new Promise((resolve) => {
|
|
855
|
+
const req = http.request({
|
|
856
|
+
hostname: 'localhost',
|
|
857
|
+
port,
|
|
858
|
+
path: '/json',
|
|
859
|
+
method: 'GET',
|
|
860
|
+
timeout: 5000,
|
|
861
|
+
}, (res) => {
|
|
862
|
+
let data = '';
|
|
863
|
+
res.on('data', (chunk) => { data += chunk.toString(); });
|
|
864
|
+
res.on('end', () => {
|
|
865
|
+
try {
|
|
866
|
+
const raw = JSON.parse(data);
|
|
867
|
+
const tabs = raw
|
|
868
|
+
.filter(t => t.type === 'page' || t.type === 'background_page' || t.url.startsWith('chrome-extension://'))
|
|
869
|
+
.map(t => ({ id: t.id, title: t.title, url: t.url, type: t.type }));
|
|
870
|
+
resolve({
|
|
871
|
+
success: true,
|
|
872
|
+
tabs,
|
|
873
|
+
message: `Found ${tabs.length} tab(s) in extension browser (port ${port})`,
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
catch (err) {
|
|
877
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
878
|
+
resolve({ success: false, tabs: [], message: `Failed to parse CDP response: ${msg}` });
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
});
|
|
882
|
+
req.on('timeout', () => {
|
|
883
|
+
req.destroy();
|
|
884
|
+
resolve({
|
|
885
|
+
success: false,
|
|
886
|
+
tabs: [],
|
|
887
|
+
message: `CDP endpoint at port ${port} did not respond within 5s. Is the extension browser running with --remote-debugging-port=${port}?`,
|
|
888
|
+
});
|
|
889
|
+
});
|
|
890
|
+
req.on('error', () => {
|
|
891
|
+
resolve({
|
|
892
|
+
success: false,
|
|
893
|
+
tabs: [],
|
|
894
|
+
message: `Cannot connect to CDP endpoint at port ${port}. Launch extension tests first (mcp__playwright__launch_ui_mode with project "demo" or "extension-manual").`,
|
|
895
|
+
});
|
|
896
|
+
});
|
|
897
|
+
req.end();
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Take a screenshot of a specific tab in the CDP-connected extension test browser.
|
|
902
|
+
* Returns a base64 PNG image for inline rendering in Claude Code.
|
|
903
|
+
*/
|
|
904
|
+
async function screenshotExtensionTab(args) {
|
|
905
|
+
const { port, url_pattern, tab_id } = args;
|
|
906
|
+
// Step 1: List tabs
|
|
907
|
+
const listResult = await listExtensionTabs({ port });
|
|
908
|
+
if (!listResult.success || listResult.tabs.length === 0) {
|
|
909
|
+
return {
|
|
910
|
+
success: false,
|
|
911
|
+
message: listResult.success
|
|
912
|
+
? `No tabs found in extension browser at port ${port}`
|
|
913
|
+
: listResult.message,
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
// Step 2: Find the target tab
|
|
917
|
+
let targetTab;
|
|
918
|
+
if (tab_id) {
|
|
919
|
+
targetTab = listResult.tabs.find(t => t.id === tab_id);
|
|
920
|
+
if (!targetTab) {
|
|
921
|
+
return {
|
|
922
|
+
success: false,
|
|
923
|
+
message: `Tab with id "${tab_id}" not found. Available tabs: ${listResult.tabs.map(t => t.id).join(', ')}`,
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
else if (url_pattern) {
|
|
928
|
+
targetTab = listResult.tabs.find(t => t.url.includes(url_pattern));
|
|
929
|
+
if (!targetTab) {
|
|
930
|
+
// Fall back to first tab
|
|
931
|
+
targetTab = listResult.tabs[0];
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
else {
|
|
935
|
+
targetTab = listResult.tabs[0];
|
|
936
|
+
}
|
|
937
|
+
if (!targetTab) {
|
|
938
|
+
return { success: false, message: 'No tab selected' };
|
|
939
|
+
}
|
|
940
|
+
const selectedTab = targetTab;
|
|
941
|
+
// Step 3: Activate the tab via CDP HTTP endpoint
|
|
942
|
+
await new Promise((resolve) => {
|
|
943
|
+
const req = http.request({ hostname: 'localhost', port, path: `/json/activate/${selectedTab.id}`, method: 'GET', timeout: 3000 }, () => resolve());
|
|
944
|
+
req.on('error', () => resolve());
|
|
945
|
+
req.on('timeout', () => { req.destroy(); resolve(); });
|
|
946
|
+
req.end();
|
|
947
|
+
});
|
|
948
|
+
// Step 4: Get WebSocket debugger URL for this tab
|
|
949
|
+
const wsDebuggerUrl = await new Promise((resolve) => {
|
|
950
|
+
const req = http.request({ hostname: 'localhost', port, path: `/json`, method: 'GET', timeout: 5000 }, (res) => {
|
|
951
|
+
let data = '';
|
|
952
|
+
res.on('data', (chunk) => { data += chunk.toString(); });
|
|
953
|
+
res.on('end', () => {
|
|
954
|
+
try {
|
|
955
|
+
const tabs = JSON.parse(data);
|
|
956
|
+
const tab = tabs.find(t => t.id === selectedTab.id);
|
|
957
|
+
resolve(tab?.webSocketDebuggerUrl ?? null);
|
|
958
|
+
}
|
|
959
|
+
catch {
|
|
960
|
+
resolve(null);
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
});
|
|
964
|
+
req.on('error', () => resolve(null));
|
|
965
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
966
|
+
req.end();
|
|
967
|
+
});
|
|
968
|
+
if (!wsDebuggerUrl) {
|
|
969
|
+
return {
|
|
970
|
+
success: false,
|
|
971
|
+
tab: selectedTab,
|
|
972
|
+
message: `Failed to get WebSocket debugger URL for tab "${selectedTab.title}" (${selectedTab.url})`,
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
// Step 5: Connect via raw WebSocket and capture screenshot via CDP Page.captureScreenshot
|
|
976
|
+
const base64png = await new Promise((resolve) => {
|
|
977
|
+
const url = new URL(wsDebuggerUrl);
|
|
978
|
+
const wsPort = parseInt(url.port || String(port));
|
|
979
|
+
const key = crypto.randomBytes(16).toString('base64');
|
|
980
|
+
const socket = net.createConnection({ host: url.hostname, port: wsPort }, () => {
|
|
981
|
+
const handshake = [
|
|
982
|
+
`GET ${url.pathname} HTTP/1.1`,
|
|
983
|
+
`Host: ${url.host}`,
|
|
984
|
+
'Upgrade: websocket',
|
|
985
|
+
'Connection: Upgrade',
|
|
986
|
+
`Sec-WebSocket-Key: ${key}`,
|
|
987
|
+
'Sec-WebSocket-Version: 13',
|
|
988
|
+
'',
|
|
989
|
+
'',
|
|
990
|
+
].join('\r\n');
|
|
991
|
+
socket.write(handshake);
|
|
992
|
+
});
|
|
993
|
+
let upgraded = false;
|
|
994
|
+
let buffer = Buffer.alloc(0);
|
|
995
|
+
socket.setTimeout(10000);
|
|
996
|
+
socket.on('timeout', () => { socket.destroy(); resolve(null); });
|
|
997
|
+
socket.on('error', () => resolve(null));
|
|
998
|
+
socket.on('data', (chunk) => {
|
|
999
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
1000
|
+
if (!upgraded) {
|
|
1001
|
+
const headerEnd = buffer.indexOf('\r\n\r\n');
|
|
1002
|
+
if (headerEnd === -1)
|
|
1003
|
+
return;
|
|
1004
|
+
upgraded = true;
|
|
1005
|
+
buffer = buffer.slice(headerEnd + 4);
|
|
1006
|
+
// Send Page.captureScreenshot
|
|
1007
|
+
const msg = JSON.stringify({ id: 1, method: 'Page.captureScreenshot', params: { format: 'png' } });
|
|
1008
|
+
const msgBytes = Buffer.from(msg, 'utf8');
|
|
1009
|
+
const mask = crypto.randomBytes(4);
|
|
1010
|
+
// Build WebSocket frame: FIN=1, opcode=1 (text), MASK=1, payload length
|
|
1011
|
+
let headerBuf;
|
|
1012
|
+
if (msgBytes.length < 126) {
|
|
1013
|
+
headerBuf = Buffer.alloc(6); // 2 bytes header + 4 bytes mask
|
|
1014
|
+
headerBuf[0] = 0x81;
|
|
1015
|
+
headerBuf[1] = 0x80 | msgBytes.length;
|
|
1016
|
+
mask.copy(headerBuf, 2);
|
|
1017
|
+
}
|
|
1018
|
+
else if (msgBytes.length < 65536) {
|
|
1019
|
+
headerBuf = Buffer.alloc(8); // 2 + 2 extended len + 4 mask
|
|
1020
|
+
headerBuf[0] = 0x81;
|
|
1021
|
+
headerBuf[1] = 0x80 | 126;
|
|
1022
|
+
headerBuf.writeUInt16BE(msgBytes.length, 2);
|
|
1023
|
+
mask.copy(headerBuf, 4);
|
|
1024
|
+
}
|
|
1025
|
+
else {
|
|
1026
|
+
headerBuf = Buffer.alloc(14); // 2 + 8 extended len + 4 mask
|
|
1027
|
+
headerBuf[0] = 0x81;
|
|
1028
|
+
headerBuf[1] = 0x80 | 127;
|
|
1029
|
+
headerBuf.writeBigUInt64BE(BigInt(msgBytes.length), 2);
|
|
1030
|
+
mask.copy(headerBuf, 10);
|
|
1031
|
+
}
|
|
1032
|
+
const maskedPayload = Buffer.alloc(msgBytes.length);
|
|
1033
|
+
for (let i = 0; i < msgBytes.length; i++) {
|
|
1034
|
+
maskedPayload[i] = msgBytes[i] ^ mask[i % 4];
|
|
1035
|
+
}
|
|
1036
|
+
socket.write(Buffer.concat([headerBuf, maskedPayload]));
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
// Parse incoming WebSocket frame(s) — may arrive in chunks
|
|
1040
|
+
while (buffer.length >= 2) {
|
|
1041
|
+
const opcode = buffer[0] & 0x0f;
|
|
1042
|
+
if (opcode === 0x08) {
|
|
1043
|
+
// Connection close
|
|
1044
|
+
socket.destroy();
|
|
1045
|
+
resolve(null);
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
const isMasked = (buffer[1] & 0x80) !== 0;
|
|
1049
|
+
const payloadLenByte = buffer[1] & 0x7f;
|
|
1050
|
+
let headerLen = 2 + (isMasked ? 4 : 0);
|
|
1051
|
+
let actualLen = payloadLenByte;
|
|
1052
|
+
if (payloadLenByte === 126) {
|
|
1053
|
+
if (buffer.length < 4)
|
|
1054
|
+
return; // wait for more data
|
|
1055
|
+
actualLen = buffer.readUInt16BE(2);
|
|
1056
|
+
headerLen = 4 + (isMasked ? 4 : 0);
|
|
1057
|
+
}
|
|
1058
|
+
else if (payloadLenByte === 127) {
|
|
1059
|
+
if (buffer.length < 10)
|
|
1060
|
+
return; // wait for more data
|
|
1061
|
+
actualLen = Number(buffer.readBigUInt64BE(2));
|
|
1062
|
+
headerLen = 10 + (isMasked ? 4 : 0);
|
|
1063
|
+
}
|
|
1064
|
+
if (buffer.length < headerLen + actualLen)
|
|
1065
|
+
return; // wait for complete frame
|
|
1066
|
+
let payload = buffer.slice(headerLen, headerLen + actualLen);
|
|
1067
|
+
if (isMasked) {
|
|
1068
|
+
const maskOffset = headerLen - 4;
|
|
1069
|
+
const frameMask = buffer.slice(maskOffset, maskOffset + 4);
|
|
1070
|
+
const unmasked = Buffer.alloc(payload.length);
|
|
1071
|
+
for (let i = 0; i < payload.length; i++) {
|
|
1072
|
+
unmasked[i] = payload[i] ^ frameMask[i % 4];
|
|
1073
|
+
}
|
|
1074
|
+
payload = unmasked;
|
|
1075
|
+
}
|
|
1076
|
+
buffer = buffer.slice(headerLen + actualLen);
|
|
1077
|
+
try {
|
|
1078
|
+
const msgParsed = JSON.parse(payload.toString('utf8'));
|
|
1079
|
+
if (msgParsed.result?.data) {
|
|
1080
|
+
socket.destroy();
|
|
1081
|
+
resolve(msgParsed.result.data);
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
catch {
|
|
1086
|
+
// Not valid JSON or not our message — continue reading
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
});
|
|
1090
|
+
});
|
|
1091
|
+
if (!base64png) {
|
|
1092
|
+
return {
|
|
1093
|
+
success: false,
|
|
1094
|
+
tab: selectedTab,
|
|
1095
|
+
message: `Failed to capture screenshot of tab "${selectedTab.title}" (${selectedTab.url})`,
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
return {
|
|
1099
|
+
success: true,
|
|
1100
|
+
tab: selectedTab,
|
|
1101
|
+
image: base64png,
|
|
1102
|
+
message: `Screenshot captured for tab "${selectedTab.title}" (${selectedTab.url})`,
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
// ============================================================================
|
|
1106
|
+
// Server Setup
|
|
1107
|
+
// ============================================================================
|
|
1108
|
+
const tools = [
|
|
1109
|
+
{
|
|
1110
|
+
name: 'launch_ui_mode',
|
|
1111
|
+
description: 'Launch Playwright in interactive UI mode for manual testing and demos. ' +
|
|
1112
|
+
'Opens a browser with the Playwright test runner UI. ' +
|
|
1113
|
+
'Use project "demo" for full product demos (dashboard + extension in one session), ' +
|
|
1114
|
+
'"manual" for vendor dashboard demos, "extension-manual" for extension demos, ' +
|
|
1115
|
+
'or a vendor role project to test as a specific persona.',
|
|
1116
|
+
schema: LaunchUiModeArgsSchema,
|
|
1117
|
+
handler: launchUiMode,
|
|
1118
|
+
},
|
|
1119
|
+
{
|
|
1120
|
+
name: 'run_tests',
|
|
1121
|
+
description: 'Run Playwright E2E tests headlessly and return pass/fail results. ' +
|
|
1122
|
+
'Filter by project (persona) or test name pattern. ' +
|
|
1123
|
+
'Runs seed + auth-setup automatically as dependencies.',
|
|
1124
|
+
schema: RunTestsArgsSchema,
|
|
1125
|
+
handler: runTests,
|
|
1126
|
+
},
|
|
1127
|
+
{
|
|
1128
|
+
name: 'seed_data',
|
|
1129
|
+
description: 'Seed the E2E test database with deterministic test data. ' +
|
|
1130
|
+
'Creates vendor accounts, customers, integrations, webhooks, API keys, and audit entries. ' +
|
|
1131
|
+
'Idempotent — safe to run multiple times.',
|
|
1132
|
+
schema: SeedDataArgsSchema,
|
|
1133
|
+
handler: seedData,
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
name: 'cleanup_data',
|
|
1137
|
+
description: 'Clean up E2E test data from the database. ' +
|
|
1138
|
+
'Removes all e2e-* prefixed test records. ' +
|
|
1139
|
+
'Set E2E_CLEANUP=true internally.',
|
|
1140
|
+
schema: CleanupDataArgsSchema,
|
|
1141
|
+
handler: cleanupData,
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
name: 'get_report',
|
|
1145
|
+
description: 'Get the last Playwright HTML test report. ' +
|
|
1146
|
+
'Shows report path, last modified time, and optionally opens in browser.',
|
|
1147
|
+
schema: GetReportArgsSchema,
|
|
1148
|
+
handler: getReport,
|
|
1149
|
+
},
|
|
1150
|
+
{
|
|
1151
|
+
name: 'get_coverage_status',
|
|
1152
|
+
description: 'Check E2E test coverage by persona and page. ' +
|
|
1153
|
+
'Returns a matrix of all Playwright projects with test counts, ' +
|
|
1154
|
+
'persona labels, and active/deferred status.',
|
|
1155
|
+
schema: GetCoverageStatusArgsSchema,
|
|
1156
|
+
handler: getCoverageStatus,
|
|
1157
|
+
},
|
|
1158
|
+
{
|
|
1159
|
+
name: 'preflight_check',
|
|
1160
|
+
description: 'Run comprehensive pre-flight validation before launching Playwright. ' +
|
|
1161
|
+
'Checks config file, dependencies, browsers, test files, credentials, ' +
|
|
1162
|
+
'compilation, dev server, and auth state freshness. ALWAYS run before launch_ui_mode or run_tests. ' +
|
|
1163
|
+
'Returns structured result with pass/fail per check and recovery steps.',
|
|
1164
|
+
schema: PreflightCheckArgsSchema,
|
|
1165
|
+
handler: preflightCheck,
|
|
1166
|
+
},
|
|
1167
|
+
{
|
|
1168
|
+
name: 'run_auth_setup',
|
|
1169
|
+
description: 'Refresh Playwright auth state by running seed + auth-setup projects. ' +
|
|
1170
|
+
'Run when .auth/*.json files are missing, expired, or stale (>4h old). ' +
|
|
1171
|
+
'Automatically starts the dev server via playwright.config.ts webServer config. ' +
|
|
1172
|
+
'Takes 2–4 minutes. Required before demo tests can authenticate.',
|
|
1173
|
+
schema: RunAuthSetupArgsSchema,
|
|
1174
|
+
handler: runAuthSetup,
|
|
1175
|
+
},
|
|
1176
|
+
{
|
|
1177
|
+
name: 'list_extension_tabs',
|
|
1178
|
+
description: 'List all open tabs in the CDP-connected extension test browser. ' +
|
|
1179
|
+
'Requires the extension test browser to be running with --remote-debugging-port=9222. ' +
|
|
1180
|
+
'Launch it first with launch_ui_mode({ project: "demo" }) and click a test. ' +
|
|
1181
|
+
'Returns tab IDs, titles, and URLs — use with screenshot_extension_tab.',
|
|
1182
|
+
schema: ListExtensionTabsArgsSchema,
|
|
1183
|
+
handler: listExtensionTabs,
|
|
1184
|
+
},
|
|
1185
|
+
{
|
|
1186
|
+
name: 'screenshot_extension_tab',
|
|
1187
|
+
description: 'Take a screenshot of a specific tab in the extension test browser via CDP. ' +
|
|
1188
|
+
'Returns a base64 PNG image that renders inline in Claude Code. ' +
|
|
1189
|
+
'Use url_pattern to match by URL substring (e.g. "popup.html", "dashboard"). ' +
|
|
1190
|
+
'Requires the extension test browser to be running (see launch_ui_mode with project "demo").',
|
|
1191
|
+
schema: ScreenshotExtensionTabArgsSchema,
|
|
1192
|
+
handler: screenshotExtensionTab,
|
|
1193
|
+
},
|
|
1194
|
+
];
|
|
1195
|
+
const server = new McpServer({
|
|
1196
|
+
name: 'playwright',
|
|
1197
|
+
version: '1.0.0',
|
|
1198
|
+
tools,
|
|
1199
|
+
});
|
|
1200
|
+
server.start();
|
|
1201
|
+
//# sourceMappingURL=server.js.map
|