claudecode-omc 5.9.1 → 5.11.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/.local/settings/settings.json +8 -0
- package/.omc-curation/governance.json +3 -0
- package/.omc-curation/sources.lock.json +5 -0
- package/README.md +10 -1
- package/bundled/manifest.json +2 -1
- package/bundled/upstream/impeccable/.omc-source/bundle.json +20 -0
- package/bundled/upstream/impeccable/.omc-source/provenance.json +105 -0
- package/bundled/upstream/impeccable/agents/impeccable-manual-edit-applier.md +97 -0
- package/bundled/upstream/impeccable/skills/impeccable/SKILL.md +168 -0
- package/bundled/upstream/impeccable/skills/impeccable/reference/adapt.md +311 -0
- package/bundled/upstream/impeccable/skills/impeccable/reference/animate.md +201 -0
- package/bundled/upstream/impeccable/skills/impeccable/reference/audit.md +133 -0
- package/bundled/upstream/impeccable/skills/impeccable/reference/bolder.md +113 -0
- package/bundled/upstream/impeccable/skills/impeccable/reference/brand.md +108 -0
- package/bundled/upstream/impeccable/skills/impeccable/reference/clarify.md +288 -0
- package/bundled/upstream/impeccable/skills/impeccable/reference/codex.md +105 -0
- package/bundled/upstream/impeccable/skills/impeccable/reference/colorize.md +257 -0
- package/bundled/upstream/impeccable/skills/impeccable/reference/craft.md +123 -0
- package/bundled/upstream/impeccable/skills/impeccable/reference/critique.md +767 -0
- package/bundled/upstream/impeccable/skills/impeccable/reference/delight.md +302 -0
- package/bundled/upstream/impeccable/skills/impeccable/reference/distill.md +111 -0
- package/bundled/upstream/impeccable/skills/impeccable/reference/document.md +429 -0
- package/bundled/upstream/impeccable/skills/impeccable/reference/extract.md +69 -0
- package/bundled/upstream/impeccable/skills/impeccable/reference/harden.md +347 -0
- package/bundled/upstream/impeccable/skills/impeccable/reference/hooks.md +88 -0
- package/bundled/upstream/impeccable/skills/impeccable/reference/init.md +172 -0
- package/bundled/upstream/impeccable/skills/impeccable/reference/interaction-design.md +189 -0
- package/bundled/upstream/impeccable/skills/impeccable/reference/layout.md +161 -0
- package/bundled/upstream/impeccable/skills/impeccable/reference/live.md +718 -0
- package/bundled/upstream/impeccable/skills/impeccable/reference/onboard.md +234 -0
- package/bundled/upstream/impeccable/skills/impeccable/reference/optimize.md +258 -0
- package/bundled/upstream/impeccable/skills/impeccable/reference/overdrive.md +130 -0
- package/bundled/upstream/impeccable/skills/impeccable/reference/polish.md +241 -0
- package/bundled/upstream/impeccable/skills/impeccable/reference/product.md +60 -0
- package/bundled/upstream/impeccable/skills/impeccable/reference/quieter.md +99 -0
- package/bundled/upstream/impeccable/skills/impeccable/reference/shape.md +165 -0
- package/bundled/upstream/impeccable/skills/impeccable/reference/typeset.md +279 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/command-metadata.json +94 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/context-signals.mjs +225 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/context.mjs +280 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/critique-storage.mjs +242 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/detect-csp.mjs +198 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/detect.mjs +21 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/detector/browser/injected/index.mjs +1735 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/detector/cli/main.mjs +244 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +4907 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/detector/detect-antipatterns.mjs +43 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +252 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +552 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +1013 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +208 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/detector/engines/visual/screenshot-contrast.mjs +189 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/detector/findings.mjs +12 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/detector/node/file-system.mjs +198 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/detector/profile/profiler.mjs +166 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/detector/registry/antipatterns.mjs +419 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/detector/rules/checks.mjs +2671 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/detector/shared/color.mjs +124 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/detector/shared/constants.mjs +101 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/detector/shared/page.mjs +7 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/hook-admin.mjs +574 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/hook-before-edit.mjs +473 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/hook-lib.mjs +1286 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/hook.mjs +61 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/lib/design-parser.mjs +835 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/lib/impeccable-paths.mjs +126 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/lib/is-generated.mjs +69 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/live/browser-script-parts.mjs +49 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/live/completion.mjs +19 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/live/event-validation.mjs +137 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/live/insert-ui.mjs +458 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/live/manual-apply.mjs +939 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/live/manual-edit-routes.mjs +357 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/live/manual-edits-buffer.mjs +152 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/live/session-store.mjs +289 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/live/svelte-component.mjs +826 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/live/sveltekit-adapter.mjs +274 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/live/ui-core.mjs +180 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/live/vocabulary.mjs +36 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/live-accept.mjs +812 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/live-browser-dom.js +146 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/live-browser-session.js +123 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/live-browser.js +11086 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/live-commit-manual-edits.mjs +1241 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/live-complete.mjs +75 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/live-copy-edit-agent.mjs +683 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/live-discard-manual-edits.mjs +51 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/live-inject.mjs +583 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/live-insert.mjs +272 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/live-manual-edit-evidence.mjs +363 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/live-poll.mjs +379 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/live-resume.mjs +94 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/live-server.mjs +1134 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/live-status.mjs +61 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/live-wrap.mjs +894 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/live.mjs +246 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/modern-screenshot.umd.js +14 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/palette.mjs +633 -0
- package/bundled/upstream/impeccable/skills/impeccable/scripts/pin.mjs +214 -0
- package/package.json +1 -1
- package/src/cli/source.js +6 -0
- package/src/config/sources.js +15 -0
- package/src/merge/content-patch.js +4 -0
package/bundled/upstream/impeccable/skills/impeccable/scripts/detector/detect-antipatterns.mjs
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
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 { detectHtml } from './engines/static-html/detect-html.mjs';
|
|
27
|
+
export { detectUrl, createBrowserDetector } from './engines/browser/detect-url.mjs';
|
|
28
|
+
export { detectText, extractStyleBlocks, extractCSSinJS } from './engines/regex/detect-text.mjs';
|
|
29
|
+
export {
|
|
30
|
+
walkDir,
|
|
31
|
+
SCANNABLE_EXTENSIONS,
|
|
32
|
+
SKIP_DIRS,
|
|
33
|
+
buildImportGraph,
|
|
34
|
+
resolveImport,
|
|
35
|
+
detectFrameworkConfig,
|
|
36
|
+
isPortListening,
|
|
37
|
+
FRAMEWORK_CONFIGS,
|
|
38
|
+
} from './node/file-system.mjs';
|
|
39
|
+
export { formatFindings, detectCli } from './cli/main.mjs';
|
|
40
|
+
|
|
41
|
+
const isMainModule = process.argv[1]?.endsWith('detect-antipatterns.mjs') ||
|
|
42
|
+
process.argv[1]?.endsWith('detect-antipatterns.mjs/');
|
|
43
|
+
if (isMainModule) detectCli();
|
|
@@ -0,0 +1,252 @@
|
|
|
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
|
+
async function runVisualContrastFallback(page, serializedGroups, options, profile, target) {
|
|
11
|
+
if (options?.visualContrast === false) return [];
|
|
12
|
+
const maxCandidates = Number.isFinite(options?.visualContrastMaxCandidates)
|
|
13
|
+
? options.visualContrastMaxCandidates
|
|
14
|
+
: 12;
|
|
15
|
+
const scrollOffscreen = options?.visualContrastScrollOffscreen !== false;
|
|
16
|
+
const existingLowContrastSelectors = new Set(
|
|
17
|
+
serializedGroups
|
|
18
|
+
.filter(group => group.findings?.some(f => f.type === 'low-contrast'))
|
|
19
|
+
.map(group => group.selector)
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
let browserAnalyses = [];
|
|
24
|
+
const findings = [];
|
|
25
|
+
if (options?.visualContrastBrowser !== false) {
|
|
26
|
+
const browserFindings = await profileFindingsAsync(profile, {
|
|
27
|
+
engine: 'browser',
|
|
28
|
+
phase: 'visual-contrast',
|
|
29
|
+
ruleId: 'browser-fallback',
|
|
30
|
+
target,
|
|
31
|
+
}, async () => {
|
|
32
|
+
browserAnalyses = await page.evaluate(async ({ maxCandidates, scrollOffscreen }) => {
|
|
33
|
+
if (typeof window.impeccableAnalyzeVisualContrast !== 'function') return [];
|
|
34
|
+
return window.impeccableAnalyzeVisualContrast({ maxCandidates, scrollOffscreen });
|
|
35
|
+
}, { maxCandidates, scrollOffscreen });
|
|
36
|
+
return browserAnalyses
|
|
37
|
+
.filter(result => result.finding && !existingLowContrastSelectors.has(result.selector))
|
|
38
|
+
.map(result => result.finding);
|
|
39
|
+
});
|
|
40
|
+
findings.push(...browserFindings);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let candidates = browserAnalyses.length > 0 ? browserAnalyses : [];
|
|
44
|
+
if (candidates.length === 0) {
|
|
45
|
+
candidates = await profileStepAsync(profile, {
|
|
46
|
+
engine: 'browser',
|
|
47
|
+
phase: 'visual-contrast',
|
|
48
|
+
ruleId: 'collect-candidates',
|
|
49
|
+
target,
|
|
50
|
+
}, () => page.evaluate(({ maxCandidates }) => {
|
|
51
|
+
if (typeof window.impeccableCollectVisualContrastCandidates !== 'function') return [];
|
|
52
|
+
return window.impeccableCollectVisualContrastCandidates({ maxCandidates });
|
|
53
|
+
}, { maxCandidates }));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const viewport = options?.viewport || { width: 1280, height: 800 };
|
|
57
|
+
const browserResolvedSelectors = new Set(
|
|
58
|
+
browserAnalyses
|
|
59
|
+
.filter(result => result.status === 'fail' || result.status === 'pass')
|
|
60
|
+
.map(result => result.selector)
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
);
|
|
63
|
+
const filtered = candidates.filter(candidate =>
|
|
64
|
+
!existingLowContrastSelectors.has(candidate.selector) &&
|
|
65
|
+
!browserResolvedSelectors.has(candidate.selector)
|
|
66
|
+
);
|
|
67
|
+
if (options?.visualContrastPixel === false) return findings;
|
|
68
|
+
for (const candidate of filtered) {
|
|
69
|
+
const result = await profileFindingsAsync(profile, {
|
|
70
|
+
engine: 'browser',
|
|
71
|
+
phase: 'visual-contrast',
|
|
72
|
+
ruleId: 'pixel-diff',
|
|
73
|
+
target,
|
|
74
|
+
}, async () => {
|
|
75
|
+
const finding = await captureVisualContrastCandidate(page, candidate, viewport);
|
|
76
|
+
return finding ? [finding] : [];
|
|
77
|
+
});
|
|
78
|
+
findings.push(...result);
|
|
79
|
+
}
|
|
80
|
+
return findings;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Puppeteer detection (for URLs)
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
async function detectUrl(url, options = {}) {
|
|
88
|
+
const profile = options?.profile;
|
|
89
|
+
const waitUntil = options?.waitUntil || 'networkidle0';
|
|
90
|
+
const settleMs = Number.isFinite(options?.settleMs) ? options.settleMs : 0;
|
|
91
|
+
const viewport = options?.viewport || { width: 1280, height: 800 };
|
|
92
|
+
const externalBrowser = options?.browser || null;
|
|
93
|
+
let puppeteer;
|
|
94
|
+
if (!externalBrowser) {
|
|
95
|
+
try {
|
|
96
|
+
puppeteer = await profileStepAsync(profile, {
|
|
97
|
+
engine: 'browser',
|
|
98
|
+
phase: 'setup',
|
|
99
|
+
ruleId: 'import-puppeteer',
|
|
100
|
+
target: url,
|
|
101
|
+
}, () => import('puppeteer'));
|
|
102
|
+
} catch {
|
|
103
|
+
throw new Error('puppeteer is required for URL scanning. Install: npm install puppeteer');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Read the browser detection script — reuse it instead of reimplementing
|
|
108
|
+
const browserScriptPath = path.resolve(
|
|
109
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
110
|
+
'..',
|
|
111
|
+
'..',
|
|
112
|
+
'detect-antipatterns-browser.js'
|
|
113
|
+
);
|
|
114
|
+
let browserScript;
|
|
115
|
+
try {
|
|
116
|
+
browserScript = profileStep(profile, {
|
|
117
|
+
engine: 'browser',
|
|
118
|
+
phase: 'setup',
|
|
119
|
+
ruleId: 'read-browser-script',
|
|
120
|
+
target: url,
|
|
121
|
+
}, () => fs.readFileSync(browserScriptPath, 'utf-8'));
|
|
122
|
+
} catch {
|
|
123
|
+
throw new Error(`Browser script not found at ${browserScriptPath}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// CI runners (GitHub Actions Ubuntu) block unprivileged user namespaces, so
|
|
127
|
+
// Chrome can't initialize its sandbox there. Disable the sandbox only when
|
|
128
|
+
// running in CI; local users keep the default hardened launch.
|
|
129
|
+
const launchArgs = process.env.CI ? ['--no-sandbox', '--disable-setuid-sandbox'] : [];
|
|
130
|
+
const browser = externalBrowser || await profileStepAsync(profile, {
|
|
131
|
+
engine: 'browser',
|
|
132
|
+
phase: 'load',
|
|
133
|
+
ruleId: 'launch-browser',
|
|
134
|
+
target: url,
|
|
135
|
+
}, () => puppeteer.default.launch({ headless: true, args: launchArgs }));
|
|
136
|
+
const page = await profileStepAsync(profile, {
|
|
137
|
+
engine: 'browser',
|
|
138
|
+
phase: 'load',
|
|
139
|
+
ruleId: 'new-page',
|
|
140
|
+
target: url,
|
|
141
|
+
}, () => browser.newPage());
|
|
142
|
+
let results = [];
|
|
143
|
+
try {
|
|
144
|
+
await profileStepAsync(profile, {
|
|
145
|
+
engine: 'browser',
|
|
146
|
+
phase: 'load',
|
|
147
|
+
ruleId: 'set-viewport',
|
|
148
|
+
target: url,
|
|
149
|
+
}, () => page.setViewport(viewport));
|
|
150
|
+
await profileStepAsync(profile, {
|
|
151
|
+
engine: 'browser',
|
|
152
|
+
phase: 'load',
|
|
153
|
+
ruleId: `goto:${waitUntil}`,
|
|
154
|
+
target: url,
|
|
155
|
+
}, () => page.goto(url, { waitUntil, timeout: 30000 }));
|
|
156
|
+
if (settleMs > 0) {
|
|
157
|
+
await profileStepAsync(profile, {
|
|
158
|
+
engine: 'browser',
|
|
159
|
+
phase: 'load',
|
|
160
|
+
ruleId: 'settle',
|
|
161
|
+
target: url,
|
|
162
|
+
}, () => new Promise(resolve => setTimeout(resolve, settleMs)));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Inject the browser detection script and collect results
|
|
166
|
+
await profileStepAsync(profile, {
|
|
167
|
+
engine: 'browser',
|
|
168
|
+
phase: 'scan',
|
|
169
|
+
ruleId: 'configure-pure-detect',
|
|
170
|
+
target: url,
|
|
171
|
+
}, () => page.evaluate(() => {
|
|
172
|
+
window.__IMPECCABLE_CONFIG__ = {
|
|
173
|
+
...(window.__IMPECCABLE_CONFIG__ || {}),
|
|
174
|
+
autoScan: false,
|
|
175
|
+
};
|
|
176
|
+
}));
|
|
177
|
+
await profileStepAsync(profile, {
|
|
178
|
+
engine: 'browser',
|
|
179
|
+
phase: 'scan',
|
|
180
|
+
ruleId: 'inject-browser-script',
|
|
181
|
+
target: url,
|
|
182
|
+
}, () => page.evaluate(browserScript));
|
|
183
|
+
let serializedGroups = [];
|
|
184
|
+
results = await profileFindingsAsync(profile, {
|
|
185
|
+
engine: 'browser',
|
|
186
|
+
phase: 'scan',
|
|
187
|
+
ruleId: 'browser-scan',
|
|
188
|
+
target: url,
|
|
189
|
+
}, async () => {
|
|
190
|
+
serializedGroups = await page.evaluate(() => {
|
|
191
|
+
if (!window.impeccableDetect) return [];
|
|
192
|
+
return window.impeccableDetect({ decorate: false, serialize: true });
|
|
193
|
+
});
|
|
194
|
+
return serializedGroups.flatMap(({ findings }) =>
|
|
195
|
+
findings.map(f => ({ id: f.type, snippet: f.detail }))
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
const visualFindings = await runVisualContrastFallback(page, serializedGroups, options, profile, url);
|
|
199
|
+
results.push(...visualFindings);
|
|
200
|
+
} finally {
|
|
201
|
+
await profileStepAsync(profile, {
|
|
202
|
+
engine: 'browser',
|
|
203
|
+
phase: 'load',
|
|
204
|
+
ruleId: 'close-page',
|
|
205
|
+
target: url,
|
|
206
|
+
}, () => page.close().catch(() => {}));
|
|
207
|
+
if (!externalBrowser) {
|
|
208
|
+
await profileStepAsync(profile, {
|
|
209
|
+
engine: 'browser',
|
|
210
|
+
phase: 'load',
|
|
211
|
+
ruleId: 'close-browser',
|
|
212
|
+
target: url,
|
|
213
|
+
}, () => browser.close());
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return filterByProviders(results.map(f => finding(f.id, url, f.snippet)), options.providers);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function createBrowserDetector(options = {}) {
|
|
220
|
+
let puppeteer;
|
|
221
|
+
try {
|
|
222
|
+
puppeteer = await import('puppeteer');
|
|
223
|
+
} catch {
|
|
224
|
+
throw new Error('puppeteer is required for URL scanning. Install: npm install puppeteer');
|
|
225
|
+
}
|
|
226
|
+
const launchArgs = options.launchArgs || (process.env.CI ? ['--no-sandbox', '--disable-setuid-sandbox'] : []);
|
|
227
|
+
const browser = options.browser || await puppeteer.default.launch({
|
|
228
|
+
headless: options.headless ?? true,
|
|
229
|
+
args: launchArgs,
|
|
230
|
+
});
|
|
231
|
+
const ownsBrowser = !options.browser;
|
|
232
|
+
const defaults = {
|
|
233
|
+
waitUntil: options.waitUntil || 'load',
|
|
234
|
+
settleMs: Number.isFinite(options.settleMs) ? options.settleMs : 100,
|
|
235
|
+
viewport: options.viewport || { width: 1280, height: 800 },
|
|
236
|
+
};
|
|
237
|
+
return {
|
|
238
|
+
browser,
|
|
239
|
+
async detectUrl(url, scanOptions = {}) {
|
|
240
|
+
return detectUrl(url, {
|
|
241
|
+
...defaults,
|
|
242
|
+
...scanOptions,
|
|
243
|
+
browser,
|
|
244
|
+
});
|
|
245
|
+
},
|
|
246
|
+
async close() {
|
|
247
|
+
if (ownsBrowser) await browser.close().catch(() => {});
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export { runVisualContrastFallback, detectUrl, createBrowserDetector };
|