ultra11y 5.14.3 → 5.14.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.opencode/plugins/ultra11y.js +1 -1
- package/package.json +1 -1
- package/scripts/ultra11y.mjs +45 -11
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +45 -11
- package/skills/ultra11y/SKILL.md +1 -1
- package/skills/ultra11y/scripts/ultra11y.mjs +45 -11
- package/src/cli.ts +12 -3
- package/src/scan-local.ts +32 -12
- package/src/scan.ts +35 -1
- package/src/types.ts +1 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"name": "ultra11y",
|
|
11
11
|
"source": "./",
|
|
12
12
|
"description": "Automatic accessibility review of the code under change before every commit, push and pull request, plus full WCAG 2.2 AA / RGAA audits and conformance deliverables on demand.",
|
|
13
|
-
"version": "5.14.
|
|
13
|
+
"version": "5.14.4",
|
|
14
14
|
"author": { "name": "maxgfr" },
|
|
15
15
|
"homepage": "https://github.com/maxgfr/ultra11y",
|
|
16
16
|
"repository": "https://github.com/maxgfr/ultra11y",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
3
|
"description": "Accessibility for WCAG 2.2 AA and country standards (RGAA…): an automatic review of the code under change before every commit, push and pull request, plus full audits and conformance deliverables on demand.",
|
|
4
|
-
"version": "5.14.
|
|
4
|
+
"version": "5.14.4",
|
|
5
5
|
"author": { "name": "maxgfr" },
|
|
6
6
|
"homepage": "https://github.com/maxgfr/ultra11y",
|
|
7
7
|
"repository": "https://github.com/maxgfr/ultra11y",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
3
|
"description": "Accessibility for WCAG 2.2 AA and country standards (RGAA…): an automatic review of the code under change before every commit, push and pull request, plus full audits and conformance deliverables on demand.",
|
|
4
|
-
"version": "5.14.
|
|
4
|
+
"version": "5.14.4",
|
|
5
5
|
"author": { "name": "maxgfr", "url": "https://github.com/maxgfr" },
|
|
6
6
|
"homepage": "https://github.com/maxgfr/ultra11y",
|
|
7
7
|
"repository": "https://github.com/maxgfr/ultra11y",
|
|
@@ -27,7 +27,7 @@ import { fileURLToPath } from "node:url";
|
|
|
27
27
|
// Ownership marker: `ultra11y install --opencode` refuses to overwrite a plugin file that
|
|
28
28
|
// does not carry it, so a user's own ultra11y.js is never clobbered.
|
|
29
29
|
const ULTRA11Y_OPENCODE_PLUGIN = "ULTRA11Y_OPENCODE_PLUGIN";
|
|
30
|
-
const ULTRA11Y_PLUGIN_VERSION = "5.14.
|
|
30
|
+
const ULTRA11Y_PLUGIN_VERSION = "5.14.4";
|
|
31
31
|
|
|
32
32
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
33
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
|
-
"version": "5.14.
|
|
3
|
+
"version": "5.14.4",
|
|
4
4
|
"description": "Audit HTML/CSS/JSX against WCAG 2.2 AA and produce a dated compliance report, or author/review accessible markup (native-HTML-first). A skills.sh agent skill: an install-free static engine (bundled, no npm install) plus the agent's judgment, with check/verify gates against hallucinated non-conformities, and pluggable in-repo standards packs (RGAA France first; Section 508 & EN 301 549 next).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.33.0",
|
package/scripts/ultra11y.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { join as join51, relative as relative5, resolve as resolve16, sep as sep
|
|
|
6
6
|
import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/types.ts
|
|
9
|
-
var VERSION = "5.14.
|
|
9
|
+
var VERSION = "5.14.4";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -59185,10 +59185,23 @@ var PROBE_FIELDS = [
|
|
|
59185
59185
|
{ key: "inputOverflowSpacing", engine: "input-overflow-spacing" },
|
|
59186
59186
|
{ key: "liveRegion", engine: "live-region" }
|
|
59187
59187
|
];
|
|
59188
|
+
function loopbackToHostGateway(target) {
|
|
59189
|
+
const m = /^http:\/\/(localhost|127(?:\.\d{1,3}){3})(:\d+)?(\/.*)?$/i.exec(target);
|
|
59190
|
+
if (!m) return { url: target, addHost: false };
|
|
59191
|
+
return { url: `http://host.docker.internal${m[2] ?? ""}${m[3] ?? ""}`, addHost: true };
|
|
59192
|
+
}
|
|
59188
59193
|
function runRunner(target, isFile, tag, snapshot = true) {
|
|
59194
|
+
const hostTarget = target;
|
|
59189
59195
|
const args2 = ["run", "--rm"];
|
|
59190
59196
|
if (!snapshot) args2.push("-e", "ULTRA11Y_SNAPSHOT=0");
|
|
59191
59197
|
if (isFile) args2.push("-v", `${resolve11(target)}:${MOUNT}:ro`);
|
|
59198
|
+
else {
|
|
59199
|
+
const gw = loopbackToHostGateway(target);
|
|
59200
|
+
if (gw.addHost && process.platform === "linux") {
|
|
59201
|
+
args2.push("--add-host", "host.docker.internal:host-gateway");
|
|
59202
|
+
}
|
|
59203
|
+
target = gw.url;
|
|
59204
|
+
}
|
|
59192
59205
|
args2.push(tag, isFile ? MOUNT : target);
|
|
59193
59206
|
let stdout;
|
|
59194
59207
|
try {
|
|
@@ -59199,7 +59212,12 @@ function runRunner(target, isFile, tag, snapshot = true) {
|
|
|
59199
59212
|
throw new Error(`docker run failed \u2014 ${detail}`);
|
|
59200
59213
|
}
|
|
59201
59214
|
const line = stdout.trim().split("\n").filter(Boolean).pop() ?? "{}";
|
|
59202
|
-
|
|
59215
|
+
const out2 = JSON.parse(line);
|
|
59216
|
+
if (loopbackToHostGateway(hostTarget).addHost && out2.url) {
|
|
59217
|
+
const hostSchemeName = hostTarget.match(/^http:\/\/[^:/]+/)?.[0] ?? hostTarget;
|
|
59218
|
+
out2.url = out2.url.replace(/^http:\/\/host\.docker\.internal(?=:\d+|\/|$)/i, hostSchemeName);
|
|
59219
|
+
}
|
|
59220
|
+
return out2;
|
|
59203
59221
|
}
|
|
59204
59222
|
function hostPageOf(url, target) {
|
|
59205
59223
|
if (!url) return target;
|
|
@@ -59718,18 +59736,29 @@ function localTestedScs(interact) {
|
|
|
59718
59736
|
}
|
|
59719
59737
|
var PW_SPEC = "@playwright/test";
|
|
59720
59738
|
var AXE_SPEC = "@axe-core/playwright";
|
|
59721
|
-
function
|
|
59739
|
+
function localTierStatus(cwd) {
|
|
59740
|
+
const req = createRequire(resolve12(cwd, "package.json"));
|
|
59741
|
+
for (const spec of [PW_SPEC, AXE_SPEC]) {
|
|
59742
|
+
try {
|
|
59743
|
+
req.resolve(spec);
|
|
59744
|
+
} catch {
|
|
59745
|
+
return { ok: false, reason: `${spec} does not resolve from ${cwd}` };
|
|
59746
|
+
}
|
|
59747
|
+
}
|
|
59722
59748
|
try {
|
|
59723
|
-
const req = createRequire(resolve12(cwd, "package.json"));
|
|
59724
|
-
req.resolve(PW_SPEC);
|
|
59725
|
-
req.resolve(AXE_SPEC);
|
|
59726
59749
|
const pw = req(PW_SPEC);
|
|
59727
59750
|
const bin = pw.chromium?.executablePath?.();
|
|
59728
|
-
|
|
59729
|
-
|
|
59730
|
-
|
|
59751
|
+
if (typeof bin === "string" && bin.length > 0 && !existsSync24(bin)) {
|
|
59752
|
+
return { ok: false, reason: `no browser binary at ${bin} \u2014 run \`npx playwright install chromium\`` };
|
|
59753
|
+
}
|
|
59754
|
+
return { ok: true };
|
|
59755
|
+
} catch (e) {
|
|
59756
|
+
return { ok: false, reason: `${PW_SPEC} failed to load: ${e.message.split("\n")[0]}` };
|
|
59731
59757
|
}
|
|
59732
59758
|
}
|
|
59759
|
+
function localAvailable(cwd) {
|
|
59760
|
+
return localTierStatus(cwd).ok;
|
|
59761
|
+
}
|
|
59733
59762
|
function resolveLocalDeps(cwd) {
|
|
59734
59763
|
let chromium;
|
|
59735
59764
|
let AxeBuilder;
|
|
@@ -68668,8 +68697,13 @@ async function cmdScan(p) {
|
|
|
68668
68697
|
if (runtimeFlag === "local") useLocal = true;
|
|
68669
68698
|
else if (runtimeFlag === "docker") useLocal = false;
|
|
68670
68699
|
else if (localAvailable(cwd)) useLocal = true;
|
|
68671
|
-
else if (dockerAvailable())
|
|
68672
|
-
|
|
68700
|
+
else if (dockerAvailable()) {
|
|
68701
|
+
useLocal = false;
|
|
68702
|
+
const s = localTierStatus(cwd);
|
|
68703
|
+
console.error(
|
|
68704
|
+
lang === "fr" ? `ultra11y scan : tier local indisponible${s.ok ? "" : ` (${s.reason})`} \u2014 bascule sur Docker. Passez --runtime local pour en faire une erreur.` : `ultra11y scan: local tier unavailable${s.ok ? "" : ` (${s.reason})`} \u2014 falling back to Docker. Pass --runtime local to make this an error instead.`
|
|
68705
|
+
);
|
|
68706
|
+
} else {
|
|
68673
68707
|
console.error(
|
|
68674
68708
|
lang === "fr" ? "ultra11y scan : aucun runtime disponible \u2014 ni Playwright local (passez --cwd vers un projet avec @playwright/test + @axe-core/playwright install\xE9s), ni Docker. Voir --runtime." : "ultra11y scan: no runtime available \u2014 neither a local Playwright (pass --cwd at a project with @playwright/test + @axe-core/playwright installed) nor Docker. See --runtime."
|
|
68675
68709
|
);
|
|
@@ -5,7 +5,7 @@ when_to_use: "Invoke WITHOUT being asked whenever changed front-end code should
|
|
|
5
5
|
allowed-tools: Bash(node ${CLAUDE_SKILL_DIR}/scripts/ultra11y.mjs *), Read, Grep, Glob
|
|
6
6
|
license: MIT
|
|
7
7
|
metadata:
|
|
8
|
-
version: 5.14.
|
|
8
|
+
version: 5.14.4
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# review-a11y — accessibility review of changed code
|
|
@@ -6,7 +6,7 @@ import { join as join51, relative as relative5, resolve as resolve16, sep as sep
|
|
|
6
6
|
import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/types.ts
|
|
9
|
-
var VERSION = "5.14.
|
|
9
|
+
var VERSION = "5.14.4";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -59185,10 +59185,23 @@ var PROBE_FIELDS = [
|
|
|
59185
59185
|
{ key: "inputOverflowSpacing", engine: "input-overflow-spacing" },
|
|
59186
59186
|
{ key: "liveRegion", engine: "live-region" }
|
|
59187
59187
|
];
|
|
59188
|
+
function loopbackToHostGateway(target) {
|
|
59189
|
+
const m = /^http:\/\/(localhost|127(?:\.\d{1,3}){3})(:\d+)?(\/.*)?$/i.exec(target);
|
|
59190
|
+
if (!m) return { url: target, addHost: false };
|
|
59191
|
+
return { url: `http://host.docker.internal${m[2] ?? ""}${m[3] ?? ""}`, addHost: true };
|
|
59192
|
+
}
|
|
59188
59193
|
function runRunner(target, isFile, tag, snapshot = true) {
|
|
59194
|
+
const hostTarget = target;
|
|
59189
59195
|
const args2 = ["run", "--rm"];
|
|
59190
59196
|
if (!snapshot) args2.push("-e", "ULTRA11Y_SNAPSHOT=0");
|
|
59191
59197
|
if (isFile) args2.push("-v", `${resolve11(target)}:${MOUNT}:ro`);
|
|
59198
|
+
else {
|
|
59199
|
+
const gw = loopbackToHostGateway(target);
|
|
59200
|
+
if (gw.addHost && process.platform === "linux") {
|
|
59201
|
+
args2.push("--add-host", "host.docker.internal:host-gateway");
|
|
59202
|
+
}
|
|
59203
|
+
target = gw.url;
|
|
59204
|
+
}
|
|
59192
59205
|
args2.push(tag, isFile ? MOUNT : target);
|
|
59193
59206
|
let stdout;
|
|
59194
59207
|
try {
|
|
@@ -59199,7 +59212,12 @@ function runRunner(target, isFile, tag, snapshot = true) {
|
|
|
59199
59212
|
throw new Error(`docker run failed \u2014 ${detail}`);
|
|
59200
59213
|
}
|
|
59201
59214
|
const line = stdout.trim().split("\n").filter(Boolean).pop() ?? "{}";
|
|
59202
|
-
|
|
59215
|
+
const out2 = JSON.parse(line);
|
|
59216
|
+
if (loopbackToHostGateway(hostTarget).addHost && out2.url) {
|
|
59217
|
+
const hostSchemeName = hostTarget.match(/^http:\/\/[^:/]+/)?.[0] ?? hostTarget;
|
|
59218
|
+
out2.url = out2.url.replace(/^http:\/\/host\.docker\.internal(?=:\d+|\/|$)/i, hostSchemeName);
|
|
59219
|
+
}
|
|
59220
|
+
return out2;
|
|
59203
59221
|
}
|
|
59204
59222
|
function hostPageOf(url, target) {
|
|
59205
59223
|
if (!url) return target;
|
|
@@ -59718,18 +59736,29 @@ function localTestedScs(interact) {
|
|
|
59718
59736
|
}
|
|
59719
59737
|
var PW_SPEC = "@playwright/test";
|
|
59720
59738
|
var AXE_SPEC = "@axe-core/playwright";
|
|
59721
|
-
function
|
|
59739
|
+
function localTierStatus(cwd) {
|
|
59740
|
+
const req = createRequire(resolve12(cwd, "package.json"));
|
|
59741
|
+
for (const spec of [PW_SPEC, AXE_SPEC]) {
|
|
59742
|
+
try {
|
|
59743
|
+
req.resolve(spec);
|
|
59744
|
+
} catch {
|
|
59745
|
+
return { ok: false, reason: `${spec} does not resolve from ${cwd}` };
|
|
59746
|
+
}
|
|
59747
|
+
}
|
|
59722
59748
|
try {
|
|
59723
|
-
const req = createRequire(resolve12(cwd, "package.json"));
|
|
59724
|
-
req.resolve(PW_SPEC);
|
|
59725
|
-
req.resolve(AXE_SPEC);
|
|
59726
59749
|
const pw = req(PW_SPEC);
|
|
59727
59750
|
const bin = pw.chromium?.executablePath?.();
|
|
59728
|
-
|
|
59729
|
-
|
|
59730
|
-
|
|
59751
|
+
if (typeof bin === "string" && bin.length > 0 && !existsSync24(bin)) {
|
|
59752
|
+
return { ok: false, reason: `no browser binary at ${bin} \u2014 run \`npx playwright install chromium\`` };
|
|
59753
|
+
}
|
|
59754
|
+
return { ok: true };
|
|
59755
|
+
} catch (e) {
|
|
59756
|
+
return { ok: false, reason: `${PW_SPEC} failed to load: ${e.message.split("\n")[0]}` };
|
|
59731
59757
|
}
|
|
59732
59758
|
}
|
|
59759
|
+
function localAvailable(cwd) {
|
|
59760
|
+
return localTierStatus(cwd).ok;
|
|
59761
|
+
}
|
|
59733
59762
|
function resolveLocalDeps(cwd) {
|
|
59734
59763
|
let chromium;
|
|
59735
59764
|
let AxeBuilder;
|
|
@@ -68668,8 +68697,13 @@ async function cmdScan(p) {
|
|
|
68668
68697
|
if (runtimeFlag === "local") useLocal = true;
|
|
68669
68698
|
else if (runtimeFlag === "docker") useLocal = false;
|
|
68670
68699
|
else if (localAvailable(cwd)) useLocal = true;
|
|
68671
|
-
else if (dockerAvailable())
|
|
68672
|
-
|
|
68700
|
+
else if (dockerAvailable()) {
|
|
68701
|
+
useLocal = false;
|
|
68702
|
+
const s = localTierStatus(cwd);
|
|
68703
|
+
console.error(
|
|
68704
|
+
lang === "fr" ? `ultra11y scan : tier local indisponible${s.ok ? "" : ` (${s.reason})`} \u2014 bascule sur Docker. Passez --runtime local pour en faire une erreur.` : `ultra11y scan: local tier unavailable${s.ok ? "" : ` (${s.reason})`} \u2014 falling back to Docker. Pass --runtime local to make this an error instead.`
|
|
68705
|
+
);
|
|
68706
|
+
} else {
|
|
68673
68707
|
console.error(
|
|
68674
68708
|
lang === "fr" ? "ultra11y scan : aucun runtime disponible \u2014 ni Playwright local (passez --cwd vers un projet avec @playwright/test + @axe-core/playwright install\xE9s), ni Docker. Voir --runtime." : "ultra11y scan: no runtime available \u2014 neither a local Playwright (pass --cwd at a project with @playwright/test + @axe-core/playwright installed) nor Docker. See --runtime."
|
|
68675
68709
|
);
|
package/skills/ultra11y/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: "Use to AUDIT existing HTML/CSS/JSX against WCAG 2.2 AA accessibili
|
|
|
4
4
|
when_to_use: "Invoke when the user asks for an accessibility AUDIT or a formal deliverable: audit a repo, site or page against WCAG 2.2 AA or a country standard (RGAA, Section 508, EN 301 549); produce a dated conformance report, a criterion grid, a PRD backlog, or tickets (GitHub/GitLab/Jira); author accessible markup from scratch; wire the repo gate (init --hook/--ci) or a rendered-DOM capture pipeline. For a review of the code UNDER CHANGE (diff, branch, PR), use the `review-a11y` skill instead."
|
|
5
5
|
license: MIT
|
|
6
6
|
metadata:
|
|
7
|
-
version: 5.14.
|
|
7
|
+
version: 5.14.4
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# ultra11y — audit WCAG 2.2 AA and write accessible markup
|
|
@@ -6,7 +6,7 @@ import { join as join51, relative as relative5, resolve as resolve16, sep as sep
|
|
|
6
6
|
import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/types.ts
|
|
9
|
-
var VERSION = "5.14.
|
|
9
|
+
var VERSION = "5.14.4";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
var INAPPLICABLE_STATUS = "C";
|
|
12
12
|
|
|
@@ -59185,10 +59185,23 @@ var PROBE_FIELDS = [
|
|
|
59185
59185
|
{ key: "inputOverflowSpacing", engine: "input-overflow-spacing" },
|
|
59186
59186
|
{ key: "liveRegion", engine: "live-region" }
|
|
59187
59187
|
];
|
|
59188
|
+
function loopbackToHostGateway(target) {
|
|
59189
|
+
const m = /^http:\/\/(localhost|127(?:\.\d{1,3}){3})(:\d+)?(\/.*)?$/i.exec(target);
|
|
59190
|
+
if (!m) return { url: target, addHost: false };
|
|
59191
|
+
return { url: `http://host.docker.internal${m[2] ?? ""}${m[3] ?? ""}`, addHost: true };
|
|
59192
|
+
}
|
|
59188
59193
|
function runRunner(target, isFile, tag, snapshot = true) {
|
|
59194
|
+
const hostTarget = target;
|
|
59189
59195
|
const args2 = ["run", "--rm"];
|
|
59190
59196
|
if (!snapshot) args2.push("-e", "ULTRA11Y_SNAPSHOT=0");
|
|
59191
59197
|
if (isFile) args2.push("-v", `${resolve11(target)}:${MOUNT}:ro`);
|
|
59198
|
+
else {
|
|
59199
|
+
const gw = loopbackToHostGateway(target);
|
|
59200
|
+
if (gw.addHost && process.platform === "linux") {
|
|
59201
|
+
args2.push("--add-host", "host.docker.internal:host-gateway");
|
|
59202
|
+
}
|
|
59203
|
+
target = gw.url;
|
|
59204
|
+
}
|
|
59192
59205
|
args2.push(tag, isFile ? MOUNT : target);
|
|
59193
59206
|
let stdout;
|
|
59194
59207
|
try {
|
|
@@ -59199,7 +59212,12 @@ function runRunner(target, isFile, tag, snapshot = true) {
|
|
|
59199
59212
|
throw new Error(`docker run failed \u2014 ${detail}`);
|
|
59200
59213
|
}
|
|
59201
59214
|
const line = stdout.trim().split("\n").filter(Boolean).pop() ?? "{}";
|
|
59202
|
-
|
|
59215
|
+
const out2 = JSON.parse(line);
|
|
59216
|
+
if (loopbackToHostGateway(hostTarget).addHost && out2.url) {
|
|
59217
|
+
const hostSchemeName = hostTarget.match(/^http:\/\/[^:/]+/)?.[0] ?? hostTarget;
|
|
59218
|
+
out2.url = out2.url.replace(/^http:\/\/host\.docker\.internal(?=:\d+|\/|$)/i, hostSchemeName);
|
|
59219
|
+
}
|
|
59220
|
+
return out2;
|
|
59203
59221
|
}
|
|
59204
59222
|
function hostPageOf(url, target) {
|
|
59205
59223
|
if (!url) return target;
|
|
@@ -59718,18 +59736,29 @@ function localTestedScs(interact) {
|
|
|
59718
59736
|
}
|
|
59719
59737
|
var PW_SPEC = "@playwright/test";
|
|
59720
59738
|
var AXE_SPEC = "@axe-core/playwright";
|
|
59721
|
-
function
|
|
59739
|
+
function localTierStatus(cwd) {
|
|
59740
|
+
const req = createRequire(resolve12(cwd, "package.json"));
|
|
59741
|
+
for (const spec of [PW_SPEC, AXE_SPEC]) {
|
|
59742
|
+
try {
|
|
59743
|
+
req.resolve(spec);
|
|
59744
|
+
} catch {
|
|
59745
|
+
return { ok: false, reason: `${spec} does not resolve from ${cwd}` };
|
|
59746
|
+
}
|
|
59747
|
+
}
|
|
59722
59748
|
try {
|
|
59723
|
-
const req = createRequire(resolve12(cwd, "package.json"));
|
|
59724
|
-
req.resolve(PW_SPEC);
|
|
59725
|
-
req.resolve(AXE_SPEC);
|
|
59726
59749
|
const pw = req(PW_SPEC);
|
|
59727
59750
|
const bin = pw.chromium?.executablePath?.();
|
|
59728
|
-
|
|
59729
|
-
|
|
59730
|
-
|
|
59751
|
+
if (typeof bin === "string" && bin.length > 0 && !existsSync24(bin)) {
|
|
59752
|
+
return { ok: false, reason: `no browser binary at ${bin} \u2014 run \`npx playwright install chromium\`` };
|
|
59753
|
+
}
|
|
59754
|
+
return { ok: true };
|
|
59755
|
+
} catch (e) {
|
|
59756
|
+
return { ok: false, reason: `${PW_SPEC} failed to load: ${e.message.split("\n")[0]}` };
|
|
59731
59757
|
}
|
|
59732
59758
|
}
|
|
59759
|
+
function localAvailable(cwd) {
|
|
59760
|
+
return localTierStatus(cwd).ok;
|
|
59761
|
+
}
|
|
59733
59762
|
function resolveLocalDeps(cwd) {
|
|
59734
59763
|
let chromium;
|
|
59735
59764
|
let AxeBuilder;
|
|
@@ -68668,8 +68697,13 @@ async function cmdScan(p) {
|
|
|
68668
68697
|
if (runtimeFlag === "local") useLocal = true;
|
|
68669
68698
|
else if (runtimeFlag === "docker") useLocal = false;
|
|
68670
68699
|
else if (localAvailable(cwd)) useLocal = true;
|
|
68671
|
-
else if (dockerAvailable())
|
|
68672
|
-
|
|
68700
|
+
else if (dockerAvailable()) {
|
|
68701
|
+
useLocal = false;
|
|
68702
|
+
const s = localTierStatus(cwd);
|
|
68703
|
+
console.error(
|
|
68704
|
+
lang === "fr" ? `ultra11y scan : tier local indisponible${s.ok ? "" : ` (${s.reason})`} \u2014 bascule sur Docker. Passez --runtime local pour en faire une erreur.` : `ultra11y scan: local tier unavailable${s.ok ? "" : ` (${s.reason})`} \u2014 falling back to Docker. Pass --runtime local to make this an error instead.`
|
|
68705
|
+
);
|
|
68706
|
+
} else {
|
|
68673
68707
|
console.error(
|
|
68674
68708
|
lang === "fr" ? "ultra11y scan : aucun runtime disponible \u2014 ni Playwright local (passez --cwd vers un projet avec @playwright/test + @axe-core/playwright install\xE9s), ni Docker. Voir --runtime." : "ultra11y scan: no runtime available \u2014 neither a local Playwright (pass --cwd at a project with @playwright/test + @axe-core/playwright installed) nor Docker. See --runtime."
|
|
68675
68709
|
);
|
package/src/cli.ts
CHANGED
|
@@ -53,7 +53,7 @@ import {
|
|
|
53
53
|
import { entriesFrom, isLedger, ledgerPath, mergeLedger, readLedger, replayLedger, type VerdictLedger, writeLedger } from "./ledger.js";
|
|
54
54
|
import { BATCH_SIZE, apiKeyFromEnv, applyRawVerdicts, judgeAll, modelFromEnv } from "./llm.js";
|
|
55
55
|
import { runScan, runScanMany, runCrawlScan, runSampleScan, mergeDynamic, mergeSnapshotAudit, cleanDynamic, dockerAvailable } from "./scan.js";
|
|
56
|
-
import { runScanLocal, runScanManyLocal, runCrawlScanLocal, runSampleScanLocal, localAvailable } from "./scan-local.js";
|
|
56
|
+
import { runScanLocal, runScanManyLocal, runCrawlScanLocal, runSampleScanLocal, localAvailable, localTierStatus } from "./scan-local.js";
|
|
57
57
|
import { validateSample, lintSample, kindLabel, proposeSamplePages, mergeSample, sampleFromSnapshots, unionSample } from "./sample.js";
|
|
58
58
|
import { createAdapter } from "./external/registry.js";
|
|
59
59
|
import { diffSides, sideOfExternal, type DiffBucket } from "./external/diff.js";
|
|
@@ -3244,8 +3244,17 @@ async function cmdScan(p: ParsedArgs): Promise<number> {
|
|
|
3244
3244
|
if (runtimeFlag === "local") useLocal = true;
|
|
3245
3245
|
else if (runtimeFlag === "docker") useLocal = false;
|
|
3246
3246
|
else if (localAvailable(cwd)) useLocal = true;
|
|
3247
|
-
else if (dockerAvailable())
|
|
3248
|
-
|
|
3247
|
+
else if (dockerAvailable()) {
|
|
3248
|
+
useLocal = false;
|
|
3249
|
+
// A silent degrade is indistinguishable from a working fallback — name what was
|
|
3250
|
+
// missing, so the one line a reader gets answers "what do I install".
|
|
3251
|
+
const s = localTierStatus(cwd);
|
|
3252
|
+
console.error(
|
|
3253
|
+
lang === "fr"
|
|
3254
|
+
? `ultra11y scan : tier local indisponible${s.ok ? "" : ` (${s.reason})`} — bascule sur Docker. Passez --runtime local pour en faire une erreur.`
|
|
3255
|
+
: `ultra11y scan: local tier unavailable${s.ok ? "" : ` (${s.reason})`} — falling back to Docker. Pass --runtime local to make this an error instead.`,
|
|
3256
|
+
);
|
|
3257
|
+
} else {
|
|
3249
3258
|
console.error(
|
|
3250
3259
|
lang === "fr"
|
|
3251
3260
|
? "ultra11y scan : aucun runtime disponible — ni Playwright local (passez --cwd vers un projet avec @playwright/test + @axe-core/playwright installés), ni Docker. Voir --runtime."
|
package/src/scan-local.ts
CHANGED
|
@@ -59,6 +59,37 @@ interface LocalDeps {
|
|
|
59
59
|
const PW_SPEC = "@playwright/test";
|
|
60
60
|
const AXE_SPEC = "@axe-core/playwright";
|
|
61
61
|
|
|
62
|
+
/** Why the LOCAL tier is (un)usable from `cwd` — the one line a reader of a
|
|
63
|
+
* `runtime: auto` log gets before the Docker fallback takes over. A silent degrade is
|
|
64
|
+
* indistinguishable from a working fallback, so the reason must NAME what to install. */
|
|
65
|
+
export type LocalTierStatus = { ok: true } | { ok: false; reason: string };
|
|
66
|
+
|
|
67
|
+
/** The same probe as `localAvailable`, keeping the reason instead of collapsing it to a
|
|
68
|
+
* boolean. Drives the `runtime: auto` decision AND the message printed when it degrades
|
|
69
|
+
* to Docker. */
|
|
70
|
+
export function localTierStatus(cwd: string): LocalTierStatus {
|
|
71
|
+
const req = createRequire(resolve(cwd, "package.json"));
|
|
72
|
+
for (const spec of [PW_SPEC, AXE_SPEC]) {
|
|
73
|
+
try {
|
|
74
|
+
req.resolve(spec);
|
|
75
|
+
} catch {
|
|
76
|
+
return { ok: false, reason: `${spec} does not resolve from ${cwd}` };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const pw = req(PW_SPEC) as { chromium?: { executablePath?: () => string } };
|
|
81
|
+
const bin = pw.chromium?.executablePath?.();
|
|
82
|
+
// No path at all ⇒ a Playwright that cannot tell us; treat as available and let the
|
|
83
|
+
// launch report its own failure, rather than refusing a tier that might work.
|
|
84
|
+
if (typeof bin === "string" && bin.length > 0 && !existsSync(bin)) {
|
|
85
|
+
return { ok: false, reason: `no browser binary at ${bin} — run \`npx playwright install chromium\`` };
|
|
86
|
+
}
|
|
87
|
+
return { ok: true };
|
|
88
|
+
} catch (e) {
|
|
89
|
+
return { ok: false, reason: `${PW_SPEC} failed to load: ${(e as Error).message.split("\n")[0]}` };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
62
93
|
/** Is the LOCAL tier actually usable from `cwd`? Drives `runtime: auto`.
|
|
63
94
|
*
|
|
64
95
|
* Both packages must resolve — and the browser BINARY must be on disk. The second half is not
|
|
@@ -73,18 +104,7 @@ const AXE_SPEC = "@axe-core/playwright";
|
|
|
73
104
|
* browser, and failed rather than degrading. `executablePath()` is a pure path computation, so
|
|
74
105
|
* this stays cheap: no launch, no download, one `stat`. */
|
|
75
106
|
export function localAvailable(cwd: string): boolean {
|
|
76
|
-
|
|
77
|
-
const req = createRequire(resolve(cwd, "package.json"));
|
|
78
|
-
req.resolve(PW_SPEC);
|
|
79
|
-
req.resolve(AXE_SPEC);
|
|
80
|
-
const pw = req(PW_SPEC) as { chromium?: { executablePath?: () => string } };
|
|
81
|
-
const bin = pw.chromium?.executablePath?.();
|
|
82
|
-
// No path at all ⇒ a Playwright that cannot tell us; treat as available and let the launch
|
|
83
|
-
// report its own failure, rather than refusing a tier that might work.
|
|
84
|
-
return typeof bin !== "string" || bin.length === 0 ? true : existsSync(bin);
|
|
85
|
-
} catch {
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
107
|
+
return localTierStatus(cwd).ok;
|
|
88
108
|
}
|
|
89
109
|
|
|
90
110
|
/** Load Playwright `chromium` + the `AxeBuilder` class from the target project. */
|
package/src/scan.ts
CHANGED
|
@@ -251,10 +251,34 @@ const PROBE_FIELDS: { key: keyof RunnerOutput; engine: Exclude<DynamicEngine, "a
|
|
|
251
251
|
{ key: "liveRegion", engine: "live-region" },
|
|
252
252
|
];
|
|
253
253
|
|
|
254
|
+
/** Rewrite a HOST loopback URL into one a container can reach. `docker run` gets no
|
|
255
|
+
* `--network host`, so the container's 127.0.0.1 is its own — a scan of
|
|
256
|
+
* `http://localhost:3000` (the action's `start:` + `urls:` shape) died on
|
|
257
|
+
* ERR_CONNECTION_REFUSED while the app ran the whole time, which is how this
|
|
258
|
+
* repository's CI stayed red for a day. `host.docker.internal` is natively resolved by
|
|
259
|
+
* Docker Desktop (macOS/Windows); on Linux the caller adds
|
|
260
|
+
* `--add-host host.docker.internal:host-gateway`. https:// loopback is deliberately NOT
|
|
261
|
+
* rewritten: the container would fail the self-signed certificate anyway. */
|
|
262
|
+
export function loopbackToHostGateway(target: string): { url: string; addHost: boolean } {
|
|
263
|
+
const m = /^http:\/\/(localhost|127(?:\.\d{1,3}){3})(:\d+)?(\/.*)?$/i.exec(target);
|
|
264
|
+
if (!m) return { url: target, addHost: false };
|
|
265
|
+
return { url: `http://host.docker.internal${m[2] ?? ""}${m[3] ?? ""}`, addHost: true };
|
|
266
|
+
}
|
|
267
|
+
|
|
254
268
|
function runRunner(target: string, isFile: boolean, tag: string, snapshot = true): RunnerOutput {
|
|
269
|
+
const hostTarget = target;
|
|
255
270
|
const args = ["run", "--rm"];
|
|
256
271
|
if (!snapshot) args.push("-e", "ULTRA11Y_SNAPSHOT=0");
|
|
257
272
|
if (isFile) args.push("-v", `${resolve(target)}:${MOUNT}:ro`);
|
|
273
|
+
else {
|
|
274
|
+
// The container must reach the HOST's loopback through host.docker.internal; on
|
|
275
|
+
// Linux that name needs the host-gateway mapping Docker Desktop provides natively.
|
|
276
|
+
const gw = loopbackToHostGateway(target);
|
|
277
|
+
if (gw.addHost && process.platform === "linux") {
|
|
278
|
+
args.push("--add-host", "host.docker.internal:host-gateway");
|
|
279
|
+
}
|
|
280
|
+
target = gw.url;
|
|
281
|
+
}
|
|
258
282
|
args.push(tag, isFile ? MOUNT : target);
|
|
259
283
|
let stdout: string;
|
|
260
284
|
try {
|
|
@@ -272,7 +296,17 @@ function runRunner(target: string, isFile: boolean, tag: string, snapshot = true
|
|
|
272
296
|
throw new Error(`docker run failed — ${detail}`);
|
|
273
297
|
}
|
|
274
298
|
const line = stdout.trim().split("\n").filter(Boolean).pop() ?? "{}";
|
|
275
|
-
|
|
299
|
+
const out = JSON.parse(line) as RunnerOutput;
|
|
300
|
+
// Report what a HOST reader can open: the runner finished on host.docker.internal, and
|
|
301
|
+
// every downstream citation (hostPageOf, snapshot identity) derives from out.url. Only
|
|
302
|
+
// the HOSTNAME is restored — the port stays from the container URL, which was copied
|
|
303
|
+
// from the target, so pasting a full origin would double it
|
|
304
|
+
// (`http://127.0.0.1:8931:8931/…`).
|
|
305
|
+
if (loopbackToHostGateway(hostTarget).addHost && out.url) {
|
|
306
|
+
const hostSchemeName = hostTarget.match(/^http:\/\/[^:/]+/)?.[0] ?? hostTarget;
|
|
307
|
+
out.url = out.url.replace(/^http:\/\/host\.docker\.internal(?=:\d+|\/|$)/i, hostSchemeName);
|
|
308
|
+
}
|
|
309
|
+
return out;
|
|
276
310
|
}
|
|
277
311
|
|
|
278
312
|
/** Map the runner's reported page back to a HOST-meaningful citation: the container mount
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Single source of truth for shared types + the embedded version. sync-version.mjs
|
|
2
2
|
// rewrites VERSION at release time (kept in lockstep with package.json + SKILL.md).
|
|
3
|
-
export const VERSION = "5.14.
|
|
3
|
+
export const VERSION = "5.14.4";
|
|
4
4
|
export const SCHEMA_VERSION = 2;
|
|
5
5
|
|
|
6
6
|
// Lang = the UI FRAME's language — the `L` localization tables each module (report,
|