rcf-lite 0.19.0 → 0.21.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/CHANGELOG.md +38 -0
- package/bin/rcf.js +5 -0
- package/blueprints/application-admin-console/CHANGELOG.md +11 -0
- package/blueprints/application-admin-console/README.md +98 -0
- package/blueprints/application-admin-console/blueprint.json +226 -0
- package/blueprints/application-admin-console/contributions/adrs/adr-2201-application-admin-console-capability-vocabulary.json +25 -0
- package/blueprints/application-admin-console/contributions/adrs/adr-2202-application-admin-console-baseline-roles.json +25 -0
- package/blueprints/application-admin-console/contributions/adrs/adr-2203-application-admin-console-invite-transport.json +20 -0
- package/blueprints/application-admin-console/contributions/adrs/adr-2204-application-admin-console-audit-retention.json +20 -0
- package/blueprints/application-admin-console/contributions/requirements/application-admin-console-req-001.json +18 -0
- package/blueprints/application-admin-console/contributions/requirements/application-admin-console-req-002.json +18 -0
- package/blueprints/application-admin-console/contributions/requirements/application-admin-console-req-003.json +18 -0
- package/blueprints/application-admin-console/contributions/requirements/application-admin-console-req-004.json +18 -0
- package/blueprints/application-admin-console/contributions/requirements/application-admin-console-req-005.json +18 -0
- package/blueprints/application-admin-console/contributions/requirements/application-admin-console-req-006.json +18 -0
- package/blueprints/application-admin-console/contributions/tacs/tac-2201-application-admin-console-shell.json +40 -0
- package/blueprints/application-admin-console/contributions/tacs/tac-2202-application-admin-console-capability-discovery.json +44 -0
- package/blueprints/application-admin-console/contributions/tacs/tac-2203-application-admin-console-permission-matrix.json +34 -0
- package/blueprints/application-admin-console/contributions/tacs/tac-2204-application-admin-console-audit-view.json +40 -0
- package/blueprints/application-admin-console/contributions/user-stories/application-admin-console-us-21101.json +34 -0
- package/blueprints/application-admin-console/contributions/user-stories/application-admin-console-us-21102.json +34 -0
- package/blueprints/application-admin-console/contributions/user-stories/application-admin-console-us-21103.json +34 -0
- package/blueprints/application-admin-console/contributions/user-stories/application-admin-console-us-21104.json +28 -0
- package/blueprints/application-admin-console/contributions/user-stories/application-admin-console-us-21105.json +34 -0
- package/blueprints/application-admin-console/contributions/user-stories/application-admin-console-us-21106.json +27 -0
- package/blueprints/application-admin-console/contributions/user-stories/application-admin-console-us-21107.json +27 -0
- package/blueprints/application-admin-console/contributions/user-stories/application-admin-console-us-21108.json +27 -0
- package/blueprints/application-admin-console/contributions/user-stories/application-admin-console-us-21109.json +33 -0
- package/blueprints/application-admin-console/docs/topics.md +35 -0
- package/blueprints/application-admin-console/guide/application-admin-console.md +29 -0
- package/blueprints/application-admin-console/probe-packs/application-admin-console.pack.mjs +182 -0
- package/blueprints/application-api-rest/docs/topics.md +5 -0
- package/blueprints/application-charts/CHANGELOG.md +12 -0
- package/blueprints/application-charts/README.md +73 -0
- package/blueprints/application-charts/blueprint.json +26 -0
- package/blueprints/application-charts/contributions/adrs/adr-1901-application-charts-engine-choice.json +25 -0
- package/blueprints/application-charts/contributions/adrs/adr-1902-application-charts-accessible-palette.json +25 -0
- package/blueprints/application-charts/contributions/adrs/adr-1903-application-charts-motion.json +25 -0
- package/blueprints/application-charts/contributions/requirements/application-charts-req-001.json +15 -0
- package/blueprints/application-charts/contributions/requirements/application-charts-req-002.json +15 -0
- package/blueprints/application-charts/contributions/requirements/application-charts-req-003.json +15 -0
- package/blueprints/application-charts/contributions/requirements/application-charts-req-004.json +15 -0
- package/blueprints/application-charts/contributions/tacs/tac-1901-application-charts-render-shell.json +42 -0
- package/blueprints/application-charts/contributions/tacs/tac-1902-application-charts-keyboard-traversal.json +40 -0
- package/blueprints/application-charts/contributions/user-stories/application-charts-us-18101.json +35 -0
- package/blueprints/application-charts/contributions/user-stories/application-charts-us-18102.json +35 -0
- package/blueprints/application-charts/contributions/user-stories/application-charts-us-18103.json +35 -0
- package/blueprints/application-charts/contributions/user-stories/application-charts-us-18104.json +35 -0
- package/blueprints/application-charts/contributions/user-stories/application-charts-us-18105.json +35 -0
- package/blueprints/application-charts/contributions/user-stories/application-charts-us-18106.json +35 -0
- package/blueprints/application-charts/docs/topics.md +37 -0
- package/blueprints/application-charts/guide/application-charts.md +44 -0
- package/blueprints/application-charts/probe-packs/application-charts.pack.mjs +191 -0
- package/blueprints/application-dashboard/CHANGELOG.md +10 -0
- package/blueprints/application-dashboard/README.md +102 -0
- package/blueprints/application-dashboard/assets/guidance/dashboard-design.md +124 -0
- package/blueprints/application-dashboard/blueprint.json +29 -0
- package/blueprints/application-dashboard/contributions/adrs/adr-2001-application-dashboard-primary-kpi-enum.json +25 -0
- package/blueprints/application-dashboard/contributions/adrs/adr-2002-application-dashboard-timeframe-presets.json +25 -0
- package/blueprints/application-dashboard/contributions/adrs/adr-2003-application-dashboard-export-formats.json +25 -0
- package/blueprints/application-dashboard/contributions/requirements/application-dashboard-req-001.json +15 -0
- package/blueprints/application-dashboard/contributions/requirements/application-dashboard-req-002.json +15 -0
- package/blueprints/application-dashboard/contributions/requirements/application-dashboard-req-003.json +15 -0
- package/blueprints/application-dashboard/contributions/requirements/application-dashboard-req-004.json +15 -0
- package/blueprints/application-dashboard/contributions/requirements/application-dashboard-req-005.json +15 -0
- package/blueprints/application-dashboard/contributions/tacs/tac-2001-application-dashboard-tile-grid.json +47 -0
- package/blueprints/application-dashboard/contributions/tacs/tac-2002-application-dashboard-filter-chrome.json +41 -0
- package/blueprints/application-dashboard/contributions/tacs/tac-2003-application-dashboard-export-handle.json +35 -0
- package/blueprints/application-dashboard/contributions/user-stories/application-dashboard-us-19101.json +35 -0
- package/blueprints/application-dashboard/contributions/user-stories/application-dashboard-us-19102.json +35 -0
- package/blueprints/application-dashboard/contributions/user-stories/application-dashboard-us-19103.json +35 -0
- package/blueprints/application-dashboard/contributions/user-stories/application-dashboard-us-19104.json +35 -0
- package/blueprints/application-dashboard/contributions/user-stories/application-dashboard-us-19105.json +35 -0
- package/blueprints/application-dashboard/contributions/user-stories/application-dashboard-us-19106.json +35 -0
- package/blueprints/application-dashboard/contributions/user-stories/application-dashboard-us-19107.json +35 -0
- package/blueprints/application-dashboard/docs/topics.md +43 -0
- package/blueprints/application-dashboard/guide/application-dashboard.md +48 -0
- package/blueprints/application-dashboard/probe-packs/application-dashboard.pack.mjs +231 -0
- package/blueprints/application-datatable/CHANGELOG.md +8 -0
- package/blueprints/application-datatable/README.md +43 -0
- package/blueprints/application-datatable/blueprint.json +33 -0
- package/blueprints/application-datatable/contributions/adrs/adr-1801-application-datatable-pattern-choice.json +25 -0
- package/blueprints/application-datatable/contributions/adrs/adr-1802-application-datatable-url-state.json +20 -0
- package/blueprints/application-datatable/contributions/adrs/adr-1803-application-datatable-page-size.json +20 -0
- package/blueprints/application-datatable/contributions/adrs/adr-1804-application-datatable-selection-persistence.json +20 -0
- package/blueprints/application-datatable/contributions/requirements/application-datatable-req-001.json +15 -0
- package/blueprints/application-datatable/contributions/requirements/application-datatable-req-002.json +15 -0
- package/blueprints/application-datatable/contributions/requirements/application-datatable-req-003.json +15 -0
- package/blueprints/application-datatable/contributions/requirements/application-datatable-req-004.json +15 -0
- package/blueprints/application-datatable/contributions/requirements/application-datatable-req-005.json +15 -0
- package/blueprints/application-datatable/contributions/requirements/application-datatable-req-006.json +15 -0
- package/blueprints/application-datatable/contributions/tacs/tac-1801-application-datatable-shell.json +50 -0
- package/blueprints/application-datatable/contributions/tacs/tac-1802-application-datatable-query-adapter.json +39 -0
- package/blueprints/application-datatable/contributions/tacs/tac-1803-application-datatable-selection-model.json +28 -0
- package/blueprints/application-datatable/contributions/user-stories/application-datatable-us-17101.json +44 -0
- package/blueprints/application-datatable/contributions/user-stories/application-datatable-us-17102.json +44 -0
- package/blueprints/application-datatable/contributions/user-stories/application-datatable-us-17103.json +44 -0
- package/blueprints/application-datatable/contributions/user-stories/application-datatable-us-17104.json +44 -0
- package/blueprints/application-datatable/contributions/user-stories/application-datatable-us-17105.json +44 -0
- package/blueprints/application-datatable/contributions/user-stories/application-datatable-us-17106.json +44 -0
- package/blueprints/application-datatable/contributions/user-stories/application-datatable-us-17107.json +44 -0
- package/blueprints/application-datatable/contributions/user-stories/application-datatable-us-17108.json +44 -0
- package/blueprints/application-datatable/contributions/user-stories/application-datatable-us-17109.json +44 -0
- package/blueprints/application-datatable/docs/topics.md +35 -0
- package/blueprints/application-datatable/guide/application-datatable.md +38 -0
- package/blueprints/application-datatable/probe-packs/application-datatable.pack.mjs +240 -0
- package/blueprints/application-error-handling/docs/topics.md +5 -0
- package/blueprints/application-notifications-in-app/CHANGELOG.md +9 -0
- package/blueprints/application-notifications-in-app/README.md +80 -0
- package/blueprints/application-notifications-in-app/blueprint.json +30 -0
- package/blueprints/application-notifications-in-app/contributions/adrs/adr-2101-application-notifications-in-app-live-region-role-mapping.json +25 -0
- package/blueprints/application-notifications-in-app/contributions/adrs/adr-2102-application-notifications-in-app-toast-timeout-floor.json +25 -0
- package/blueprints/application-notifications-in-app/contributions/adrs/adr-2103-application-notifications-in-app-retention-window.json +25 -0
- package/blueprints/application-notifications-in-app/contributions/requirements/application-notifications-in-app-req-001.json +15 -0
- package/blueprints/application-notifications-in-app/contributions/requirements/application-notifications-in-app-req-002.json +15 -0
- package/blueprints/application-notifications-in-app/contributions/requirements/application-notifications-in-app-req-003.json +15 -0
- package/blueprints/application-notifications-in-app/contributions/requirements/application-notifications-in-app-req-004.json +15 -0
- package/blueprints/application-notifications-in-app/contributions/requirements/application-notifications-in-app-req-005.json +15 -0
- package/blueprints/application-notifications-in-app/contributions/tacs/tac-2101-application-notifications-in-app-live-region.json +46 -0
- package/blueprints/application-notifications-in-app/contributions/tacs/tac-2102-application-notifications-in-app-centre.json +52 -0
- package/blueprints/application-notifications-in-app/contributions/tacs/tac-2103-application-notifications-in-app-preferences.json +40 -0
- package/blueprints/application-notifications-in-app/contributions/user-stories/application-notifications-in-app-us-20101.json +35 -0
- package/blueprints/application-notifications-in-app/contributions/user-stories/application-notifications-in-app-us-20102.json +35 -0
- package/blueprints/application-notifications-in-app/contributions/user-stories/application-notifications-in-app-us-20103.json +35 -0
- package/blueprints/application-notifications-in-app/contributions/user-stories/application-notifications-in-app-us-20104.json +35 -0
- package/blueprints/application-notifications-in-app/contributions/user-stories/application-notifications-in-app-us-20105.json +35 -0
- package/blueprints/application-notifications-in-app/contributions/user-stories/application-notifications-in-app-us-20106.json +26 -0
- package/blueprints/application-notifications-in-app/contributions/user-stories/application-notifications-in-app-us-20107.json +26 -0
- package/blueprints/application-notifications-in-app/contributions/user-stories/application-notifications-in-app-us-20108.json +26 -0
- package/blueprints/application-notifications-in-app/docs/topics.md +39 -0
- package/blueprints/application-notifications-in-app/guide/application-notifications-in-app.md +47 -0
- package/blueprints/application-notifications-in-app/probe-packs/application-notifications-in-app.pack.mjs +328 -0
- package/blueprints/application-spa/docs/topics.md +5 -0
- package/blueprints/delivery-ci-workflows/docs/topics.md +5 -0
- package/blueprints/deploy-cloudflare-workers/docs/topics.md +5 -0
- package/blueprints/email-smtp-resend/docs/topics.md +5 -0
- package/blueprints/observability-essentials/docs/topics.md +5 -0
- package/blueprints/observability-logging/CHANGELOG.md +5 -0
- package/blueprints/observability-logging/README.md +1 -1
- package/blueprints/observability-logging/blueprint.json +113 -21
- package/blueprints/observability-logging/docs/topics.md +5 -0
- package/blueprints/observability-probe-endpoints/docs/topics.md +5 -0
- package/blueprints/persistence-data-d1/docs/topics.md +5 -0
- package/blueprints/persistence-data-sqlite/docs/topics.md +5 -0
- package/blueprints/security-auth-clerk/CHANGELOG.md +5 -0
- package/blueprints/security-auth-clerk/blueprint.json +2 -1
- package/blueprints/security-auth-clerk/docs/topics.md +5 -0
- package/blueprints/security-auth-keycloak/CHANGELOG.md +5 -0
- package/blueprints/security-auth-keycloak/blueprint.json +2 -1
- package/blueprints/security-auth-keycloak/docs/topics.md +5 -0
- package/blueprints/security-auth-magic-link/CHANGELOG.md +5 -0
- package/blueprints/security-auth-magic-link/blueprint.json +2 -1
- package/blueprints/security-auth-magic-link/docs/topics.md +5 -0
- package/blueprints/security-auth-oauth2/CHANGELOG.md +5 -0
- package/blueprints/security-auth-oauth2/blueprint.json +2 -1
- package/blueprints/security-auth-oauth2/docs/topics.md +5 -0
- package/blueprints/security-secrets-management/docs/topics.md +5 -0
- package/fixtures/canary-manifest.json +9 -9
- package/package.json +2 -2
- package/rcf/code-nodes/cn-098.json +13 -0
- package/rcf/code-nodes/cn-099.json +13 -0
- package/rcf/code-nodes/cn-100.json +13 -0
- package/rcf/code-nodes/cn-101.json +13 -0
- package/rcf/code-nodes/cn-102.json +13 -0
- package/rcf/code-nodes/cn-103.json +13 -0
- package/rcf/code-nodes/cn-104.json +13 -0
- package/rcf/code-nodes/cn-105.json +13 -0
- package/rcf/code-nodes/cn-106.json +13 -0
- package/rcf/code-nodes/cn-107.json +14 -0
- package/rcf/code-nodes/cn-108.json +13 -0
- package/rcf/code-nodes/cn-109.json +12 -0
- package/rcf/code-nodes/cn-110.json +12 -0
- package/rcf/code-nodes/cn-111.json +14 -0
- package/rcf/code-nodes/cn-112.json +14 -0
- package/rcf/code-nodes/cn-113.json +12 -0
- package/rcf/code-nodes/cn-114.json +13 -0
- package/rcf/code-nodes/cn-115.json +13 -0
- package/rcf/code-nodes/cn-116.json +12 -0
- package/rcf/code-nodes/cn-117.json +13 -0
- package/rcf/code-nodes/cn-118.json +13 -0
- package/rcf/code-nodes/cn-119.json +13 -0
- package/rcf/code-nodes/cn-120.json +13 -0
- package/rcf/code-nodes/cn-121.json +12 -0
- package/rcf/code-nodes/cn-122.json +12 -0
- package/rcf/code-nodes/cn-123.json +13 -0
- package/rcf/code-nodes/cn-124.json +13 -0
- package/rcf/code-nodes/cn-125.json +13 -0
- package/rcf/code-nodes/cn-126.json +13 -0
- package/rcf/code-nodes/cn-127.json +12 -0
- package/rcf/code-nodes/cn-128.json +14 -0
- package/rcf/code-nodes/cn-129.json +14 -0
- package/rcf/code-nodes/cn-130.json +14 -0
- package/rcf/code-nodes/cn-131.json +15 -0
- package/rcf/code-nodes/cn-132.json +12 -0
- package/rcf/code-nodes/cn-133.json +13 -0
- package/rcf/evals/eval-001.json +55 -0
- package/rcf/fbs/fbs-035.json +18 -0
- package/rcf/fbs/fbs-036.json +22 -0
- package/rcf/fbs/fbs-037.json +19 -0
- package/rcf/fbs/fbs-038.json +19 -0
- package/rcf/fbs/fbs-039.json +21 -0
- package/rcf/fbs/fbs-040.json +20 -0
- package/rcf/fbs/fbs-041.json +13 -0
- package/rcf/fbs/fbs-042.json +13 -0
- package/rcf/requirements/req-016.json +37 -0
- package/rcf/requirements/req-017.json +18 -0
- package/rcf/requirements/req-018.json +18 -0
- package/rcf/requirements/req-019.json +38 -0
- package/rcf/requirements/req-020.json +13 -0
- package/rcf/requirements/req-021.json +13 -0
- package/rcf/requirements/req-022.json +13 -0
- package/rcf/requirements/req-023.json +14 -0
- package/rcf/test-suites/ts-045.json +46 -0
- package/rcf/test-suites/ts-046.json +78 -0
- package/rcf/test-suites/ts-047.json +54 -0
- package/rcf/test-suites/ts-048.json +54 -0
- package/rcf/test-suites/ts-049.json +70 -0
- package/rcf/test-suites/ts-050.json +69 -0
- package/rcf/test-suites/ts-051.json +22 -0
- package/rcf/test-suites/ts-052.json +20 -0
- package/rcf/user-stories/us-1601.json +37 -0
- package/rcf/user-stories/us-1701.json +56 -0
- package/rcf/user-stories/us-1801.json +40 -0
- package/rcf/user-stories/us-1901.json +40 -0
- package/rcf/user-stories/us-2001.json +50 -0
- package/rcf/user-stories/us-2101.json +45 -0
- package/rcf/user-stories/us-2201.json +60 -0
- package/rcf/user-stories/us-2301.json +51 -0
- package/releases/releases.yaml +20 -1
- package/src/blueprint/apply.js +98 -1
- package/src/blueprint/capabilities.js +318 -0
- package/src/blueprint/loader.js +247 -0
- package/src/browser-verify/boot.js +180 -0
- package/src/browser-verify/index.js +13 -0
- package/src/browser-verify/manifest-writer.js +31 -6
- package/src/browser-verify/pack-browser.js +294 -0
- package/src/browser-verify/pack-loader.js +176 -0
- package/src/browser-verify/pack-runner.js +193 -0
- package/src/browser-verify/pack-schema.js +155 -0
- package/src/browser-verify/runner.js +24 -1
- package/src/cli/blueprint.js +92 -0
- package/src/cli/browser-verify.js +147 -7
- package/src/cli/create.js +14 -0
- package/src/cli/eval-coverage.js +221 -0
- package/src/cli/eval.js +43 -0
- package/src/cli/finalise.js +64 -0
- package/src/cli/help.js +4 -0
- package/src/core/errors/index.js +7 -0
- package/src/core/store/ids.js +5 -1
- package/src/core/store/init.js +4 -0
- package/src/core/store/loader.js +4 -0
- package/src/core/store/validator.js +8 -1
- package/src/core/store/walker.js +71 -2
- package/src/core/store/writer.js +4 -0
- package/src/eval/judge.js +338 -0
- package/src/finalise/index.js +8 -0
- package/src/finalise/ingest.js +27 -0
- package/src/finalise/ship-without-eval.js +123 -0
- package/src/query/eval-coverage.js +162 -0
- package/src/verify/chain/index.js +67 -0
- package/src/verify/cli/run.js +15 -0
- package/src/verify/engine/index.js +10 -0
- package/src/verify/verdict/index.js +46 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// Boot fallback for probe packs (visual-round-spec-2026-09-04 §3.5).
|
|
2
|
+
//
|
|
3
|
+
// The normal path is a dev server already running at `runtimeUrl`. When
|
|
4
|
+
// a pack declares `boot: { bootCommand, waitForUrl, waitForSelector }`
|
|
5
|
+
// AND the runtime is not answering, this module spawns `bootCommand`
|
|
6
|
+
// from the project root, waits (bounded) for `waitForUrl` then
|
|
7
|
+
// `waitForSelector`, hands the running server off to the pack pass,
|
|
8
|
+
// and stops the process it started when the pass completes.
|
|
9
|
+
//
|
|
10
|
+
// Never used as a per-blueprint harness: this only fires when the URL
|
|
11
|
+
// is genuinely unreachable, per Baz's ruling that the running dev
|
|
12
|
+
// server is the normal case (2026-09-04).
|
|
13
|
+
|
|
14
|
+
import { spawn } from 'node:child_process';
|
|
15
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
16
|
+
|
|
17
|
+
export const DEFAULT_URL_TIMEOUT_MS = 60000;
|
|
18
|
+
export const DEFAULT_SELECTOR_TIMEOUT_MS = 10000;
|
|
19
|
+
export const DEFAULT_REACHABLE_PROBE_TIMEOUT_MS = 3000;
|
|
20
|
+
const POLL_INTERVAL_MS = 500;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} url
|
|
24
|
+
* @param {(url: string, init?: object) => Promise<any>} fetchFn
|
|
25
|
+
* @param {number} timeoutMs
|
|
26
|
+
* @returns {Promise<boolean>}
|
|
27
|
+
*/
|
|
28
|
+
export async function isReachable(url, fetchFn, timeoutMs = DEFAULT_REACHABLE_PROBE_TIMEOUT_MS) {
|
|
29
|
+
if (typeof url !== 'string' || url.length === 0) return false;
|
|
30
|
+
const controller = new AbortController();
|
|
31
|
+
const t = setTimeout(() => controller.abort(), timeoutMs);
|
|
32
|
+
try {
|
|
33
|
+
const res = await fetchFn(url, { signal: controller.signal });
|
|
34
|
+
if (!res) return false;
|
|
35
|
+
// Any HTTP response counts as "server up"; even a 404 tells us a
|
|
36
|
+
// process is listening. Only a network failure means unreachable.
|
|
37
|
+
return typeof res.status === 'number' && res.status > 0;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
} finally {
|
|
41
|
+
clearTimeout(t);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Choose the first boot config across loaded packs (any is fine; the
|
|
47
|
+
* spec pins one boot per pack pass because runtimeUrl is single).
|
|
48
|
+
*
|
|
49
|
+
* @param {Array<import('./pack-loader.js').LoadedPack>} packs
|
|
50
|
+
* @returns {object|null}
|
|
51
|
+
*/
|
|
52
|
+
export function pickBootFromPacks(packs) {
|
|
53
|
+
if (!Array.isArray(packs)) return null;
|
|
54
|
+
for (const pack of packs) {
|
|
55
|
+
if (pack?.boot && typeof pack.boot === 'object') return pack.boot;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* If the runtime does not answer AND `boot` is declared, bring it up.
|
|
62
|
+
*
|
|
63
|
+
* @param {object} args
|
|
64
|
+
* @param {object|null} args.boot
|
|
65
|
+
* @param {string} args.runtimeUrl
|
|
66
|
+
* @param {string} args.projectRoot
|
|
67
|
+
* @param {(url: string, init?: object) => Promise<any>} args.fetch
|
|
68
|
+
* @param {object|null} [args.browser] probe-pack browser (for waitForSelector via snapshot)
|
|
69
|
+
* @param {(line: string) => void} [args.logger]
|
|
70
|
+
* @param {() => number} [args.now] injectable clock (unit tests)
|
|
71
|
+
* @param {{ urlMs?: number, selectorMs?: number, reachableProbeMs?: number }} [args.timeouts]
|
|
72
|
+
* @param {(command: string, opts: object) => import('node:child_process').ChildProcess} [args.spawnFn] injectable spawner (unit tests)
|
|
73
|
+
* @returns {Promise<{ started: boolean, source: 'already-up'|'no-boot'|'no-boot-command'|'boot-command'|'no-runtime-url', stop: () => Promise<void>, notes: string[] }>}
|
|
74
|
+
*/
|
|
75
|
+
export async function bootIfNeeded({
|
|
76
|
+
boot,
|
|
77
|
+
runtimeUrl,
|
|
78
|
+
projectRoot,
|
|
79
|
+
fetch: fetchFn,
|
|
80
|
+
browser = null,
|
|
81
|
+
logger = () => {},
|
|
82
|
+
now = () => Date.now(),
|
|
83
|
+
timeouts = {},
|
|
84
|
+
spawnFn = spawn,
|
|
85
|
+
}) {
|
|
86
|
+
const notes = [];
|
|
87
|
+
const urlTimeout = timeouts.urlMs ?? DEFAULT_URL_TIMEOUT_MS;
|
|
88
|
+
const selectorTimeout = timeouts.selectorMs ?? DEFAULT_SELECTOR_TIMEOUT_MS;
|
|
89
|
+
const reachableTimeout = timeouts.reachableProbeMs ?? DEFAULT_REACHABLE_PROBE_TIMEOUT_MS;
|
|
90
|
+
|
|
91
|
+
if (!boot || typeof boot !== 'object') {
|
|
92
|
+
return { started: false, source: 'no-boot', stop: async () => {}, notes };
|
|
93
|
+
}
|
|
94
|
+
const probeUrl = typeof boot.waitForUrl === 'string' && boot.waitForUrl.length > 0
|
|
95
|
+
? boot.waitForUrl
|
|
96
|
+
: runtimeUrl;
|
|
97
|
+
if (typeof probeUrl !== 'string' || probeUrl.length === 0) {
|
|
98
|
+
return { started: false, source: 'no-runtime-url', stop: async () => {}, notes };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (await isReachable(probeUrl, fetchFn, reachableTimeout)) {
|
|
102
|
+
notes.push(`runtime ${probeUrl} already reachable; boot skipped`);
|
|
103
|
+
return { started: false, source: 'already-up', stop: async () => {}, notes };
|
|
104
|
+
}
|
|
105
|
+
if (typeof boot.bootCommand !== 'string' || boot.bootCommand.length === 0) {
|
|
106
|
+
notes.push(`runtime ${probeUrl} unreachable but pack declares no bootCommand`);
|
|
107
|
+
return { started: false, source: 'no-boot-command', stop: async () => {}, notes };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
logger(`[boot] runtime ${probeUrl} unreachable; spawning: ${boot.bootCommand}`);
|
|
111
|
+
const [cmd, ...args] = boot.bootCommand.split(/\s+/).filter(Boolean);
|
|
112
|
+
const child = spawnFn(cmd, args, {
|
|
113
|
+
cwd: projectRoot,
|
|
114
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
115
|
+
env: { ...process.env },
|
|
116
|
+
detached: false,
|
|
117
|
+
});
|
|
118
|
+
child.stdout?.on('data', (b) => logger(`[boot:stdout] ${b.toString('utf8').trimEnd()}`));
|
|
119
|
+
child.stderr?.on('data', (b) => logger(`[boot:stderr] ${b.toString('utf8').trimEnd()}`));
|
|
120
|
+
|
|
121
|
+
const start = now();
|
|
122
|
+
let up = false;
|
|
123
|
+
while (now() - start < urlTimeout) {
|
|
124
|
+
if (await isReachable(probeUrl, fetchFn, reachableTimeout)) { up = true; break; }
|
|
125
|
+
await sleep(POLL_INTERVAL_MS);
|
|
126
|
+
}
|
|
127
|
+
if (!up) {
|
|
128
|
+
await stopChild(child);
|
|
129
|
+
throw new Error(`boot: waitForUrl ${probeUrl} did not respond within ${urlTimeout}ms (bootCommand: ${boot.bootCommand})`);
|
|
130
|
+
}
|
|
131
|
+
notes.push(`bootCommand up at ${probeUrl} after ${now() - start}ms`);
|
|
132
|
+
|
|
133
|
+
if (typeof boot.waitForSelector === 'string' && boot.waitForSelector.length > 0 && browser) {
|
|
134
|
+
try {
|
|
135
|
+
await browser.goto(probeUrl);
|
|
136
|
+
const s0 = now();
|
|
137
|
+
let selectorSeen = false;
|
|
138
|
+
while (now() - s0 < selectorTimeout) {
|
|
139
|
+
const snap = await browser.snapshot();
|
|
140
|
+
if (matchesSnapshot(snap, boot.waitForSelector)) { selectorSeen = true; break; }
|
|
141
|
+
await sleep(POLL_INTERVAL_MS);
|
|
142
|
+
}
|
|
143
|
+
if (!selectorSeen) notes.push(`waitForSelector '${boot.waitForSelector}' not observed within ${selectorTimeout}ms; proceeding anyway`);
|
|
144
|
+
else notes.push(`waitForSelector '${boot.waitForSelector}' observed`);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
notes.push(`waitForSelector probe failed: ${err.message}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
started: true,
|
|
152
|
+
source: 'boot-command',
|
|
153
|
+
stop: async () => { await stopChild(child); },
|
|
154
|
+
notes,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function stopChild(child) {
|
|
159
|
+
try { child.kill('SIGTERM'); } catch { /* fine */ }
|
|
160
|
+
await new Promise((resolve) => {
|
|
161
|
+
if (typeof child.exitCode === 'number' && child.exitCode !== null) return resolve();
|
|
162
|
+
const timer = setTimeout(() => { try { child.kill('SIGKILL'); } catch { /* fine */ } resolve(); }, 3000);
|
|
163
|
+
child.once?.('exit', () => { clearTimeout(timer); resolve(); });
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Best-effort match of a "selector-shaped" hint against a snapshot text.
|
|
169
|
+
* We accept: raw substring, an aria role hint (e.g. `role="grid"` -> "grid"),
|
|
170
|
+
* or a text-content hint. Exposed for unit tests.
|
|
171
|
+
*/
|
|
172
|
+
export function matchesSnapshot(snapshot, selectorOrRef) {
|
|
173
|
+
if (typeof snapshot !== 'string' || typeof selectorOrRef !== 'string') return false;
|
|
174
|
+
if (snapshot.includes(selectorOrRef)) return true;
|
|
175
|
+
const roleMatch = selectorOrRef.match(/\[role\s*=\s*['"]?([\w-]+)['"]?\]/);
|
|
176
|
+
if (roleMatch && snapshot.includes(roleMatch[1])) return true;
|
|
177
|
+
const bracketed = selectorOrRef.replace(/[[\]"'=*]/g, ' ').trim();
|
|
178
|
+
if (bracketed && snapshot.includes(bracketed)) return true;
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
@@ -27,3 +27,16 @@ export {
|
|
|
27
27
|
runAgentScreenshotCritique,
|
|
28
28
|
stubBrowserDriver,
|
|
29
29
|
} from './runner.js';
|
|
30
|
+
|
|
31
|
+
export { validatePackModule, PACK_SCHEMA_INTERNALS } from './pack-schema.js';
|
|
32
|
+
export { loadProbePacks, readContributedAcIds } from './pack-loader.js';
|
|
33
|
+
export { runProbePacksForFbs } from './pack-runner.js';
|
|
34
|
+
export { createPackBrowser, parseEvaluateResult } from './pack-browser.js';
|
|
35
|
+
export {
|
|
36
|
+
bootIfNeeded,
|
|
37
|
+
isReachable,
|
|
38
|
+
matchesSnapshot,
|
|
39
|
+
pickBootFromPacks,
|
|
40
|
+
DEFAULT_URL_TIMEOUT_MS,
|
|
41
|
+
DEFAULT_SELECTOR_TIMEOUT_MS,
|
|
42
|
+
} from './boot.js';
|
|
@@ -27,25 +27,49 @@ export function nextBrowserVerificationId(manifest, fbsId) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* Aggregate the record verdict per spec §8.5
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* Aggregate the record verdict per spec §8.5, extended for the visual
|
|
31
|
+
* round T-0 probe-pack pass (visual-round-spec-2026-09-04 §3.4):
|
|
32
|
+
* block if any invariant severity=block verdict=fail, OR any auth smoke fail,
|
|
33
|
+
* OR any pack check / pre-check severity=block verdict=fail;
|
|
34
|
+
* warn if any invariant severity=warn verdict=fail (and no block),
|
|
35
|
+
* OR any pack check / pre-check severity=warn verdict=fail (and no block);
|
|
33
36
|
* pass otherwise.
|
|
34
37
|
*
|
|
38
|
+
* `probePacks` is optional so pre-T-0 callers keep the old signature.
|
|
39
|
+
*
|
|
35
40
|
* @param {Array<{ invariant: string, verdict: 'pass'|'warn'|'fail', severity: 'block'|'warn'|'advisory' }>} invariantChecks
|
|
36
41
|
* @param {Array<{ verdict: 'pass'|'warn'|'fail' }>} authSmokeChecks
|
|
42
|
+
* @param {Array<{ applicable?: boolean, checks?: Array<{ verdict: 'pass'|'warn'|'fail'|'skipped', severity?: 'block'|'warn'|'advisory' }>, preChecks?: Array<{ verdict: 'pass'|'warn'|'fail'|'skipped', severity?: 'block'|'warn'|'advisory' }> }>} [probePacks]
|
|
37
43
|
* @returns {'pass'|'warn'|'block'}
|
|
38
44
|
*/
|
|
39
|
-
export function aggregateVerdict(invariantChecks, authSmokeChecks) {
|
|
45
|
+
export function aggregateVerdict(invariantChecks, authSmokeChecks, probePacks) {
|
|
40
46
|
for (const check of invariantChecks) {
|
|
41
47
|
if (check.severity === 'block' && check.verdict === 'fail') return 'block';
|
|
42
48
|
}
|
|
43
49
|
for (const check of authSmokeChecks ?? []) {
|
|
44
50
|
if (check.verdict === 'fail') return 'block';
|
|
45
51
|
}
|
|
52
|
+
for (const pack of probePacks ?? []) {
|
|
53
|
+
if (pack?.applicable === false) continue;
|
|
54
|
+
for (const c of pack?.checks ?? []) {
|
|
55
|
+
if (c?.severity === 'block' && c?.verdict === 'fail') return 'block';
|
|
56
|
+
}
|
|
57
|
+
for (const c of pack?.preChecks ?? []) {
|
|
58
|
+
if (c?.severity === 'block' && c?.verdict === 'fail') return 'block';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
46
61
|
for (const check of invariantChecks) {
|
|
47
62
|
if (check.severity === 'warn' && (check.verdict === 'fail' || check.verdict === 'warn')) return 'warn';
|
|
48
63
|
}
|
|
64
|
+
for (const pack of probePacks ?? []) {
|
|
65
|
+
if (pack?.applicable === false) continue;
|
|
66
|
+
for (const c of pack?.checks ?? []) {
|
|
67
|
+
if (c?.severity === 'warn' && c?.verdict === 'fail') return 'warn';
|
|
68
|
+
}
|
|
69
|
+
for (const c of pack?.preChecks ?? []) {
|
|
70
|
+
if (c?.severity === 'warn' && c?.verdict === 'fail') return 'warn';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
49
73
|
return 'pass';
|
|
50
74
|
}
|
|
51
75
|
|
|
@@ -67,10 +91,10 @@ export function aggregateVerdict(invariantChecks, authSmokeChecks) {
|
|
|
67
91
|
*/
|
|
68
92
|
export function composeBrowserVerificationRecord({
|
|
69
93
|
manifest, fbsId, mode, runtimeProfile, runtimeUrl, routesChecked,
|
|
70
|
-
invariantChecks, authSmokeChecks = [], notes, now = new Date(),
|
|
94
|
+
invariantChecks, authSmokeChecks = [], probePacks = [], notes, now = new Date(),
|
|
71
95
|
}) {
|
|
72
96
|
const id = nextBrowserVerificationId(manifest, fbsId);
|
|
73
|
-
const verdict = aggregateVerdict(invariantChecks, authSmokeChecks);
|
|
97
|
+
const verdict = aggregateVerdict(invariantChecks, authSmokeChecks, probePacks);
|
|
74
98
|
const record = {
|
|
75
99
|
id,
|
|
76
100
|
fbsId,
|
|
@@ -83,6 +107,7 @@ export function composeBrowserVerificationRecord({
|
|
|
83
107
|
verdict,
|
|
84
108
|
};
|
|
85
109
|
if (authSmokeChecks && authSmokeChecks.length > 0) record.authSmokeChecks = authSmokeChecks;
|
|
110
|
+
if (Array.isArray(probePacks) && probePacks.length > 0) record.probePacks = probePacks;
|
|
86
111
|
if (typeof notes === 'string' && notes.length > 0) record.notes = notes;
|
|
87
112
|
return record;
|
|
88
113
|
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// packBrowser: a real headless browser exposed to blueprint-shipped
|
|
2
|
+
// probe packs (visual-round-spec-2026-09-04 §3.3). The browser is
|
|
3
|
+
// provisioned through the pinned Playwright MCP server as a JSON-RPC
|
|
4
|
+
// 2.0 stdio client, with an optional cheaper route via a `playwright`
|
|
5
|
+
// installation already resolved from the consuming project's own
|
|
6
|
+
// node_modules. Both routes expose the same small stable API:
|
|
7
|
+
//
|
|
8
|
+
// goto(url) navigate to a URL
|
|
9
|
+
// snapshot() accessibility-tree text (searchable)
|
|
10
|
+
// evaluate(fn, arg?) run JS in page context; returns parsed value
|
|
11
|
+
// click(selectorOrRef, hint) click an element by CSS selector or a11y ref
|
|
12
|
+
// type(selectorOrRef, text) type into an editable element
|
|
13
|
+
// press(key) press a keyboard key
|
|
14
|
+
// resize(width, height) set viewport dimensions (breakpoint reflow probes)
|
|
15
|
+
// screenshot(filename?) capture a PNG; returns descriptor / path
|
|
16
|
+
// close() close the page + terminate the browser process
|
|
17
|
+
//
|
|
18
|
+
// Zero new npm dependencies: MCP over stdio uses only Node built-ins;
|
|
19
|
+
// the cheap route uses `playwright` peer dep if the project already
|
|
20
|
+
// installed it. No LLM call anywhere.
|
|
21
|
+
|
|
22
|
+
import { spawn } from 'node:child_process';
|
|
23
|
+
import { createRequire } from 'node:module';
|
|
24
|
+
import { pathToFileURL } from 'node:url';
|
|
25
|
+
import { resolve as resolvePath } from 'node:path';
|
|
26
|
+
|
|
27
|
+
import { PLAYWRIGHT_MCP_VERSION } from '../verify/engine/launcher.js';
|
|
28
|
+
|
|
29
|
+
const DEFAULT_TOOL_TIMEOUT_MS = 30000;
|
|
30
|
+
const INIT_TIMEOUT_MS = 45000;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Compose a packBrowser for use by probe packs.
|
|
34
|
+
*
|
|
35
|
+
* @param {object} [options]
|
|
36
|
+
* @param {string} [options.projectRoot] consuming project root; used to try the cheap route
|
|
37
|
+
* @param {string} [options.playwrightMcpVersion] pinned MCP semver (defaults to PLAYWRIGHT_MCP_VERSION)
|
|
38
|
+
* @param {'auto'|'mcp'|'project'} [options.route] force a route (default 'auto')
|
|
39
|
+
* @param {(line: string) => void} [options.logger]
|
|
40
|
+
* @param {NodeJS.ProcessEnv} [options.env]
|
|
41
|
+
* @param {number} [options.timeoutMs] per-RPC timeout
|
|
42
|
+
* @returns {Promise<object>} the packBrowser handle
|
|
43
|
+
*/
|
|
44
|
+
export async function createPackBrowser(options = {}) {
|
|
45
|
+
const {
|
|
46
|
+
projectRoot,
|
|
47
|
+
playwrightMcpVersion = PLAYWRIGHT_MCP_VERSION,
|
|
48
|
+
route = 'auto',
|
|
49
|
+
logger = () => {},
|
|
50
|
+
env = process.env,
|
|
51
|
+
timeoutMs = DEFAULT_TOOL_TIMEOUT_MS,
|
|
52
|
+
} = options;
|
|
53
|
+
|
|
54
|
+
if (route === 'project' || (route === 'auto' && projectRoot && await tryResolveProjectPlaywright(projectRoot))) {
|
|
55
|
+
const handle = await createProjectPlaywrightHandle({ projectRoot, logger });
|
|
56
|
+
if (handle) return handle;
|
|
57
|
+
if (route === 'project') throw new Error(`packBrowser: project route requested but 'playwright' is not installed under ${projectRoot}`);
|
|
58
|
+
}
|
|
59
|
+
return createMcpPackBrowser({ playwrightMcpVersion, logger, env, timeoutMs });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function tryResolveProjectPlaywright(projectRoot) {
|
|
63
|
+
try {
|
|
64
|
+
const req = createRequire(resolvePath(projectRoot, 'package.json'));
|
|
65
|
+
req.resolve('playwright');
|
|
66
|
+
return true;
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function createProjectPlaywrightHandle({ projectRoot, logger }) {
|
|
73
|
+
try {
|
|
74
|
+
const req = createRequire(resolvePath(projectRoot, 'package.json'));
|
|
75
|
+
const playwrightPath = req.resolve('playwright');
|
|
76
|
+
const mod = await import(pathToFileURL(playwrightPath).href);
|
|
77
|
+
const chromium = mod?.chromium ?? mod?.default?.chromium;
|
|
78
|
+
if (!chromium) return null;
|
|
79
|
+
const browser = await chromium.launch({ headless: true });
|
|
80
|
+
const context = await browser.newContext();
|
|
81
|
+
const page = await context.newPage();
|
|
82
|
+
return Object.freeze({
|
|
83
|
+
provisioning: { via: 'project-playwright', playwrightPath },
|
|
84
|
+
async goto(url) { await page.goto(url, { waitUntil: 'load' }); },
|
|
85
|
+
async snapshot() {
|
|
86
|
+
const tree = await page.accessibility.snapshot({ interestingOnly: false });
|
|
87
|
+
return renderA11yTree(tree);
|
|
88
|
+
},
|
|
89
|
+
async evaluate(fn, arg) {
|
|
90
|
+
if (typeof fn === 'function') return page.evaluate(fn, arg);
|
|
91
|
+
return page.evaluate(String(fn));
|
|
92
|
+
},
|
|
93
|
+
async click(selector) { await page.click(selector); },
|
|
94
|
+
async type(selector, text) { await page.fill(selector, text); },
|
|
95
|
+
async press(key) { await page.keyboard.press(key); },
|
|
96
|
+
async resize(width, height) {
|
|
97
|
+
const w = normaliseDim(width, 'width');
|
|
98
|
+
const h = normaliseDim(height, 'height');
|
|
99
|
+
await page.setViewportSize({ width: w, height: h });
|
|
100
|
+
},
|
|
101
|
+
async screenshot(filename) {
|
|
102
|
+
const target = filename ? { path: filename } : undefined;
|
|
103
|
+
const buf = await page.screenshot(target);
|
|
104
|
+
return filename ?? `data:image/png;base64,${buf.toString('base64')}`;
|
|
105
|
+
},
|
|
106
|
+
async close() {
|
|
107
|
+
logger('[pack-browser] closing project-playwright browser');
|
|
108
|
+
try { await page.close(); } catch { /* fine */ }
|
|
109
|
+
try { await context.close(); } catch { /* fine */ }
|
|
110
|
+
try { await browser.close(); } catch { /* fine */ }
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
} catch (err) {
|
|
114
|
+
logger(`[pack-browser] project-playwright route failed: ${err.message}`);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function renderA11yTree(node, depth = 0) {
|
|
120
|
+
if (!node) return '';
|
|
121
|
+
const pad = ' '.repeat(depth);
|
|
122
|
+
const parts = [];
|
|
123
|
+
parts.push(`${pad}${node.role ?? '?'} ${node.name ? JSON.stringify(node.name) : ''}`.trimEnd());
|
|
124
|
+
for (const child of node.children ?? []) parts.push(renderA11yTree(child, depth + 1));
|
|
125
|
+
return parts.join('\n');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function createMcpPackBrowser({ playwrightMcpVersion, logger, env, timeoutMs }) {
|
|
129
|
+
const child = spawn('npx', ['-y', `@playwright/mcp@${playwrightMcpVersion}`, '--headless', '--isolated'], {
|
|
130
|
+
env: { ...env },
|
|
131
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
/** @type {Map<number, { resolve: (v: any) => void, reject: (e: Error) => void, timer: NodeJS.Timeout|null }>} */
|
|
135
|
+
const pending = new Map();
|
|
136
|
+
let nextId = 1;
|
|
137
|
+
let buffer = '';
|
|
138
|
+
let exited = false;
|
|
139
|
+
|
|
140
|
+
child.stdout.on('data', (chunk) => {
|
|
141
|
+
buffer += chunk.toString('utf8');
|
|
142
|
+
for (;;) {
|
|
143
|
+
const nl = buffer.indexOf('\n');
|
|
144
|
+
if (nl === -1) break;
|
|
145
|
+
const line = buffer.slice(0, nl).trim();
|
|
146
|
+
buffer = buffer.slice(nl + 1);
|
|
147
|
+
if (!line) continue;
|
|
148
|
+
let msg;
|
|
149
|
+
try { msg = JSON.parse(line); } catch { continue; }
|
|
150
|
+
if (msg.id != null && pending.has(msg.id)) {
|
|
151
|
+
const p = pending.get(msg.id);
|
|
152
|
+
pending.delete(msg.id);
|
|
153
|
+
if (p.timer) clearTimeout(p.timer);
|
|
154
|
+
if (msg.error) p.reject(new Error(msg.error.message ?? `MCP error ${JSON.stringify(msg.error)}`));
|
|
155
|
+
else p.resolve(msg.result);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
child.stderr.on('data', (chunk) => logger(`[playwright-mcp] ${chunk.toString('utf8').trimEnd()}`));
|
|
160
|
+
child.on('exit', (code) => {
|
|
161
|
+
exited = true;
|
|
162
|
+
for (const [, p] of pending) {
|
|
163
|
+
if (p.timer) clearTimeout(p.timer);
|
|
164
|
+
p.reject(new Error(`Playwright MCP exited (code=${code})`));
|
|
165
|
+
}
|
|
166
|
+
pending.clear();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
function rpc(method, params, { timeout = timeoutMs } = {}) {
|
|
170
|
+
if (exited) return Promise.reject(new Error('Playwright MCP process already exited'));
|
|
171
|
+
const id = nextId++;
|
|
172
|
+
return new Promise((resolve, reject) => {
|
|
173
|
+
const timer = setTimeout(() => {
|
|
174
|
+
if (pending.has(id)) {
|
|
175
|
+
pending.delete(id);
|
|
176
|
+
reject(new Error(`MCP method ${method} timed out after ${timeout}ms`));
|
|
177
|
+
}
|
|
178
|
+
}, timeout);
|
|
179
|
+
pending.set(id, { resolve, reject, timer });
|
|
180
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function tool(name, args) {
|
|
185
|
+
const result = await rpc('tools/call', { name, arguments: args ?? {} });
|
|
186
|
+
if (result?.isError) {
|
|
187
|
+
const text = textOf(result);
|
|
188
|
+
throw new Error(`Playwright MCP ${name} failed: ${text || JSON.stringify(result)}`);
|
|
189
|
+
}
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
await rpc('initialize', {
|
|
194
|
+
protocolVersion: '2024-11-05',
|
|
195
|
+
capabilities: {},
|
|
196
|
+
clientInfo: { name: 'rcf-lite-pack-browser', version: '0.0.0' },
|
|
197
|
+
}, { timeout: INIT_TIMEOUT_MS });
|
|
198
|
+
// Best-effort initialized notification (some servers ignore, that is fine).
|
|
199
|
+
try {
|
|
200
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' })}\n`);
|
|
201
|
+
} catch { /* fine */ }
|
|
202
|
+
|
|
203
|
+
return Object.freeze({
|
|
204
|
+
provisioning: { via: 'playwright-mcp', version: playwrightMcpVersion },
|
|
205
|
+
async goto(url) {
|
|
206
|
+
await tool('browser_navigate', { url });
|
|
207
|
+
},
|
|
208
|
+
async snapshot() {
|
|
209
|
+
return textOf(await tool('browser_snapshot', {}));
|
|
210
|
+
},
|
|
211
|
+
async evaluate(fn, arg) {
|
|
212
|
+
const src = composeEvaluateSource(fn, arg);
|
|
213
|
+
const text = textOf(await tool('browser_evaluate', { function: src }));
|
|
214
|
+
return parseEvaluateResult(text);
|
|
215
|
+
},
|
|
216
|
+
async click(selector, hint) {
|
|
217
|
+
await tool('browser_click', { target: selector, element: hint ?? selector });
|
|
218
|
+
},
|
|
219
|
+
async type(selector, text, hint) {
|
|
220
|
+
await tool('browser_type', { target: selector, text, element: hint ?? selector });
|
|
221
|
+
},
|
|
222
|
+
async press(key) {
|
|
223
|
+
await tool('browser_press_key', { key });
|
|
224
|
+
},
|
|
225
|
+
async resize(width, height) {
|
|
226
|
+
const w = normaliseDim(width, 'width');
|
|
227
|
+
const h = normaliseDim(height, 'height');
|
|
228
|
+
await tool('browser_resize', { width: w, height: h });
|
|
229
|
+
},
|
|
230
|
+
async screenshot(filename) {
|
|
231
|
+
const args = filename ? { filename } : {};
|
|
232
|
+
return textOf(await tool('browser_take_screenshot', args));
|
|
233
|
+
},
|
|
234
|
+
async close() {
|
|
235
|
+
logger('[pack-browser] closing playwright-mcp browser');
|
|
236
|
+
try { await tool('browser_close', {}); } catch { /* fine */ }
|
|
237
|
+
try { child.stdin.end(); } catch { /* fine */ }
|
|
238
|
+
await new Promise((resolve) => {
|
|
239
|
+
if (exited) return resolve();
|
|
240
|
+
const timer = setTimeout(() => { try { child.kill('SIGKILL'); } catch { /* fine */ } resolve(); }, 5000);
|
|
241
|
+
child.once('exit', () => { clearTimeout(timer); resolve(); });
|
|
242
|
+
});
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Coerce a viewport dimension argument to a positive integer.
|
|
249
|
+
* Exposed for unit tests through resize() call sites.
|
|
250
|
+
*/
|
|
251
|
+
function normaliseDim(value, label) {
|
|
252
|
+
const n = Number(value);
|
|
253
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
254
|
+
throw new Error(`packBrowser.resize: ${label} must be a positive number (got ${JSON.stringify(value)})`);
|
|
255
|
+
}
|
|
256
|
+
return Math.round(n);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function textOf(result) {
|
|
260
|
+
const parts = Array.isArray(result?.content) ? result.content : [];
|
|
261
|
+
return parts.map((c) => (c?.type === 'text' ? (c.text ?? '') : '')).join('\n');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function composeEvaluateSource(fn, arg) {
|
|
265
|
+
if (typeof fn !== 'function') return String(fn);
|
|
266
|
+
if (arg === undefined) return fn.toString();
|
|
267
|
+
return `() => (${fn.toString()})(${JSON.stringify(arg)})`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* The MCP browser_evaluate response is a human-readable text block, usually of
|
|
272
|
+
* the shape "- Ran Playwright code:\n... - Result: <json-ish>". Parse the trailing
|
|
273
|
+
* value out of it and JSON-parse when possible. Exported for unit tests.
|
|
274
|
+
*/
|
|
275
|
+
export function parseEvaluateResult(text) {
|
|
276
|
+
if (typeof text !== 'string') return text;
|
|
277
|
+
// MCP 0.0.80+ returns "### Result\n<value>\n### Ran Playwright code\n```js..."
|
|
278
|
+
const headed = text.match(/###\s*Result\s*\n([\s\S]*?)(?=\n###\s|\n?$)/);
|
|
279
|
+
if (headed) {
|
|
280
|
+
const inner = headed[1].trim().replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/i, '').trim();
|
|
281
|
+
try { return JSON.parse(inner); } catch { return inner; }
|
|
282
|
+
}
|
|
283
|
+
// Older MCP shape: "Result: <value>" trailing block.
|
|
284
|
+
const marker = text.lastIndexOf('Result:');
|
|
285
|
+
const tail = marker >= 0 ? text.slice(marker + 'Result:'.length).trim() : text.trim();
|
|
286
|
+
const stripped = tail.replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/i, '').trim();
|
|
287
|
+
try { return JSON.parse(stripped); } catch { return stripped; }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Internals exposed to unit tests. Not part of the public seam a
|
|
292
|
+
* probe pack sees; do not import from a pack.
|
|
293
|
+
*/
|
|
294
|
+
export const PACK_BROWSER_INTERNALS = Object.freeze({ normaliseDim });
|