waypoint-skills 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.
Files changed (132) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +348 -0
  3. package/README.npm.md +56 -0
  4. package/cli/bin/cli.js +127 -0
  5. package/cli/bin/lib/paths.mjs +31 -0
  6. package/cli/bin/postinstall.mjs +25 -0
  7. package/manifest.json +107 -0
  8. package/package.json +44 -0
  9. package/packages/agents/inspiration-scout.md +105 -0
  10. package/packages/agents/orchestrator.md +186 -0
  11. package/packages/agents/scrutiny-validator.md +136 -0
  12. package/packages/agents/user-testing-validator.md +171 -0
  13. package/packages/agents/validator.md +102 -0
  14. package/packages/agents/worker.md +116 -0
  15. package/packages/agents/wp-router.md +69 -0
  16. package/packages/hooks/hooks.json.example +12 -0
  17. package/packages/hooks/templates/mission-worktree-bootstrap.sh +88 -0
  18. package/packages/hooks/templates/run-assertions.sh +48 -0
  19. package/packages/rules/adversarial-context-isolation.mdc +57 -0
  20. package/packages/rules/serial-git-enforcement.mdc +77 -0
  21. package/packages/skills/caveman/SKILL.md +78 -0
  22. package/packages/skills/design-taste-frontend/SKILL.md +1206 -0
  23. package/packages/skills/gpt-taste/SKILL.md +74 -0
  24. package/packages/skills/impeccable/SKILL.md +164 -0
  25. package/packages/skills/impeccable/reference/adapt.md +311 -0
  26. package/packages/skills/impeccable/reference/animate.md +201 -0
  27. package/packages/skills/impeccable/reference/audit.md +133 -0
  28. package/packages/skills/impeccable/reference/bolder.md +120 -0
  29. package/packages/skills/impeccable/reference/brand.md +108 -0
  30. package/packages/skills/impeccable/reference/clarify.md +288 -0
  31. package/packages/skills/impeccable/reference/codex.md +105 -0
  32. package/packages/skills/impeccable/reference/colorize.md +257 -0
  33. package/packages/skills/impeccable/reference/craft.md +123 -0
  34. package/packages/skills/impeccable/reference/critique.md +780 -0
  35. package/packages/skills/impeccable/reference/delight.md +302 -0
  36. package/packages/skills/impeccable/reference/distill.md +111 -0
  37. package/packages/skills/impeccable/reference/document.md +429 -0
  38. package/packages/skills/impeccable/reference/extract.md +69 -0
  39. package/packages/skills/impeccable/reference/harden.md +347 -0
  40. package/packages/skills/impeccable/reference/hooks.md +90 -0
  41. package/packages/skills/impeccable/reference/init.md +172 -0
  42. package/packages/skills/impeccable/reference/interaction-design.md +189 -0
  43. package/packages/skills/impeccable/reference/layout.md +161 -0
  44. package/packages/skills/impeccable/reference/live.md +718 -0
  45. package/packages/skills/impeccable/reference/onboard.md +234 -0
  46. package/packages/skills/impeccable/reference/optimize.md +258 -0
  47. package/packages/skills/impeccable/reference/overdrive.md +130 -0
  48. package/packages/skills/impeccable/reference/polish.md +241 -0
  49. package/packages/skills/impeccable/reference/product.md +60 -0
  50. package/packages/skills/impeccable/reference/quieter.md +99 -0
  51. package/packages/skills/impeccable/reference/shape.md +165 -0
  52. package/packages/skills/impeccable/reference/typeset.md +279 -0
  53. package/packages/skills/impeccable/scripts/command-metadata.json +94 -0
  54. package/packages/skills/impeccable/scripts/context-signals.mjs +225 -0
  55. package/packages/skills/impeccable/scripts/context.mjs +961 -0
  56. package/packages/skills/impeccable/scripts/critique-storage.mjs +242 -0
  57. package/packages/skills/impeccable/scripts/detect-csp.mjs +198 -0
  58. package/packages/skills/impeccable/scripts/detect.mjs +21 -0
  59. package/packages/skills/impeccable/scripts/detector/browser/injected/index.mjs +1937 -0
  60. package/packages/skills/impeccable/scripts/detector/cli/main.mjs +290 -0
  61. package/packages/skills/impeccable/scripts/detector/design-system.mjs +750 -0
  62. package/packages/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +5185 -0
  63. package/packages/skills/impeccable/scripts/detector/detect-antipatterns.mjs +50 -0
  64. package/packages/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +277 -0
  65. package/packages/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +568 -0
  66. package/packages/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +1015 -0
  67. package/packages/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +234 -0
  68. package/packages/skills/impeccable/scripts/detector/engines/visual/screenshot-contrast.mjs +189 -0
  69. package/packages/skills/impeccable/scripts/detector/findings.mjs +12 -0
  70. package/packages/skills/impeccable/scripts/detector/node/file-system.mjs +198 -0
  71. package/packages/skills/impeccable/scripts/detector/profile/profiler.mjs +166 -0
  72. package/packages/skills/impeccable/scripts/detector/registry/antipatterns.mjs +459 -0
  73. package/packages/skills/impeccable/scripts/detector/rules/checks.mjs +2707 -0
  74. package/packages/skills/impeccable/scripts/detector/shared/color.mjs +124 -0
  75. package/packages/skills/impeccable/scripts/detector/shared/constants.mjs +101 -0
  76. package/packages/skills/impeccable/scripts/detector/shared/inline-ignores.mjs +148 -0
  77. package/packages/skills/impeccable/scripts/detector/shared/page.mjs +7 -0
  78. package/packages/skills/impeccable/scripts/hook-admin.mjs +660 -0
  79. package/packages/skills/impeccable/scripts/hook-before-edit.mjs +476 -0
  80. package/packages/skills/impeccable/scripts/hook-lib.mjs +1632 -0
  81. package/packages/skills/impeccable/scripts/hook.mjs +61 -0
  82. package/packages/skills/impeccable/scripts/lib/design-parser.mjs +842 -0
  83. package/packages/skills/impeccable/scripts/lib/impeccable-config.mjs +638 -0
  84. package/packages/skills/impeccable/scripts/lib/impeccable-paths.mjs +128 -0
  85. package/packages/skills/impeccable/scripts/lib/is-generated.mjs +69 -0
  86. package/packages/skills/impeccable/scripts/lib/target-args.mjs +42 -0
  87. package/packages/skills/impeccable/scripts/live/browser-script-parts.mjs +49 -0
  88. package/packages/skills/impeccable/scripts/live/completion.mjs +19 -0
  89. package/packages/skills/impeccable/scripts/live/event-validation.mjs +137 -0
  90. package/packages/skills/impeccable/scripts/live/insert-ui.mjs +458 -0
  91. package/packages/skills/impeccable/scripts/live/manual-apply.mjs +939 -0
  92. package/packages/skills/impeccable/scripts/live/manual-edit-routes.mjs +357 -0
  93. package/packages/skills/impeccable/scripts/live/manual-edits-buffer.mjs +152 -0
  94. package/packages/skills/impeccable/scripts/live/session-store.mjs +289 -0
  95. package/packages/skills/impeccable/scripts/live/svelte-component.mjs +826 -0
  96. package/packages/skills/impeccable/scripts/live/sveltekit-adapter.mjs +274 -0
  97. package/packages/skills/impeccable/scripts/live/ui-core.mjs +180 -0
  98. package/packages/skills/impeccable/scripts/live/vocabulary.mjs +36 -0
  99. package/packages/skills/impeccable/scripts/live-accept.mjs +812 -0
  100. package/packages/skills/impeccable/scripts/live-browser-dom.js +146 -0
  101. package/packages/skills/impeccable/scripts/live-browser-session.js +123 -0
  102. package/packages/skills/impeccable/scripts/live-browser.js +11173 -0
  103. package/packages/skills/impeccable/scripts/live-commit-manual-edits.mjs +1241 -0
  104. package/packages/skills/impeccable/scripts/live-complete.mjs +75 -0
  105. package/packages/skills/impeccable/scripts/live-copy-edit-agent.mjs +683 -0
  106. package/packages/skills/impeccable/scripts/live-discard-manual-edits.mjs +51 -0
  107. package/packages/skills/impeccable/scripts/live-inject.mjs +583 -0
  108. package/packages/skills/impeccable/scripts/live-insert.mjs +272 -0
  109. package/packages/skills/impeccable/scripts/live-manual-edit-evidence.mjs +363 -0
  110. package/packages/skills/impeccable/scripts/live-poll.mjs +384 -0
  111. package/packages/skills/impeccable/scripts/live-resume.mjs +94 -0
  112. package/packages/skills/impeccable/scripts/live-server.mjs +1135 -0
  113. package/packages/skills/impeccable/scripts/live-status.mjs +61 -0
  114. package/packages/skills/impeccable/scripts/live-target.mjs +30 -0
  115. package/packages/skills/impeccable/scripts/live-wrap.mjs +894 -0
  116. package/packages/skills/impeccable/scripts/live.mjs +297 -0
  117. package/packages/skills/impeccable/scripts/modern-screenshot.umd.js +14 -0
  118. package/packages/skills/impeccable/scripts/palette.mjs +633 -0
  119. package/packages/skills/impeccable/scripts/pin.mjs +214 -0
  120. package/packages/skills/ponytail/SKILL.md +117 -0
  121. package/packages/skills/stitch-design-taste/DESIGN.md +121 -0
  122. package/packages/skills/stitch-design-taste/SKILL.md +184 -0
  123. package/packages/skills/waypoint/SKILL.md +67 -0
  124. package/packages/skills/wp/SKILL.md +330 -0
  125. package/packages/skills/wp/caveman-wire.md +148 -0
  126. package/packages/skills/wp/reference.md +411 -0
  127. package/scripts/detect-platform.sh +32 -0
  128. package/scripts/install.sh +123 -0
  129. package/scripts/lib/common.sh +215 -0
  130. package/scripts/sync-skills.sh +21 -0
  131. package/scripts/uninstall.sh +38 -0
  132. package/scripts/waypoint +281 -0
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Anti-Pattern Detector for Impeccable
5
+ * Copyright (c) 2026 Paul Bakaus
6
+ * SPDX-License-Identifier: Apache-2.0
7
+ *
8
+ * Public API facade. Runtime engines live under cli/engine/engines/.
9
+ */
10
+
11
+ import { detectCli } from './cli/main.mjs';
12
+
13
+ export { ANTIPATTERNS, RULE_ENGINE_SUPPORT, getAntipattern, getRulesForCategory, getRuleEngineSupport } from './registry/antipatterns.mjs';
14
+ export { SAFE_TAGS, BORDER_SAFE_TAGS, OVERUSED_FONTS, GENERIC_FONTS, KNOWN_SERIF_FONTS } from './shared/constants.mjs';
15
+ export { isNeutralColor, parseRgb, relativeLuminance, contrastRatio, parseGradientColors, hasChroma, getHue, colorToHex } from './shared/color.mjs';
16
+ export { isFullPage } from './shared/page.mjs';
17
+ export {
18
+ checkElementBorders,
19
+ checkElementMotion,
20
+ checkElementGlow,
21
+ checkPageTypography,
22
+ checkPageLayout,
23
+ checkHtmlPatterns,
24
+ } from './rules/checks.mjs';
25
+ export { createDetectorProfile, summarizeDetectorProfile } from './profile/profiler.mjs';
26
+ export {
27
+ parseFrontmatter as parseDesignFrontmatter,
28
+ normalizeDesignSystem,
29
+ loadDesignSystemForCwd,
30
+ checkSourceDesignSystem,
31
+ collectStaticDesignSystemFindings,
32
+ } from './design-system.mjs';
33
+ export { detectHtml } from './engines/static-html/detect-html.mjs';
34
+ export { detectUrl, createBrowserDetector } from './engines/browser/detect-url.mjs';
35
+ export { detectText, extractStyleBlocks, extractCSSinJS } from './engines/regex/detect-text.mjs';
36
+ export {
37
+ walkDir,
38
+ SCANNABLE_EXTENSIONS,
39
+ SKIP_DIRS,
40
+ buildImportGraph,
41
+ resolveImport,
42
+ detectFrameworkConfig,
43
+ isPortListening,
44
+ FRAMEWORK_CONFIGS,
45
+ } from './node/file-system.mjs';
46
+ export { formatFindings, detectCli } from './cli/main.mjs';
47
+
48
+ const isMainModule = process.argv[1]?.endsWith('detect-antipatterns.mjs') ||
49
+ process.argv[1]?.endsWith('detect-antipatterns.mjs/');
50
+ if (isMainModule) detectCli();
@@ -0,0 +1,277 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import { finding } from '../../findings.mjs';
6
+ import { filterByProviders } from '../../registry/antipatterns.mjs';
7
+ import { profileFindingsAsync, profileStep, profileStepAsync } from '../../profile/profiler.mjs';
8
+ import { captureVisualContrastCandidate } from '../visual/screenshot-contrast.mjs';
9
+
10
+ function serializeDesignSystemForBrowser(designSystem) {
11
+ if (!designSystem?.present) return null;
12
+ return {
13
+ present: true,
14
+ hasFonts: designSystem.hasFonts === true,
15
+ allowedFonts: Array.from(designSystem.allowedFonts || []),
16
+ hasColors: designSystem.hasColors === true,
17
+ allowedColors: Array.from(designSystem.allowedColorKeys?.values?.() || [])
18
+ .map(entry => entry?.color)
19
+ .filter(color => color && Number.isFinite(color.r) && Number.isFinite(color.g) && Number.isFinite(color.b))
20
+ .map(color => ({ r: color.r, g: color.g, b: color.b })),
21
+ hasRadii: designSystem.hasRadii === true,
22
+ allowedRadii: (designSystem.allowedRadii || [])
23
+ .map(entry => Number(entry?.px))
24
+ .filter(px => Number.isFinite(px)),
25
+ hasPillRadius: designSystem.hasPillRadius === true,
26
+ };
27
+ }
28
+
29
+ async function runVisualContrastFallback(page, serializedGroups, options, profile, target) {
30
+ if (options?.visualContrast === false) return [];
31
+ const maxCandidates = Number.isFinite(options?.visualContrastMaxCandidates)
32
+ ? options.visualContrastMaxCandidates
33
+ : 12;
34
+ const scrollOffscreen = options?.visualContrastScrollOffscreen !== false;
35
+ const existingLowContrastSelectors = new Set(
36
+ serializedGroups
37
+ .filter(group => group.findings?.some(f => f.type === 'low-contrast'))
38
+ .map(group => group.selector)
39
+ .filter(Boolean)
40
+ );
41
+
42
+ let browserAnalyses = [];
43
+ const findings = [];
44
+ if (options?.visualContrastBrowser !== false) {
45
+ const browserFindings = await profileFindingsAsync(profile, {
46
+ engine: 'browser',
47
+ phase: 'visual-contrast',
48
+ ruleId: 'browser-fallback',
49
+ target,
50
+ }, async () => {
51
+ browserAnalyses = await page.evaluate(async ({ maxCandidates, scrollOffscreen }) => {
52
+ if (typeof window.impeccableAnalyzeVisualContrast !== 'function') return [];
53
+ return window.impeccableAnalyzeVisualContrast({ maxCandidates, scrollOffscreen });
54
+ }, { maxCandidates, scrollOffscreen });
55
+ return browserAnalyses
56
+ .filter(result => result.finding && !existingLowContrastSelectors.has(result.selector))
57
+ .map(result => result.finding);
58
+ });
59
+ findings.push(...browserFindings);
60
+ }
61
+
62
+ let candidates = browserAnalyses.length > 0 ? browserAnalyses : [];
63
+ if (candidates.length === 0) {
64
+ candidates = await profileStepAsync(profile, {
65
+ engine: 'browser',
66
+ phase: 'visual-contrast',
67
+ ruleId: 'collect-candidates',
68
+ target,
69
+ }, () => page.evaluate(({ maxCandidates }) => {
70
+ if (typeof window.impeccableCollectVisualContrastCandidates !== 'function') return [];
71
+ return window.impeccableCollectVisualContrastCandidates({ maxCandidates });
72
+ }, { maxCandidates }));
73
+ }
74
+
75
+ const viewport = options?.viewport || { width: 1280, height: 800 };
76
+ const browserResolvedSelectors = new Set(
77
+ browserAnalyses
78
+ .filter(result => result.status === 'fail' || result.status === 'pass')
79
+ .map(result => result.selector)
80
+ .filter(Boolean)
81
+ );
82
+ const filtered = candidates.filter(candidate =>
83
+ !existingLowContrastSelectors.has(candidate.selector) &&
84
+ !browserResolvedSelectors.has(candidate.selector)
85
+ );
86
+ if (options?.visualContrastPixel === false) return findings;
87
+ for (const candidate of filtered) {
88
+ const result = await profileFindingsAsync(profile, {
89
+ engine: 'browser',
90
+ phase: 'visual-contrast',
91
+ ruleId: 'pixel-diff',
92
+ target,
93
+ }, async () => {
94
+ const finding = await captureVisualContrastCandidate(page, candidate, viewport);
95
+ return finding ? [finding] : [];
96
+ });
97
+ findings.push(...result);
98
+ }
99
+ return findings;
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Puppeteer detection (for URLs)
104
+ // ---------------------------------------------------------------------------
105
+
106
+ async function detectUrl(url, options = {}) {
107
+ const profile = options?.profile;
108
+ const waitUntil = options?.waitUntil || 'networkidle0';
109
+ const settleMs = Number.isFinite(options?.settleMs) ? options.settleMs : 0;
110
+ const viewport = options?.viewport || { width: 1280, height: 800 };
111
+ const externalBrowser = options?.browser || null;
112
+ let puppeteer;
113
+ if (!externalBrowser) {
114
+ try {
115
+ puppeteer = await profileStepAsync(profile, {
116
+ engine: 'browser',
117
+ phase: 'setup',
118
+ ruleId: 'import-puppeteer',
119
+ target: url,
120
+ }, () => import('puppeteer'));
121
+ } catch {
122
+ throw new Error('puppeteer is required for URL scanning. Install: npm install puppeteer');
123
+ }
124
+ }
125
+
126
+ // Read the browser detection script — reuse it instead of reimplementing
127
+ const browserScriptPath = path.resolve(
128
+ path.dirname(fileURLToPath(import.meta.url)),
129
+ '..',
130
+ '..',
131
+ 'detect-antipatterns-browser.js'
132
+ );
133
+ let browserScript;
134
+ try {
135
+ browserScript = profileStep(profile, {
136
+ engine: 'browser',
137
+ phase: 'setup',
138
+ ruleId: 'read-browser-script',
139
+ target: url,
140
+ }, () => fs.readFileSync(browserScriptPath, 'utf-8'));
141
+ } catch {
142
+ throw new Error(`Browser script not found at ${browserScriptPath}`);
143
+ }
144
+
145
+ // CI runners (GitHub Actions Ubuntu) block unprivileged user namespaces, so
146
+ // Chrome can't initialize its sandbox there. Disable the sandbox only when
147
+ // running in CI; local users keep the default hardened launch.
148
+ const launchArgs = process.env.CI ? ['--no-sandbox', '--disable-setuid-sandbox'] : [];
149
+ const browser = externalBrowser || await profileStepAsync(profile, {
150
+ engine: 'browser',
151
+ phase: 'load',
152
+ ruleId: 'launch-browser',
153
+ target: url,
154
+ }, () => puppeteer.default.launch({ headless: true, args: launchArgs }));
155
+ const page = await profileStepAsync(profile, {
156
+ engine: 'browser',
157
+ phase: 'load',
158
+ ruleId: 'new-page',
159
+ target: url,
160
+ }, () => browser.newPage());
161
+ let results = [];
162
+ try {
163
+ await profileStepAsync(profile, {
164
+ engine: 'browser',
165
+ phase: 'load',
166
+ ruleId: 'set-viewport',
167
+ target: url,
168
+ }, () => page.setViewport(viewport));
169
+ await profileStepAsync(profile, {
170
+ engine: 'browser',
171
+ phase: 'load',
172
+ ruleId: `goto:${waitUntil}`,
173
+ target: url,
174
+ }, () => page.goto(url, { waitUntil, timeout: 30000 }));
175
+ if (settleMs > 0) {
176
+ await profileStepAsync(profile, {
177
+ engine: 'browser',
178
+ phase: 'load',
179
+ ruleId: 'settle',
180
+ target: url,
181
+ }, () => new Promise(resolve => setTimeout(resolve, settleMs)));
182
+ }
183
+
184
+ // Inject the browser detection script and collect results
185
+ const browserDesignSystem = serializeDesignSystemForBrowser(options?.designSystem);
186
+ await profileStepAsync(profile, {
187
+ engine: 'browser',
188
+ phase: 'scan',
189
+ ruleId: 'configure-pure-detect',
190
+ target: url,
191
+ }, () => page.evaluate((designSystem) => {
192
+ window.__IMPECCABLE_CONFIG__ = {
193
+ ...(window.__IMPECCABLE_CONFIG__ || {}),
194
+ autoScan: false,
195
+ ...(designSystem ? { designSystem } : {}),
196
+ };
197
+ }, browserDesignSystem));
198
+ await profileStepAsync(profile, {
199
+ engine: 'browser',
200
+ phase: 'scan',
201
+ ruleId: 'inject-browser-script',
202
+ target: url,
203
+ }, () => page.evaluate(browserScript));
204
+ let serializedGroups = [];
205
+ results = await profileFindingsAsync(profile, {
206
+ engine: 'browser',
207
+ phase: 'scan',
208
+ ruleId: 'browser-scan',
209
+ target: url,
210
+ }, async () => {
211
+ serializedGroups = await page.evaluate(() => {
212
+ if (!window.impeccableDetect) return [];
213
+ return window.impeccableDetect({ decorate: false, serialize: true });
214
+ });
215
+ return serializedGroups.flatMap(({ findings }) =>
216
+ findings.map(f => ({ id: f.type, snippet: f.detail, ignoreValue: f.ignoreValue || '' }))
217
+ );
218
+ });
219
+ const visualFindings = await runVisualContrastFallback(page, serializedGroups, options, profile, url);
220
+ results.push(...visualFindings);
221
+ } finally {
222
+ await profileStepAsync(profile, {
223
+ engine: 'browser',
224
+ phase: 'load',
225
+ ruleId: 'close-page',
226
+ target: url,
227
+ }, () => page.close().catch(() => {}));
228
+ if (!externalBrowser) {
229
+ await profileStepAsync(profile, {
230
+ engine: 'browser',
231
+ phase: 'load',
232
+ ruleId: 'close-browser',
233
+ target: url,
234
+ }, () => browser.close());
235
+ }
236
+ }
237
+ return filterByProviders(results.map(f => {
238
+ const item = finding(f.id, url, f.snippet);
239
+ if (f.ignoreValue) item.ignoreValue = f.ignoreValue;
240
+ return item;
241
+ }), options.providers);
242
+ }
243
+
244
+ async function createBrowserDetector(options = {}) {
245
+ let puppeteer;
246
+ try {
247
+ puppeteer = await import('puppeteer');
248
+ } catch {
249
+ throw new Error('puppeteer is required for URL scanning. Install: npm install puppeteer');
250
+ }
251
+ const launchArgs = options.launchArgs || (process.env.CI ? ['--no-sandbox', '--disable-setuid-sandbox'] : []);
252
+ const browser = options.browser || await puppeteer.default.launch({
253
+ headless: options.headless ?? true,
254
+ args: launchArgs,
255
+ });
256
+ const ownsBrowser = !options.browser;
257
+ const defaults = {
258
+ waitUntil: options.waitUntil || 'load',
259
+ settleMs: Number.isFinite(options.settleMs) ? options.settleMs : 100,
260
+ viewport: options.viewport || { width: 1280, height: 800 },
261
+ };
262
+ return {
263
+ browser,
264
+ async detectUrl(url, scanOptions = {}) {
265
+ return detectUrl(url, {
266
+ ...defaults,
267
+ ...scanOptions,
268
+ browser,
269
+ });
270
+ },
271
+ async close() {
272
+ if (ownsBrowser) await browser.close().catch(() => {});
273
+ },
274
+ };
275
+ }
276
+
277
+ export { runVisualContrastFallback, detectUrl, createBrowserDetector };