@webability/cli 1.1.1 → 1.1.3
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/dist/cli.js +356 -935
- package/package.json +3 -3
- package/visual-dogfood.mjs +55 -0
package/dist/cli.js
CHANGED
|
@@ -1,150 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var
|
|
3
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
-
}) : x)(function(x) {
|
|
5
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
// src/cli.ts
|
|
10
|
-
import { Command } from "commander";
|
|
11
|
-
import chalk3 from "chalk";
|
|
12
|
-
import ora from "ora";
|
|
13
|
-
|
|
14
|
-
// src/api.ts
|
|
15
|
-
var API_BASE = "https://api.webability.io";
|
|
16
|
-
async function apiRequest(path, options = {}, apiKey) {
|
|
17
|
-
const res = await fetch(`${API_BASE}${path}`, {
|
|
18
|
-
...options,
|
|
19
|
-
headers: {
|
|
20
|
-
"Content-Type": "application/json",
|
|
21
|
-
"Authorization": `Bearer ${apiKey}`,
|
|
22
|
-
"Origin": "https://app.webability.io",
|
|
23
|
-
...options.headers
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
if (!res.ok) {
|
|
27
|
-
const body = await res.text().catch(() => "");
|
|
28
|
-
throw new Error(`API ${res.status}: ${body.slice(0, 200)}`);
|
|
29
|
-
}
|
|
30
|
-
return res.json();
|
|
31
|
-
}
|
|
32
|
-
async function graphql(query, variables, apiKey) {
|
|
33
|
-
const data = await apiRequest("/graphql", {
|
|
34
|
-
method: "POST",
|
|
35
|
-
body: JSON.stringify({ query, variables })
|
|
36
|
-
}, apiKey);
|
|
37
|
-
if (data.errors?.length) {
|
|
38
|
-
throw new Error(data.errors[0].message);
|
|
39
|
-
}
|
|
40
|
-
return data.data;
|
|
41
|
-
}
|
|
42
|
-
async function scan(url, apiKey, onStatus) {
|
|
43
|
-
onStatus?.("Starting scan...");
|
|
44
|
-
const { startAccessibilityReportJob } = await graphql(
|
|
45
|
-
`query($url: String!) { startAccessibilityReportJob(url: $url, use_cache: false) { jobId } }`,
|
|
46
|
-
{ url },
|
|
47
|
-
apiKey
|
|
48
|
-
);
|
|
49
|
-
const jobId = startAccessibilityReportJob.jobId;
|
|
50
|
-
onStatus?.(`Job ${jobId.slice(0, 8)}... created`);
|
|
51
|
-
for (let i = 0; i < 60; i++) {
|
|
52
|
-
await new Promise((r) => setTimeout(r, 3e3));
|
|
53
|
-
const { getAccessibilityReportByJobId: job } = await graphql(
|
|
54
|
-
`query($jobId: String!) { getAccessibilityReportByJobId(jobId: $jobId) { status error result { savedReport { key } } } }`,
|
|
55
|
-
{ jobId },
|
|
56
|
-
apiKey
|
|
57
|
-
);
|
|
58
|
-
if (job.status === "done") {
|
|
59
|
-
return { key: job.result.savedReport.key };
|
|
60
|
-
}
|
|
61
|
-
if (job.status === "error") {
|
|
62
|
-
throw new Error(job.error || "Scan failed");
|
|
63
|
-
}
|
|
64
|
-
onStatus?.(`Scanning... (${(i + 1) * 3}s)`);
|
|
65
|
-
}
|
|
66
|
-
throw new Error("Scan timed out after 3 minutes");
|
|
67
|
-
}
|
|
68
|
-
async function getReport(r2Key, apiKey) {
|
|
69
|
-
const { fetchReportByR2Key } = await graphql(
|
|
70
|
-
`query($key: String!) { fetchReportByR2Key(r2_key: $key) { score totalElements siteImg ByFunctions { name count issues { code impact description element } } axe { violations { id impact description nodes { html target } } } } }`,
|
|
71
|
-
{ key: r2Key },
|
|
72
|
-
apiKey
|
|
73
|
-
);
|
|
74
|
-
return fetchReportByR2Key;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// src/config.ts
|
|
78
|
-
import Conf from "conf";
|
|
79
|
-
var config = new Conf({
|
|
80
|
-
projectName: "webability",
|
|
81
|
-
schema: {
|
|
82
|
-
apiKey: { type: "string", default: "" },
|
|
83
|
-
accessCode: { type: "string", default: "" },
|
|
84
|
-
defaultFormat: { type: "string", default: "table", enum: ["table", "json", "csv"] }
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
function getApiKey() {
|
|
88
|
-
return process.env.WEBABILITY_API_KEY || config.get("apiKey") || "";
|
|
89
|
-
}
|
|
90
|
-
function setApiKey(key) {
|
|
91
|
-
config.set("apiKey", key);
|
|
92
|
-
}
|
|
93
|
-
function getAccessCode() {
|
|
94
|
-
return config.get("accessCode") || "";
|
|
95
|
-
}
|
|
96
|
-
function setAccessCode(code) {
|
|
97
|
-
config.set("accessCode", code);
|
|
98
|
-
}
|
|
99
|
-
function isActivated() {
|
|
100
|
-
return !!getAccessCode();
|
|
101
|
-
}
|
|
102
|
-
function clearConfig() {
|
|
103
|
-
config.clear();
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// src/local-scan.ts
|
|
107
|
-
import { scan as scan2, detectFramework } from "@webability/core";
|
|
108
|
-
var WCAG_TAG_MAP = {
|
|
109
|
-
"A": ["wcag2a"],
|
|
110
|
-
"AA": ["wcag2a", "wcag2aa", "wcag21aa", "wcag22aa"],
|
|
111
|
-
"AAA": ["wcag2a", "wcag2aa", "wcag2aaa", "wcag21aa", "wcag22aa"]
|
|
112
|
-
};
|
|
113
|
-
var IMPACT_COLORS = {
|
|
114
|
-
critical: "#ef4444",
|
|
115
|
-
serious: "#f97316",
|
|
116
|
-
moderate: "#eab308",
|
|
117
|
-
minor: "#6b7280"
|
|
118
|
-
};
|
|
119
|
-
var IMPACT_ICONS = {
|
|
120
|
-
critical: "\u2717",
|
|
121
|
-
serious: "\u25B2",
|
|
122
|
-
moderate: "\u25CF",
|
|
123
|
-
minor: "\u25CB"
|
|
124
|
-
};
|
|
125
|
-
function buildHighlightScript(issues, apiKey, framework) {
|
|
126
|
-
const serialized = JSON.stringify(
|
|
127
|
-
issues.map((i, idx) => ({
|
|
128
|
-
id: idx,
|
|
129
|
-
selector: i.selector,
|
|
130
|
-
impact: i.impact,
|
|
131
|
-
wcag: i.wcag,
|
|
132
|
-
message: i.message.slice(0, 200),
|
|
133
|
-
fixDesc: i.fix?.suggestedValue ? `Set ${i.fix.attribute}="${i.fix.suggestedValue}"` : "",
|
|
134
|
-
fixAttr: i.fix?.attribute || "",
|
|
135
|
-
fixVal: i.fix?.suggestedValue || "",
|
|
136
|
-
fixCur: i.fix?.currentValue || "",
|
|
137
|
-
canApply: !!(i.fix?.suggestedValue && !i.fix?.needsManualReview)
|
|
138
|
-
}))
|
|
139
|
-
);
|
|
140
|
-
return `
|
|
2
|
+
var ae=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,n)=>(typeof require<"u"?require:t)[n]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});import{readFileSync as Fe}from"fs";import{fileURLToPath as Se}from"url";import{dirname as Ee,join as $e}from"path";import{Command as Ie}from"commander";import a from"chalk";import I from"ora";var ne="https://api.webability.io";async function re(e,t={},n){let o=await fetch(`${ne}${e}`,{...t,headers:{"Content-Type":"application/json",Authorization:`Bearer ${n}`,Origin:"https://app.webability.io",...t.headers}});if(!o.ok){let r=await o.text().catch(()=>"");throw new Error(`API ${o.status}: ${r.slice(0,200)}`)}return o.json()}async function D(e,t,n){let o=await re("/graphql",{method:"POST",body:JSON.stringify({query:e,variables:t})},n);if(o.errors?.length)throw new Error(o.errors[0].message);return o.data}async function K(e,t,n){n?.("Starting scan...");let{startAccessibilityReportJob:o}=await D("query($url: String!) { startAccessibilityReportJob(url: $url, use_cache: false) { jobId } }",{url:e},t),r=o.jobId;n?.(`Job ${r.slice(0,8)}... created`);for(let i=0;i<60;i++){await new Promise(l=>setTimeout(l,3e3));let{getAccessibilityReportByJobId:s}=await D("query($jobId: String!) { getAccessibilityReportByJobId(jobId: $jobId) { status error result { savedReport { key } } } }",{jobId:r},t);if(s.status==="done")return{key:s.result.savedReport.key};if(s.status==="error")throw new Error(s.error||"Scan failed");n?.(`Scanning... (${(i+1)*3}s)`)}throw new Error("Scan timed out after 3 minutes")}async function U(e,t){let{fetchReportByR2Key:n}=await D("query($key: String!) { fetchReportByR2Key(r2_key: $key) { score totalElements siteImg ByFunctions { name count issues { code impact description element } } axe { violations { id impact description nodes { html target } } } } }",{key:e},t);return n}import le from"conf";var S=new le({projectName:"webability",schema:{apiKey:{type:"string",default:""},accessCode:{type:"string",default:""},defaultFormat:{type:"string",default:"table",enum:["table","json","csv"]}}});function E(){return process.env.WEBABILITY_API_KEY||S.get("apiKey")||""}function T(e){S.set("apiKey",e)}function ce(){return S.get("accessCode")||""}function O(e){S.set("accessCode",e)}function G(){return!!ce()}function J(){S.clear()}import{scan as de,detectFramework as pe,generateDevSuggestion as ue}from"@webability/core";var ge={weak_link_name:"Screen-reader users navigate by pulling up a links list (VoiceOver Rotor / NVDA Insert+F7). They hear: 'Learn more, Learn more, Learn more' \u2014 every link sounds identical. They can't tell where each goes without context.",weak_button_name:"Same problem as weak links \u2014 assistive tech announces 'Submit' or 'Click here' with no context, so users can't tell what each button does.",missing_alt:`Screen readers say 'image' (or skip it entirely) when there's no alt text. A photo of your CEO becomes 'image, image' to a blind user. Decorative images need alt="" so they're skipped intentionally.`,decorative_icon:"Decorative icons next to labelled text get announced twice: 'home icon, Home'. Wastes the user's time and creates audio clutter.",missing_label:"Form inputs without labels are invisible to screen readers \u2014 users hear 'edit text' with no idea what to type. They abandon the form.",contrast_insufficient:"Low-contrast text excludes users with low vision (1 in 12 men have some color-blindness). It's also unreadable on glare-prone screens, in sunlight, and for older eyes.",non_text_contrast_insufficient:"If a button's border or icon doesn't have 3:1 contrast against its background, low-vision users can't see the boundary or recognize the icon at all.",target_too_small:"Touch targets under 24\xD724px are hard to hit accurately for users with motor impairments (Parkinson's, MS, arthritis). Mis-taps trigger wrong actions and frustrate keyboard-only users too.",missing_skip_link:"Keyboard and screen-reader users have to Tab through your entire header (logo, nav, CTAs) on every single page. A skip link lets them jump straight to main content \u2014 a 30-second fix that saves disabled users minutes of navigation per visit.",motion_without_reduced_motion:"Vestibular disorders (~35% of adults over 40) can be triggered by parallax, auto-rotation, and rapid animations \u2014 causing nausea, dizziness, and migraines. Respecting prefers-reduced-motion is non-optional.",heading_skip:"Screen-reader users navigate by heading hierarchy \u2014 they jump h1 \u2192 h2 \u2192 h3 to scan structure. Skipping levels (h2 \u2192 h4) makes the page feel disorganized and content gets missed.",multiple_h1:"Multiple h1 elements confuse the page structure model that screen readers and SEO tools use. Each page should have exactly one h1 \u2014 the page title.",redundant_role:`Adding role="navigation" to a <nav> element is redundant \u2014 screen readers already know it's navigation. Redundant ARIA can cause double-announcements.`,duplicate_id:"Duplicate IDs break aria-labelledby, aria-describedby, and label[for] references \u2014 screen readers may read the wrong content or none at all.",missing_required_indicator:"If users don't know a field is required until they submit, they miss the requirement (especially screen-reader users who skip placeholder text). They get an error after the fact and have to re-enter data.",new_window_link:"Opening links in a new tab without warning disorients screen-reader users \u2014 they think the back button is broken. WCAG requires you announce 'opens in new tab' via aria-label or visible text.",color_only_meaning:"Using only color to indicate state (red = error, green = success) excludes the ~8% of men who are color-blind. Always pair color with an icon, label, or pattern.",insufficient_navigation:"Inconsistent nav across pages forces users to relearn the structure on every page. Especially hard on cognitive disabilities and screen-reader users who memorize tab order.",focus_not_visible:"Without a visible focus ring, keyboard users have no idea which element is selected. Tabbing through your site becomes guesswork \u2014 they hit Enter and trigger the wrong action.",aria_hidden_focusable:`An element with aria-hidden="true" but tabindex="0" creates a 'ghost' focus stop \u2014 keyboard users can focus it but screen readers don't announce it. They press Enter on something invisible to them.`,svg_missing_name:"SVG icons that convey meaning (warning triangle, X close button) need an accessible name. Without it, blind users hear 'graphic' or nothing at all."};function me(e){return ge[e.type]||""}var H={A:["wcag2a"],AA:["wcag2a","wcag2aa","wcag21aa","wcag22aa"],AAA:["wcag2a","wcag2aa","wcag2aaa","wcag21aa","wcag22aa"]},fe={critical:"#ef4444",serious:"#f97316",moderate:"#eab308",minor:"#6b7280"},he={critical:"\u2717",serious:"\u25B2",moderate:"\u25CF",minor:"\u25CB"};function be(e,t,n){let o=n||"plain-css";return`
|
|
141
3
|
(function() {
|
|
142
|
-
const issues = ${
|
|
143
|
-
const colors = ${JSON.stringify(
|
|
144
|
-
const icons = ${JSON.stringify(
|
|
145
|
-
const API_KEY = ${JSON.stringify(
|
|
4
|
+
const issues = ${JSON.stringify(e.map((i,s)=>{let l=(()=>{try{return ue(i,o)}catch{return null}})();return{id:s,selector:i.selector,impact:i.impact,wcag:i.wcag,type:i.type,message:i.message.slice(0,200),problem:l?.problem||"",recommendedFix:l?.fix||"",wcagLink:l?.wcagLink||`https://www.w3.org/WAI/WCAG21/Understanding/${i.wcag.replace(/\./g,"")}`,whyMatters:me(i),fixDesc:i.fix?.suggestedValue?`Set ${i.fix.attribute}="${i.fix.suggestedValue}"`:"",fixAttr:i.fix?.attribute||"",fixVal:i.fix?.suggestedValue||"",fixCur:i.fix?.currentValue||"",canApply:!!(i.fix?.suggestedValue&&!i.fix?.needsManualReview)}}))};
|
|
5
|
+
const colors = ${JSON.stringify(fe)};
|
|
6
|
+
const icons = ${JSON.stringify(he)};
|
|
7
|
+
const API_KEY = ${JSON.stringify(t||"")};
|
|
146
8
|
const API_URL = 'https://api.webability.io';
|
|
147
|
-
const FRAMEWORK = ${JSON.stringify(
|
|
9
|
+
const FRAMEWORK = ${JSON.stringify(n||"plain-css")};
|
|
148
10
|
const activeFilters = { critical: true, serious: true, moderate: true, minor: true };
|
|
149
11
|
const overlays = [];
|
|
150
12
|
|
|
@@ -165,74 +27,127 @@ function buildHighlightScript(issues, apiKey, framework) {
|
|
|
165
27
|
|
|
166
28
|
const style = document.createElement('style');
|
|
167
29
|
style.textContent = [
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
30
|
+
// Brand tokens (matches .impeccable.md)
|
|
31
|
+
":root{--ab-primary:#0052CC;--ab-primary-dark:#0033ED;--ab-accent:#47FFF7;--ab-navy:#1B3DB4;--ab-bg:#FFFFFF;--ab-surface:#F9FAFB;--ab-text:#111827;--ab-text-muted:#4B5563;--ab-border:#E5E7EB;--ab-success:#15803D;--ab-warning:#CA8A04;--ab-danger:#DC2626;--ab-bg-tint:#F4F8FF}",
|
|
32
|
+
"@keyframes abilyo-reveal{from{opacity:0;transform:scale(.97)}to{opacity:1;transform:scale(1)}}",
|
|
33
|
+
|
|
34
|
+
// Overlays on the page
|
|
35
|
+
".abilyo-overlay{position:absolute;pointer-events:auto;z-index:2147483640;outline:2px solid red;outline-offset:2px;border-radius:3px;box-sizing:border-box;cursor:pointer;transition:opacity .2s,outline-color .2s;animation:abilyo-reveal .25s ease-out backwards}",
|
|
36
|
+
".abilyo-overlay:hover{outline-width:3px;outline-offset:1px}",
|
|
37
|
+
".abilyo-overlay.abilyo-active{outline:3px solid #0052CC!important;outline-offset:2px}",
|
|
38
|
+
".abilyo-overlay.abilyo-hidden{opacity:0;pointer-events:none}",
|
|
39
|
+
".abilyo-overlay.abilyo-dimmed{opacity:.12;filter:blur(1px)}",
|
|
40
|
+
".abilyo-label{position:absolute;top:-22px;left:-2px;font:600 11px/14px Inter,system-ui,sans-serif;color:#fff;padding:2px 6px;border-radius:3px 3px 0 0;white-space:nowrap;pointer-events:none;z-index:2147483641}",
|
|
41
|
+
|
|
42
|
+
// Panel container
|
|
43
|
+
"#abilyo-panel{position:fixed;top:12px;right:12px;z-index:2147483647;width:400px;max-width:calc(100vw - 24px);max-height:calc(100vh - 24px);background:#fff;border-radius:14px;box-shadow:0 12px 40px rgba(0,82,204,.12),0 0 0 1px rgba(0,82,204,.08);font:13px Inter,system-ui,-apple-system,sans-serif;color:#111827;display:flex;flex-direction:column;overflow:hidden}",
|
|
44
|
+
"#abilyo-panel *{box-sizing:border-box;min-width:0}",
|
|
45
|
+
|
|
46
|
+
// Header with hero score
|
|
47
|
+
".ap-header{padding:18px 20px 14px;border-bottom:1px solid #E5E7EB;flex-shrink:0}",
|
|
48
|
+
".ap-header-top{display:flex;align-items:center;gap:8px;margin-bottom:10px}",
|
|
49
|
+
".ap-brand{display:flex;align-items:center;gap:8px;flex:1}",
|
|
50
|
+
".ap-logo{width:18px;height:18px;background:#0052CC;border-radius:4px;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:800;font-size:11px;font-family:Inter,system-ui}",
|
|
51
|
+
".ap-title{font-weight:600;font-size:13px;color:#4B5563;letter-spacing:-.01em}",
|
|
52
|
+
".ap-close{width:26px;height:26px;border:none;background:transparent;border-radius:6px;cursor:pointer;font-size:14px;color:#6B7280;display:flex;align-items:center;justify-content:center}",
|
|
53
|
+
".ap-close:hover{background:#F3F4F6;color:#111827}",
|
|
54
|
+
".ap-score-row{display:flex;align-items:baseline;gap:10px}",
|
|
55
|
+
".ap-score{font:800 32px/1 Inter,system-ui;letter-spacing:-.03em;font-feature-settings:'tnum'}",
|
|
56
|
+
".ap-score-label{display:flex;flex-direction:column;gap:1px}",
|
|
57
|
+
".ap-score-summary{font-size:13px;font-weight:600;color:#111827}",
|
|
58
|
+
".ap-score-meta{font-size:11px;color:#6B7280;font-weight:500}",
|
|
59
|
+
|
|
60
|
+
// Toolbar \u2014 toggle row with switch-style controls (NOT primary buttons)
|
|
61
|
+
".ap-toolbar{display:flex;gap:14px;padding:10px 16px;border-bottom:1px solid #E5E7EB;flex-shrink:0;background:#FAFBFC;align-items:center}",
|
|
62
|
+
".ap-toggle{display:flex;align-items:center;gap:8px;cursor:pointer;font:500 12px Inter,system-ui;color:#4B5563;user-select:none}",
|
|
63
|
+
".ap-toggle:hover{color:#111827}",
|
|
64
|
+
".ap-toggle-track{position:relative;width:30px;height:18px;background:#D1D5DB;border-radius:9px;transition:background .15s;flex-shrink:0}",
|
|
65
|
+
".ap-toggle-thumb{position:absolute;top:2px;left:2px;width:14px;height:14px;background:#fff;border-radius:50%;box-shadow:0 1px 2px rgba(0,0,0,.15);transition:transform .15s}",
|
|
66
|
+
".ap-toggle.active .ap-toggle-track{background:#0052CC}",
|
|
67
|
+
".ap-toggle.active .ap-toggle-thumb{transform:translateX(12px)}",
|
|
68
|
+
".ap-toggle-meta{font-size:11px;color:#6B7280;font-weight:500;font-feature-settings:'tnum'}",
|
|
69
|
+
|
|
70
|
+
// Filter pills \u2014 distinct on/off state
|
|
71
|
+
".ap-filters{display:flex;gap:6px;padding:10px 16px;border-bottom:1px solid #E5E7EB;flex-shrink:0;flex-wrap:wrap}",
|
|
72
|
+
".ap-fbtn{display:flex;align-items:center;gap:5px;padding:5px 10px;border-radius:14px;border:1.5px solid transparent;cursor:pointer;font:600 11px Inter,system-ui;transition:all .15s;letter-spacing:.01em}",
|
|
73
|
+
// ON state: filled with severity color
|
|
74
|
+
".ap-fbtn.active{color:#fff}",
|
|
75
|
+
// OFF state: clear "off" \u2014 gray text, gray border, no fill
|
|
76
|
+
".ap-fbtn:not(.active){background:#fff;color:#9CA3AF;border-color:#E5E7EB;text-decoration:line-through;text-decoration-thickness:1px}",
|
|
77
|
+
".ap-fbtn:not(.active):hover{color:#4B5563;border-color:#9CA3AF}",
|
|
78
|
+
".ap-fbtn-num{font-weight:800}",
|
|
79
|
+
".ap-fbtn-label{font-weight:500;text-transform:capitalize}",
|
|
80
|
+
|
|
81
|
+
// Issue list
|
|
82
|
+
".ap-list{flex:1;overflow-y:auto;scrollbar-width:thin}",
|
|
83
|
+
".ap-list::-webkit-scrollbar{width:6px}",
|
|
84
|
+
".ap-list::-webkit-scrollbar-thumb{background:#E5E7EB;border-radius:3px}",
|
|
85
|
+
".ap-row{display:flex;align-items:flex-start;gap:10px;padding:11px 16px;cursor:pointer;border-bottom:1px solid #F3F4F6;transition:background .1s}",
|
|
86
|
+
".ap-row:hover{background:#F4F8FF}",
|
|
87
|
+
".ap-row.active{background:#F4F8FF;box-shadow:inset 3px 0 0 #0052CC}",
|
|
88
|
+
".ap-row.active.ap-sticky{position:sticky;top:0;background:#F4F8FF;z-index:2;border-bottom:2px solid #0052CC}",
|
|
89
|
+
".ap-icon{width:24px;height:24px;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:800;color:#fff;flex-shrink:0;margin-top:1px;font-family:Inter,system-ui}",
|
|
90
|
+
".ap-content{flex:1;min-width:0}",
|
|
91
|
+
".ap-wcag{font-size:11px;font-weight:700;color:#0052CC;background:#F4F8FF;padding:2px 7px;border-radius:4px;letter-spacing:.01em;font-feature-settings:'tnum'}",
|
|
92
|
+
".ap-msg{font-size:13px;color:#111827;margin-top:4px;line-height:1.35;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}",
|
|
93
|
+
".ap-fix{font-size:12px;color:#15803D;margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:500}",
|
|
94
|
+
// Count badge for grouped rows (e.g. "weak link name \xD7 5")
|
|
95
|
+
".ap-count-badge{display:inline-flex;align-items:center;justify-content:center;min-width:22px;height:18px;padding:0 6px;border-radius:9px;background:#0052CC;color:#fff;font-size:10px;font-weight:700;letter-spacing:.02em;margin-left:6px;font-feature-settings:'tnum'}",
|
|
96
|
+
// Header for issue group sections in the list
|
|
97
|
+
".ap-group-header{padding:14px 16px 6px;font-size:10px;font-weight:700;color:#6B7280;text-transform:uppercase;letter-spacing:.08em;background:#FAFBFC;border-bottom:1px solid #F3F4F6;border-top:1px solid #F3F4F6;position:sticky;top:0;z-index:1}",
|
|
98
|
+
".ap-group-header:first-child{border-top:none}",
|
|
99
|
+
|
|
100
|
+
// Detail pane \u2014 visually distinct from list above
|
|
101
|
+
"#ap-detail{display:none;flex-shrink:0;border-top:2px solid #0052CC;padding:16px 18px;background:linear-gradient(180deg,#F4F8FF 0%,#FAFBFC 80px);max-height:340px;overflow-y:auto;box-shadow:inset 0 4px 8px -4px rgba(0,82,204,.08)}",
|
|
102
|
+
".apd-head{display:flex;align-items:center;gap:8px;margin-bottom:10px}",
|
|
103
|
+
".apd-wcag{font-weight:800;color:#0052CC;font-size:15px;letter-spacing:-.01em}",
|
|
104
|
+
".apd-badge{font-size:10px;font-weight:700;padding:3px 8px;border-radius:4px;color:#fff;text-transform:uppercase;letter-spacing:.06em}",
|
|
105
|
+
".apd-msg{font-size:13px;color:#111827;line-height:1.45;margin-bottom:10px;font-weight:600}",
|
|
106
|
+
// Educational "Why this matters" \u2014 the explanation block
|
|
107
|
+
".apd-section{margin-top:10px}",
|
|
108
|
+
".apd-section-label{font-size:10px;font-weight:700;color:#6B7280;text-transform:uppercase;letter-spacing:.07em;margin-bottom:4px}",
|
|
109
|
+
".apd-why{font-size:12px;line-height:1.5;color:#1F2937;background:#FAFBFC;padding:9px 12px;border-radius:6px;border-left:3px solid #0052CC;margin-top:4px}",
|
|
110
|
+
".apd-recommend{font-size:12px;line-height:1.5;color:#1F2937;margin-top:4px}",
|
|
111
|
+
".apd-recommend code{font-family:'SF Mono',Menlo,Consolas,monospace;font-size:11px;background:#F3F4F6;padding:1px 5px;border-radius:3px;color:#0033ED}",
|
|
112
|
+
".apd-learnmore{font-size:11px;color:#0052CC;text-decoration:none;display:inline-flex;align-items:center;gap:4px;margin-top:8px;font-weight:500;padding:4px 8px;border-radius:4px;background:#F4F8FF;width:fit-content}",
|
|
113
|
+
".apd-learnmore:hover{background:#DBEAFE;text-decoration:none}",
|
|
114
|
+
".apd-learnmore-arrow{font-size:11px;font-family:Inter,system-ui;line-height:1;display:inline-block;transform:translateY(-1px)}",
|
|
115
|
+
".apd-fix{font-size:12px;color:#15803D;background:#F0FDF4;padding:8px 12px;border-radius:6px;margin-top:6px;border:1px solid #BBF7D0}",
|
|
116
|
+
".apd-sel{font-size:11px;color:#6B7280;font-family:'SF Mono',Menlo,Consolas,monospace;margin-top:8px;word-break:break-all;background:#F3F4F6;padding:6px 8px;border-radius:4px}",
|
|
117
|
+
".apd-actions{display:flex;gap:8px;margin-top:12px}",
|
|
118
|
+
".apd-btn{padding:7px 14px;border-radius:7px;border:none;cursor:pointer;font:600 12px Inter,system-ui;transition:all .15s;letter-spacing:-.01em}",
|
|
119
|
+
".apd-preview{background:#F4F8FF;color:#0052CC;border:1px solid #DBEAFE}",
|
|
120
|
+
".apd-preview:hover{background:#DBEAFE;border-color:#0052CC}",
|
|
121
|
+
".apd-preview.active{background:#0052CC;color:#fff;border-color:#0052CC}",
|
|
122
|
+
".apd-apply{background:#15803D;color:#fff}",
|
|
123
|
+
".apd-apply:hover{background:#166534}",
|
|
124
|
+
".apd-apply.applied{background:#6B7280;cursor:default}",
|
|
125
|
+
|
|
126
|
+
// Before/after \u2014 branded subtle tints
|
|
127
|
+
".apd-before-after{display:flex;gap:10px;margin-top:10px;font-size:12px}",
|
|
128
|
+
".apd-ba-col{flex:1;padding:9px 10px;border-radius:6px}",
|
|
129
|
+
".apd-ba-before{background:#FEF3F2;border:1px solid #FECDD3}",
|
|
130
|
+
".apd-ba-after{background:#F4FFF8;border:1px solid #BBF7D0}",
|
|
131
|
+
".apd-ba-label{font-weight:700;font-size:9px;text-transform:uppercase;letter-spacing:.08em;margin-bottom:4px;color:#6B7280}",
|
|
132
|
+
".apd-ba-value{font-family:'SF Mono',Menlo,Consolas,monospace;font-size:11px;word-break:break-all;color:#111827}",
|
|
133
|
+
|
|
134
|
+
// AI button \u2014 branded, no purple gradient, no sparkle emoji
|
|
135
|
+
".apd-ai{background:#0052CC;color:#fff;position:relative;overflow:hidden;border:none}",
|
|
136
|
+
".apd-ai:hover{background:#0033ED;box-shadow:0 0 0 3px rgba(0,82,204,.15)}",
|
|
137
|
+
".apd-ai:disabled{opacity:.7;cursor:wait}",
|
|
138
|
+
".apd-ai::before{content:'AI';display:inline-block;font-size:9px;font-weight:800;letter-spacing:.05em;background:#47FFF7;color:#0033ED;padding:2px 5px;border-radius:3px;margin-right:7px;vertical-align:middle}",
|
|
139
|
+
"@keyframes apd-shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}",
|
|
140
|
+
".apd-ai-loading{background:linear-gradient(90deg,#0052CC 25%,#3B7FF7 50%,#0052CC 75%);background-size:200% 100%;animation:apd-shimmer 1.5s infinite}",
|
|
141
|
+
|
|
142
|
+
// AI result \u2014 brand-blue tint, not lavender
|
|
143
|
+
".apd-ai-result{margin-top:10px;padding:12px;background:#F4F8FF;border:1px solid #DBEAFE;border-radius:8px;border-left:3px solid #0052CC}",
|
|
144
|
+
".apd-ai-explain{font-size:13px;color:#111827;margin-bottom:8px;line-height:1.45}",
|
|
145
|
+
".apd-ai-code{font-size:12px;font-family:'SF Mono',Menlo,Consolas,monospace;color:#1B3DB4;background:#fff;padding:7px 10px;border-radius:5px;word-break:break-all;border:1px solid #DBEAFE}",
|
|
146
|
+
|
|
147
|
+
// Minimized pill \u2014 branded
|
|
148
|
+
"#abilyo-pill{display:none;position:fixed;top:12px;right:12px;z-index:2147483647;background:#0052CC;color:#fff;border-radius:10px;padding:9px 14px;cursor:pointer;font:700 13px/1 Inter,system-ui;box-shadow:0 4px 16px rgba(0,82,204,.35);transition:transform .15s}",
|
|
149
|
+
"#abilyo-pill:hover{transform:scale(1.05)}",
|
|
150
|
+
"#abilyo-pill .ap-pill-num{font-weight:800;font-feature-settings:'tnum';margin-right:4px}",
|
|
236
151
|
].join('\\n');
|
|
237
152
|
document.head.appendChild(style);
|
|
238
153
|
|
|
@@ -306,31 +221,109 @@ function buildHighlightScript(issues, apiKey, framework) {
|
|
|
306
221
|
const filtersEl = h('div', { className: 'ap-filters' });
|
|
307
222
|
const toolbarEl = h('div', { className: 'ap-toolbar' });
|
|
308
223
|
|
|
309
|
-
//
|
|
224
|
+
// Score \u2014 Lighthouse-style: capped per-severity penalties so a site with
|
|
225
|
+
// 159 moderates doesn't crash to 0% (which is meaningless and demoralizing).
|
|
226
|
+
// Each severity contributes a bounded amount; floor at 5% so users always
|
|
227
|
+
// have somewhere to go.
|
|
228
|
+
function computeScore(c) {
|
|
229
|
+
if (c.critical + c.serious + c.moderate + c.minor === 0) return 100;
|
|
230
|
+
const criticalPenalty = Math.min(c.critical * 12, 35); // up to -35
|
|
231
|
+
const seriousPenalty = Math.min(c.serious * 4, 25); // up to -25
|
|
232
|
+
const moderatePenalty = Math.min(c.moderate * 1, 20); // up to -20
|
|
233
|
+
const minorPenalty = Math.min(c.minor * 0.5, 10); // up to -10
|
|
234
|
+
return Math.max(5, Math.round(100 - criticalPenalty - seriousPenalty - moderatePenalty - minorPenalty));
|
|
235
|
+
}
|
|
236
|
+
const score = computeScore(counts);
|
|
237
|
+
const scoreColor = score >= 90 ? '#15803D' : score >= 70 ? '#CA8A04' : score >= 40 ? '#EA580C' : '#DC2626';
|
|
238
|
+
const scoreSummary = score >= 90 ? 'Looking good' : score >= 70 ? 'Mostly good' : score >= 40 ? 'Needs work' : 'Failing';
|
|
239
|
+
|
|
240
|
+
// Toolbar \u2014 switch-style toggles (clearly stateful, not action buttons).
|
|
241
|
+
// role=switch needs tabindex + Space/Enter to be reachable & operable
|
|
242
|
+
// without a mouse \u2014 same a11y bar we hold customer code to.
|
|
243
|
+
function activateOnKeydown(handler) {
|
|
244
|
+
return (ev) => {
|
|
245
|
+
if (ev.key === ' ' || ev.key === 'Enter' || ev.code === 'Space') {
|
|
246
|
+
ev.preventDefault();
|
|
247
|
+
handler();
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
310
252
|
let overlaysVisible = true;
|
|
311
|
-
const
|
|
253
|
+
const overlayMeta = h('span', { className: 'ap-toggle-meta' }, 'on');
|
|
254
|
+
const toggleOverlays = () => {
|
|
312
255
|
overlaysVisible = !overlaysVisible;
|
|
313
|
-
|
|
256
|
+
overlayToggle.classList.toggle('active', overlaysVisible);
|
|
257
|
+
overlayToggle.setAttribute('aria-checked', String(overlaysVisible));
|
|
258
|
+
overlayMeta.textContent = overlaysVisible ? 'on' : 'off';
|
|
314
259
|
document.querySelectorAll('.abilyo-overlay').forEach(o => { (o).style.display = overlaysVisible ? '' : 'none'; });
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
260
|
+
};
|
|
261
|
+
const overlayToggle = h('div', {
|
|
262
|
+
className: 'ap-toggle active',
|
|
263
|
+
role: 'switch',
|
|
264
|
+
tabindex: '0',
|
|
265
|
+
'aria-checked': 'true',
|
|
266
|
+
'aria-label': 'Highlight issues on page',
|
|
267
|
+
onClick: toggleOverlays,
|
|
268
|
+
onKeydown: activateOnKeydown(toggleOverlays),
|
|
269
|
+
}, [
|
|
270
|
+
h('span', { className: 'ap-toggle-track' }, [h('span', { className: 'ap-toggle-thumb' })]),
|
|
271
|
+
document.createTextNode('Highlight on page'),
|
|
272
|
+
overlayMeta,
|
|
273
|
+
]);
|
|
274
|
+
toolbarEl.appendChild(overlayToggle);
|
|
275
|
+
|
|
276
|
+
// Labels toggle \u2014 hide the small WCAG number badges above each overlay if too noisy
|
|
277
|
+
let labelsVisible = true;
|
|
278
|
+
const labelsMeta = h('span', { className: 'ap-toggle-meta' }, 'on');
|
|
279
|
+
const toggleLabels = () => {
|
|
280
|
+
labelsVisible = !labelsVisible;
|
|
281
|
+
labelsToggle.classList.toggle('active', labelsVisible);
|
|
282
|
+
labelsToggle.setAttribute('aria-checked', String(labelsVisible));
|
|
283
|
+
labelsMeta.textContent = labelsVisible ? 'on' : 'off';
|
|
284
|
+
document.querySelectorAll('.abilyo-label').forEach(l => { (l).style.display = labelsVisible ? '' : 'none'; });
|
|
285
|
+
};
|
|
286
|
+
const labelsToggle = h('div', {
|
|
287
|
+
className: 'ap-toggle active',
|
|
288
|
+
role: 'switch',
|
|
289
|
+
tabindex: '0',
|
|
290
|
+
'aria-checked': 'true',
|
|
291
|
+
'aria-label': 'Show severity labels',
|
|
292
|
+
onClick: toggleLabels,
|
|
293
|
+
onKeydown: activateOnKeydown(toggleLabels),
|
|
294
|
+
}, [
|
|
295
|
+
h('span', { className: 'ap-toggle-track' }, [h('span', { className: 'ap-toggle-thumb' })]),
|
|
296
|
+
document.createTextNode('Labels'),
|
|
297
|
+
labelsMeta,
|
|
298
|
+
]);
|
|
299
|
+
toolbarEl.appendChild(labelsToggle);
|
|
324
300
|
|
|
325
301
|
const minBtn = h('button', { className: 'ap-close', title: 'Minimize', onClick: () => { panel.style.display = 'none'; pill.style.display = 'block'; } }, '\\u2014');
|
|
326
302
|
|
|
327
|
-
|
|
328
|
-
|
|
303
|
+
// Branded minimized pill
|
|
304
|
+
const pillNum = h('span', { className: 'ap-pill-num' }, String(issues.length));
|
|
305
|
+
const pill = h('div', { id: 'abilyo-pill', onClick: () => { panel.style.display = 'flex'; pill.style.display = 'none'; } }, [
|
|
306
|
+
pillNum, document.createTextNode(' issues')
|
|
307
|
+
]);
|
|
308
|
+
|
|
309
|
+
// Header \u2014 score is hero, brand mark is small
|
|
310
|
+
const headerTop = h('div', { className: 'ap-header-top' }, [
|
|
311
|
+
h('div', { className: 'ap-brand' }, [
|
|
329
312
|
h('div', { className: 'ap-logo' }, 'A'),
|
|
330
313
|
h('div', { className: 'ap-title' }, 'Abilyo'),
|
|
331
|
-
h('div', { className: 'ap-count' }, String(issues.length) + ' issues'),
|
|
332
|
-
minBtn,
|
|
333
314
|
]),
|
|
315
|
+
minBtn,
|
|
316
|
+
]);
|
|
317
|
+
const scoreRow = h('div', { className: 'ap-score-row' }, [
|
|
318
|
+
h('div', { className: 'ap-score', style: { color: scoreColor } }, score + '%'),
|
|
319
|
+
h('div', { className: 'ap-score-label' }, [
|
|
320
|
+
h('div', { className: 'ap-score-summary' }, issues.length + ' issue' + (issues.length === 1 ? '' : 's') + ' \xB7 ' + scoreSummary),
|
|
321
|
+
h('div', { className: 'ap-score-meta' }, 'WCAG 2.1 AA'),
|
|
322
|
+
]),
|
|
323
|
+
]);
|
|
324
|
+
|
|
325
|
+
const panel = h('div', { id: 'abilyo-panel' }, [
|
|
326
|
+
h('div', { className: 'ap-header' }, [headerTop, scoreRow]),
|
|
334
327
|
toolbarEl,
|
|
335
328
|
filtersEl,
|
|
336
329
|
listEl,
|
|
@@ -339,21 +332,31 @@ function buildHighlightScript(issues, apiKey, framework) {
|
|
|
339
332
|
document.body.appendChild(panel);
|
|
340
333
|
document.body.appendChild(pill);
|
|
341
334
|
|
|
342
|
-
// --- Filter
|
|
335
|
+
// --- Filter pills \u2014 clear on/off state ---
|
|
343
336
|
for (const sev of ['critical','serious','moderate','minor']) {
|
|
344
337
|
if (counts[sev] === 0) continue;
|
|
345
|
-
const dot = h('span', { className: 'ap-dot', style: { background: '#fff' } });
|
|
346
338
|
const btn = h('button', {
|
|
347
339
|
className: 'ap-fbtn active',
|
|
348
|
-
|
|
349
|
-
title: sev,
|
|
340
|
+
title: 'Toggle ' + sev,
|
|
350
341
|
onClick: () => {
|
|
351
342
|
activeFilters[sev] = !activeFilters[sev];
|
|
352
343
|
btn.classList.toggle('active', activeFilters[sev]);
|
|
353
|
-
|
|
344
|
+
// ON: filled with severity color. OFF: gray strikethrough.
|
|
345
|
+
if (activeFilters[sev]) {
|
|
346
|
+
btn.style.background = colors[sev];
|
|
347
|
+
btn.style.borderColor = colors[sev];
|
|
348
|
+
} else {
|
|
349
|
+
btn.style.background = '';
|
|
350
|
+
btn.style.borderColor = '';
|
|
351
|
+
}
|
|
354
352
|
applyFilters();
|
|
355
353
|
},
|
|
356
|
-
}, [
|
|
354
|
+
}, [
|
|
355
|
+
h('span', { className: 'ap-fbtn-num' }, String(counts[sev])),
|
|
356
|
+
h('span', { className: 'ap-fbtn-label' }, sev),
|
|
357
|
+
]);
|
|
358
|
+
btn.style.background = colors[sev];
|
|
359
|
+
btn.style.borderColor = colors[sev];
|
|
357
360
|
filtersEl.appendChild(btn);
|
|
358
361
|
}
|
|
359
362
|
|
|
@@ -362,22 +365,75 @@ function buildHighlightScript(issues, apiKey, framework) {
|
|
|
362
365
|
|
|
363
366
|
function renderList() {
|
|
364
367
|
listEl.replaceChildren();
|
|
365
|
-
|
|
366
|
-
|
|
368
|
+
|
|
369
|
+
// Build groups: one row per (type + wcag + recommendedFix), with all matching ids
|
|
370
|
+
const visible = deduped.filter(i => activeFilters[i.impact]);
|
|
371
|
+
const groupMap = new Map();
|
|
372
|
+
for (const issue of visible) {
|
|
373
|
+
const key = (issue.type || issue.wcag) + '::' + issue.wcag + '::' + (issue.recommendedFix || issue.fixDesc || '');
|
|
374
|
+
const existing = groupMap.get(key);
|
|
375
|
+
if (existing) {
|
|
376
|
+
existing.ids.push(issue.id);
|
|
377
|
+
} else {
|
|
378
|
+
groupMap.set(key, { lead: issue, ids: [issue.id] });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const groups = Array.from(groupMap.values());
|
|
382
|
+
|
|
383
|
+
// Sort: severity order, then group size desc within severity
|
|
384
|
+
const sevOrder = { critical: 0, serious: 1, moderate: 2, minor: 3 };
|
|
385
|
+
groups.sort((a, b) => {
|
|
386
|
+
const s = (sevOrder[a.lead.impact] ?? 9) - (sevOrder[b.lead.impact] ?? 9);
|
|
387
|
+
if (s !== 0) return s;
|
|
388
|
+
return b.ids.length - a.ids.length;
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// Render with severity section headers
|
|
392
|
+
let currentSection = '';
|
|
393
|
+
for (const g of groups) {
|
|
394
|
+
if (g.lead.impact !== currentSection) {
|
|
395
|
+
currentSection = g.lead.impact;
|
|
396
|
+
const total = groups.filter(x => x.lead.impact === currentSection).reduce((n, x) => n + x.ids.length, 0);
|
|
397
|
+
listEl.appendChild(h('div', { className: 'ap-group-header' }, currentSection.toUpperCase() + ' \\u00b7 ' + total));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const isActive = g.ids.includes(activeId);
|
|
367
401
|
const contentChildren = [
|
|
368
|
-
h('span', { className: 'ap-wcag' },
|
|
369
|
-
h('
|
|
370
|
-
h('div', { className: 'ap-msg' }, issue.message),
|
|
402
|
+
h('span', { className: 'ap-wcag' }, g.lead.wcag),
|
|
403
|
+
h('div', { className: 'ap-msg' }, g.lead.problem || g.lead.message),
|
|
371
404
|
];
|
|
372
|
-
if (
|
|
405
|
+
if (g.ids.length > 1) {
|
|
406
|
+
contentChildren[0] = h('span', null, [
|
|
407
|
+
h('span', { className: 'ap-wcag' }, g.lead.wcag),
|
|
408
|
+
h('span', { className: 'ap-count-badge', style: { background: colors[g.lead.impact] } }, '\\u00d7' + g.ids.length),
|
|
409
|
+
]);
|
|
410
|
+
}
|
|
411
|
+
if (g.lead.recommendedFix && g.lead.recommendedFix !== g.lead.problem) {
|
|
412
|
+
contentChildren.push(h('div', { className: 'ap-fix' }, g.lead.recommendedFix));
|
|
413
|
+
} else if (g.lead.fixDesc) {
|
|
414
|
+
contentChildren.push(h('div', { className: 'ap-fix' }, 'Fix: ' + g.lead.fixDesc));
|
|
415
|
+
}
|
|
373
416
|
|
|
374
417
|
const row = h('div', {
|
|
375
|
-
className: 'ap-row' + (
|
|
376
|
-
onClick: () => selectIssue(
|
|
377
|
-
onMouseenter: () => {
|
|
378
|
-
|
|
418
|
+
className: 'ap-row' + (isActive ? ' active ap-sticky' : ''),
|
|
419
|
+
onClick: () => selectIssue(g.lead.id),
|
|
420
|
+
onMouseenter: () => {
|
|
421
|
+
// Highlight all members of the group on hover
|
|
422
|
+
for (const memberId of g.ids) {
|
|
423
|
+
const o = overlays[memberId];
|
|
424
|
+
if (o) o.style.outlineWidth = '3px';
|
|
425
|
+
}
|
|
426
|
+
const o = overlays[g.lead.id];
|
|
427
|
+
if (o) o.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
428
|
+
},
|
|
429
|
+
onMouseleave: () => {
|
|
430
|
+
for (const memberId of g.ids) {
|
|
431
|
+
const o = overlays[memberId];
|
|
432
|
+
if (o && memberId !== activeId) o.style.outlineWidth = '2px';
|
|
433
|
+
}
|
|
434
|
+
},
|
|
379
435
|
}, [
|
|
380
|
-
h('div', { className: 'ap-icon', style: { background: colors[
|
|
436
|
+
h('div', { className: 'ap-icon', style: { background: colors[g.lead.impact] }, title: g.lead.impact }, icons[g.lead.impact]),
|
|
381
437
|
h('div', { className: 'ap-content' }, contentChildren),
|
|
382
438
|
]);
|
|
383
439
|
listEl.appendChild(row);
|
|
@@ -429,7 +485,7 @@ function buildHighlightScript(issues, apiKey, framework) {
|
|
|
429
485
|
appliedFixes.add(issue.id);
|
|
430
486
|
previewState.id = -1;
|
|
431
487
|
const ov = overlays[issue.id];
|
|
432
|
-
if (ov) { ov.style.outlineColor = '#
|
|
488
|
+
if (ov) { ov.style.outlineColor = '#15803D'; ov.querySelector('.abilyo-label').style.background = '#15803D'; ov.querySelector('.abilyo-label').textContent = 'Fixed'; }
|
|
433
489
|
}
|
|
434
490
|
|
|
435
491
|
function selectIssue(id) {
|
|
@@ -456,7 +512,39 @@ function buildHighlightScript(issues, apiKey, framework) {
|
|
|
456
512
|
h('span', { className: 'apd-wcag' }, 'WCAG ' + issue.wcag),
|
|
457
513
|
h('span', { className: 'apd-badge', style: { background: colors[issue.impact] } }, issue.impact),
|
|
458
514
|
]));
|
|
459
|
-
|
|
515
|
+
// The headline \u2014 what's wrong (use richer "problem" if available, else the raw message)
|
|
516
|
+
detailEl.appendChild(h('div', { className: 'apd-msg' }, issue.problem || issue.message));
|
|
517
|
+
|
|
518
|
+
// Why this matters \u2014 user-impact explanation
|
|
519
|
+
if (issue.whyMatters) {
|
|
520
|
+
detailEl.appendChild(h('div', { className: 'apd-section' }, [
|
|
521
|
+
h('div', { className: 'apd-section-label' }, 'Why it matters'),
|
|
522
|
+
h('div', { className: 'apd-why' }, issue.whyMatters),
|
|
523
|
+
]));
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Recommended fix \u2014 plain-language guidance from generateDevSuggestion
|
|
527
|
+
if (issue.recommendedFix && issue.recommendedFix !== issue.problem) {
|
|
528
|
+
const recommendEl = h('div', { className: 'apd-recommend' });
|
|
529
|
+
recommendEl.textContent = issue.recommendedFix;
|
|
530
|
+
detailEl.appendChild(h('div', { className: 'apd-section' }, [
|
|
531
|
+
h('div', { className: 'apd-section-label' }, 'How to fix it'),
|
|
532
|
+
recommendEl,
|
|
533
|
+
]));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Link to W3C WCAG explainer
|
|
537
|
+
if (issue.wcagLink) {
|
|
538
|
+
detailEl.appendChild(h('a', {
|
|
539
|
+
className: 'apd-learnmore',
|
|
540
|
+
href: issue.wcagLink,
|
|
541
|
+
target: '_blank',
|
|
542
|
+
rel: 'noopener noreferrer',
|
|
543
|
+
}, [
|
|
544
|
+
document.createTextNode('Read WCAG ' + issue.wcag),
|
|
545
|
+
h('span', { className: 'apd-learnmore-arrow' }, '\\u2192'), // \u2192 (right arrow, not unicode-escape \u2197)
|
|
546
|
+
]));
|
|
547
|
+
}
|
|
460
548
|
|
|
461
549
|
if (issue.canApply && !appliedFixes.has(issue.id)) {
|
|
462
550
|
const ba = h('div', { className: 'apd-before-after' }, [
|
|
@@ -471,39 +559,36 @@ function buildHighlightScript(issues, apiKey, framework) {
|
|
|
471
559
|
]);
|
|
472
560
|
detailEl.appendChild(ba);
|
|
473
561
|
|
|
474
|
-
const previewBtn = h('button', { className: 'apd-btn apd-preview' }, '
|
|
475
|
-
const applyBtn = h('button', { className: 'apd-btn apd-apply' }, '
|
|
562
|
+
const previewBtn = h('button', { className: 'apd-btn apd-preview' }, 'Preview');
|
|
563
|
+
const applyBtn = h('button', { className: 'apd-btn apd-apply' }, 'Apply fix');
|
|
476
564
|
|
|
477
565
|
previewBtn.addEventListener('click', () => {
|
|
478
566
|
const on = previewFix(issue);
|
|
479
567
|
previewBtn.classList.toggle('active', on);
|
|
480
|
-
previewBtn.textContent = on ? '
|
|
568
|
+
previewBtn.textContent = on ? 'Revert' : 'Preview';
|
|
481
569
|
});
|
|
482
570
|
applyBtn.addEventListener('click', () => {
|
|
483
571
|
applyFix(issue);
|
|
484
572
|
applyBtn.classList.add('applied');
|
|
485
|
-
applyBtn.textContent = '
|
|
573
|
+
applyBtn.textContent = 'Applied';
|
|
486
574
|
previewBtn.style.display = 'none';
|
|
487
575
|
});
|
|
488
576
|
|
|
489
577
|
detailEl.appendChild(h('div', { className: 'apd-actions' }, [previewBtn, applyBtn]));
|
|
490
578
|
} else if (appliedFixes.has(issue.id)) {
|
|
491
|
-
detailEl.appendChild(h('div', { className: 'apd-fix' }, '
|
|
579
|
+
detailEl.appendChild(h('div', { className: 'apd-fix' }, 'Fix applied'));
|
|
492
580
|
} else if (issue.fixDesc) {
|
|
493
|
-
detailEl.appendChild(h('div', { className: 'apd-fix' },
|
|
581
|
+
detailEl.appendChild(h('div', { className: 'apd-fix' }, issue.fixDesc));
|
|
494
582
|
}
|
|
495
583
|
|
|
496
|
-
// AI Fix button \u2014
|
|
584
|
+
// AI Fix button \u2014 branded, no purple gradient, AI pill via ::before
|
|
497
585
|
if (!appliedFixes.has(issue.id)) {
|
|
498
586
|
const aiResultContainer = h('div');
|
|
499
|
-
const aiBtn = h('button', { className: 'apd-btn apd-ai' },
|
|
500
|
-
h('span', { className: 'apd-ai-sparkle' }, '\\u2728'),
|
|
501
|
-
document.createTextNode('AI Fix'),
|
|
502
|
-
]);
|
|
587
|
+
const aiBtn = h('button', { className: 'apd-btn apd-ai' }, document.createTextNode('Generate fix'));
|
|
503
588
|
aiBtn.addEventListener('click', async () => {
|
|
504
589
|
aiBtn.disabled = true;
|
|
505
590
|
aiBtn.classList.add('apd-ai-loading');
|
|
506
|
-
aiBtn.textContent = '\\
|
|
591
|
+
aiBtn.textContent = 'Generating fix\\u2026';
|
|
507
592
|
try {
|
|
508
593
|
const el = document.querySelector(issue.selector);
|
|
509
594
|
const elHtml = el ? el.outerHTML.slice(0, 600) : '';
|
|
@@ -531,7 +616,7 @@ function buildHighlightScript(issues, apiKey, framework) {
|
|
|
531
616
|
if (!res.ok) throw new Error('API error');
|
|
532
617
|
const data = await res.json();
|
|
533
618
|
aiBtn.classList.remove('apd-ai-loading');
|
|
534
|
-
aiBtn.textContent = '
|
|
619
|
+
aiBtn.textContent = 'Generate fix';
|
|
535
620
|
aiBtn.disabled = false;
|
|
536
621
|
|
|
537
622
|
const alts = data.alternatives || [];
|
|
@@ -539,15 +624,15 @@ function buildHighlightScript(issues, apiKey, framework) {
|
|
|
539
624
|
|
|
540
625
|
for (let ai = 0; ai < alts.length; ai++) {
|
|
541
626
|
const alt = alts[ai];
|
|
542
|
-
const altBox = h('div', { className: 'apd-ai-result', style: ai > 0 ? { marginTop: '
|
|
543
|
-
h('div', { style: { fontWeight: '700', fontSize: '12px', color: '#
|
|
627
|
+
const altBox = h('div', { className: 'apd-ai-result', style: ai > 0 ? { marginTop: '8px' } : {} }, [
|
|
628
|
+
h('div', { style: { fontWeight: '700', fontSize: '12px', color: '#0052CC', marginBottom: '6px', textTransform: 'uppercase', letterSpacing: '.05em' } }, alt.label || 'Option ' + (ai+1)),
|
|
544
629
|
h('div', { className: 'apd-ai-explain' }, alt.explanation || ''),
|
|
545
630
|
h('div', { className: 'apd-ai-code' }, alt.frameworkCode || (alt.attribute + '="' + alt.value + '"')),
|
|
546
631
|
]);
|
|
547
632
|
|
|
548
|
-
const altActions = h('div', { className: 'apd-actions', style: { marginTop: '
|
|
549
|
-
const pBtn = h('button', { className: 'apd-btn apd-preview', style: { fontSize: '11px', padding: '
|
|
550
|
-
const aBtn = h('button', { className: 'apd-btn apd-apply', style: { fontSize: '11px', padding: '
|
|
633
|
+
const altActions = h('div', { className: 'apd-actions', style: { marginTop: '8px' } });
|
|
634
|
+
const pBtn = h('button', { className: 'apd-btn apd-preview', style: { fontSize: '11px', padding: '5px 11px' } }, 'Preview');
|
|
635
|
+
const aBtn = h('button', { className: 'apd-btn apd-apply', style: { fontSize: '11px', padding: '5px 11px' } }, 'Apply');
|
|
551
636
|
|
|
552
637
|
pBtn.addEventListener('click', () => {
|
|
553
638
|
const t = document.querySelector(issue.selector);
|
|
@@ -562,7 +647,7 @@ function buildHighlightScript(issues, apiKey, framework) {
|
|
|
562
647
|
if (alt.attribute === 'style') t.setAttribute('style', (previewState.original ? previewState.original + ';' : '') + alt.value);
|
|
563
648
|
else if (alt.previewCss) t.setAttribute('style', (previewState.original ? previewState.original + ';' : '') + alt.previewCss);
|
|
564
649
|
else if (alt.attribute) t.setAttribute(alt.attribute, alt.value);
|
|
565
|
-
pBtn.textContent = '
|
|
650
|
+
pBtn.textContent = 'Revert'; pBtn.classList.add('active');
|
|
566
651
|
});
|
|
567
652
|
aBtn.addEventListener('click', () => {
|
|
568
653
|
const t = document.querySelector(issue.selector);
|
|
@@ -571,8 +656,8 @@ function buildHighlightScript(issues, apiKey, framework) {
|
|
|
571
656
|
else if (alt.attribute) t.setAttribute(alt.attribute, alt.value);
|
|
572
657
|
appliedFixes.add(issue.id); previewState.id = -1;
|
|
573
658
|
const o = overlays[issue.id];
|
|
574
|
-
if (o) { o.style.outlineColor = '#
|
|
575
|
-
aBtn.classList.add('applied'); aBtn.textContent = '
|
|
659
|
+
if (o) { o.style.outlineColor = '#0052CC'; const l = o.querySelector('.abilyo-label'); if (l) { l.style.background = '#0052CC'; l.textContent = '\\u2713 Fixed'; } }
|
|
660
|
+
aBtn.classList.add('applied'); aBtn.textContent = 'Applied'; pBtn.style.display = 'none';
|
|
576
661
|
});
|
|
577
662
|
altActions.appendChild(pBtn);
|
|
578
663
|
altActions.appendChild(aBtn);
|
|
@@ -581,7 +666,7 @@ function buildHighlightScript(issues, apiKey, framework) {
|
|
|
581
666
|
}
|
|
582
667
|
} catch {
|
|
583
668
|
aiBtn.classList.remove('apd-ai-loading');
|
|
584
|
-
aiBtn.textContent = '
|
|
669
|
+
aiBtn.textContent = 'Try again';
|
|
585
670
|
aiBtn.disabled = false;
|
|
586
671
|
}
|
|
587
672
|
});
|
|
@@ -612,138 +697,7 @@ function buildHighlightScript(issues, apiKey, framework) {
|
|
|
612
697
|
|
|
613
698
|
renderList();
|
|
614
699
|
})();
|
|
615
|
-
`;
|
|
616
|
-
}
|
|
617
|
-
async function localScan(url, options) {
|
|
618
|
-
const wcagTags = WCAG_TAG_MAP[options.wcag.toUpperCase()] ?? WCAG_TAG_MAP["AA"];
|
|
619
|
-
const viewport = ["mobile", "tablet", "desktop"].includes(options.viewport ?? "") ? options.viewport : "desktop";
|
|
620
|
-
const pw = await import("playwright");
|
|
621
|
-
const browser = await pw.chromium.launch({ headless: !options.show });
|
|
622
|
-
const context = await browser.newContext({ viewport: viewport === "mobile" ? { width: 375, height: 667 } : viewport === "tablet" ? { width: 768, height: 1024 } : { width: 1280, height: 720 } });
|
|
623
|
-
const page = await context.newPage();
|
|
624
|
-
const targetUrl = url.startsWith("http") ? url : `https://${url}`;
|
|
625
|
-
await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 3e4 });
|
|
626
|
-
const fw = await detectFramework(page);
|
|
627
|
-
const result = await scan2(page, {
|
|
628
|
-
wcagTags,
|
|
629
|
-
includeAxe: true,
|
|
630
|
-
dismissModals: true,
|
|
631
|
-
deep: options.deep,
|
|
632
|
-
deepApiUrl: options.deep ? "https://api.webability.io" : void 0,
|
|
633
|
-
deepApiKey: options.deep ? options.apiKey : void 0
|
|
634
|
-
});
|
|
635
|
-
if (options.show && result.issues.length > 0) {
|
|
636
|
-
await page.evaluate(buildHighlightScript(result.issues, options.apiKey, fw.framework));
|
|
637
|
-
await page.evaluate(() => window.scrollTo(0, 0));
|
|
638
|
-
return { ...result, framework: fw.framework, _browser: browser };
|
|
639
|
-
}
|
|
640
|
-
await browser.close();
|
|
641
|
-
return { ...result, framework: fw.framework };
|
|
642
|
-
}
|
|
643
|
-
async function closeBrowser(result) {
|
|
644
|
-
if (result._browser) {
|
|
645
|
-
await result._browser.close();
|
|
646
|
-
delete result._browser;
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// src/flow-scan.ts
|
|
651
|
-
import { scan as scan3, detectFramework as detectFramework2 } from "@webability/core";
|
|
652
|
-
var WCAG_TAG_MAP2 = {
|
|
653
|
-
"A": ["wcag2a"],
|
|
654
|
-
"AA": ["wcag2a", "wcag2aa", "wcag21aa", "wcag22aa"],
|
|
655
|
-
"AAA": ["wcag2a", "wcag2aa", "wcag2aaa", "wcag21aa", "wcag22aa"]
|
|
656
|
-
};
|
|
657
|
-
async function flowScan(startUrl, options = {}) {
|
|
658
|
-
const wcagTags = WCAG_TAG_MAP2[(options.wcag || "AA").toUpperCase()] ?? WCAG_TAG_MAP2["AA"];
|
|
659
|
-
const targetUrl = startUrl.startsWith("http") ? startUrl : `https://${startUrl}`;
|
|
660
|
-
const t0 = Date.now();
|
|
661
|
-
const pw = await import("playwright");
|
|
662
|
-
const browser = await pw.chromium.launch({ headless: false });
|
|
663
|
-
const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
|
|
664
|
-
const page = await context.newPage();
|
|
665
|
-
const pageResults = [];
|
|
666
|
-
let framework = "plain-css";
|
|
667
|
-
let scanning = false;
|
|
668
|
-
let scanQueued = false;
|
|
669
|
-
const scannedUrls = /* @__PURE__ */ new Set();
|
|
670
|
-
const scanCurrentPage = async () => {
|
|
671
|
-
if (scanning) {
|
|
672
|
-
scanQueued = true;
|
|
673
|
-
return;
|
|
674
|
-
}
|
|
675
|
-
scanning = true;
|
|
676
|
-
try {
|
|
677
|
-
await page.waitForLoadState("domcontentloaded", { timeout: 5e3 }).catch(() => {
|
|
678
|
-
});
|
|
679
|
-
await page.waitForTimeout(800);
|
|
680
|
-
const url = page.url();
|
|
681
|
-
if (scannedUrls.has(url)) {
|
|
682
|
-
scanning = false;
|
|
683
|
-
return;
|
|
684
|
-
}
|
|
685
|
-
scannedUrls.add(url);
|
|
686
|
-
if (!framework || framework === "plain-css") {
|
|
687
|
-
const fw = await detectFramework2(page).catch(() => ({ framework: "plain-css" }));
|
|
688
|
-
framework = fw.framework;
|
|
689
|
-
}
|
|
690
|
-
const result = await scan3(page, { wcagTags, includeAxe: true, dismissModals: false });
|
|
691
|
-
pageResults.push({ url, issues: result.issues, scannedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
692
|
-
options.onPageScan?.(url, result.issues.length);
|
|
693
|
-
} catch {
|
|
694
|
-
} finally {
|
|
695
|
-
scanning = false;
|
|
696
|
-
if (scanQueued) {
|
|
697
|
-
scanQueued = false;
|
|
698
|
-
setTimeout(scanCurrentPage, 100);
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
};
|
|
702
|
-
page.on("framenavigated", (frame) => {
|
|
703
|
-
if (frame === page.mainFrame()) {
|
|
704
|
-
setTimeout(scanCurrentPage, 600);
|
|
705
|
-
}
|
|
706
|
-
});
|
|
707
|
-
await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 3e4 });
|
|
708
|
-
await new Promise((resolve) => {
|
|
709
|
-
browser.on("disconnected", () => resolve());
|
|
710
|
-
process.once("SIGINT", () => resolve());
|
|
711
|
-
});
|
|
712
|
-
await browser.close().catch(() => {
|
|
713
|
-
});
|
|
714
|
-
const issueMap = /* @__PURE__ */ new Map();
|
|
715
|
-
for (const pr of pageResults) {
|
|
716
|
-
for (const issue of pr.issues) {
|
|
717
|
-
const key = `${issue.type}::${issue.selector}::${issue.wcag}`;
|
|
718
|
-
const existing = issueMap.get(key);
|
|
719
|
-
if (existing) {
|
|
720
|
-
if (!existing.foundOn.includes(pr.url)) {
|
|
721
|
-
existing.foundOn.push(pr.url);
|
|
722
|
-
existing.pageCount = existing.foundOn.length;
|
|
723
|
-
}
|
|
724
|
-
} else {
|
|
725
|
-
issueMap.set(key, { ...issue, foundOn: [pr.url], pageCount: 1 });
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
return {
|
|
730
|
-
pages: pageResults,
|
|
731
|
-
uniqueIssues: Array.from(issueMap.values()).sort((a, b) => {
|
|
732
|
-
const order = { critical: 0, serious: 1, moderate: 2, minor: 3 };
|
|
733
|
-
const aOrd = order[a.impact] ?? 2;
|
|
734
|
-
const bOrd = order[b.impact] ?? 2;
|
|
735
|
-
if (aOrd !== bOrd) return aOrd - bOrd;
|
|
736
|
-
return b.pageCount - a.pageCount;
|
|
737
|
-
}),
|
|
738
|
-
framework,
|
|
739
|
-
totalDuration: Date.now() - t0
|
|
740
|
-
};
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
// src/init.ts
|
|
744
|
-
import { writeFileSync, existsSync } from "fs";
|
|
745
|
-
import chalk from "chalk";
|
|
746
|
-
var DEFAULT_CONFIG = `project: my-project
|
|
700
|
+
`}async function Y(e,t){let n=H[t.wcag.toUpperCase()]??H.AA,o=["mobile","tablet","desktop"].includes(t.viewport??"")?t.viewport:"desktop",i=await(await import("playwright")).chromium.launch({headless:!t.show,ignoreDefaultArgs:t.show?["--enable-automation"]:void 0,args:t.show?["--disable-blink-features=AutomationControlled","--no-default-browser-check","--disable-infobars","--no-first-run","--silent-launch","--disable-features=DialMediaRouteProvider"]:void 0}),l=await(await i.newContext({viewport:o==="mobile"?{width:390,height:844}:o==="tablet"?{width:820,height:1180}:{width:1280,height:720}})).newPage(),d=e.startsWith("http")?e:`https://${e}`;await l.goto(d,{waitUntil:"domcontentloaded",timeout:3e4});let f=await pe(l),c=await de(l,{wcagTags:n,includeAxe:t.includeAxe??!0,dismissModals:!0,deep:t.deep,deepApiUrl:t.deep?"https://api.webability.io":void 0,deepApiKey:t.deep?t.apiKey:void 0});return t.show&&c.issues.length>0?(await l.evaluate(be(c.issues,t.apiKey,f.framework)),await l.evaluate(()=>window.scrollTo(0,0)),{...c,framework:f.framework,_browser:i}):(await i.close(),{...c,framework:f.framework})}async function _(e){e._browser&&(await e._browser.close(),delete e._browser)}import{scan as ye,detectFramework as xe}from"@webability/core";var X={A:["wcag2a"],AA:["wcag2a","wcag2aa","wcag21aa","wcag22aa"],AAA:["wcag2a","wcag2aa","wcag2aaa","wcag21aa","wcag22aa"]};async function Q(e,t={}){let n=X[(t.wcag||"AA").toUpperCase()]??X.AA,o=e.startsWith("http")?e:`https://${e}`,r=Date.now(),s=await(await import("playwright")).chromium.launch({headless:!1}),d=await(await s.newContext({viewport:{width:1280,height:720}})).newPage(),f=[],c="plain-css",g=!1,b=!1,x=new Set,q=async()=>{if(g){b=!0;return}g=!0;try{await d.waitForLoadState("domcontentloaded",{timeout:5e3}).catch(()=>{}),await d.waitForTimeout(800);let u=d.url();if(x.has(u)){g=!1;return}x.add(u),(!c||c==="plain-css")&&(c=(await xe(d).catch(()=>({framework:"plain-css"}))).framework);let y=await ye(d,{wcagTags:n,includeAxe:!0,dismissModals:!1});f.push({url:u,issues:y.issues,scannedAt:new Date().toISOString()}),t.onPageScan?.(u,y.issues.length)}catch{}finally{g=!1,b&&(b=!1,setTimeout(q,100))}};d.on("framenavigated",u=>{u===d.mainFrame()&&setTimeout(q,600)}),await d.goto(o,{waitUntil:"domcontentloaded",timeout:3e4}),await new Promise(u=>{s.on("disconnected",()=>u()),process.once("SIGINT",()=>u())}),await s.close().catch(()=>{});let P=new Map;for(let u of f)for(let y of u.issues){let C=`${y.type}::${y.selector}::${y.wcag}`,w=P.get(C);w?w.foundOn.includes(u.url)||(w.foundOn.push(u.url),w.pageCount=w.foundOn.length):P.set(C,{...y,foundOn:[u.url],pageCount:1})}return{pages:f,uniqueIssues:Array.from(P.values()).sort((u,y)=>{let C={critical:0,serious:1,moderate:2,minor:3},w=C[u.impact]??2,V=C[y.impact]??2;return w!==V?w-V:y.pageCount-u.pageCount}),framework:c,totalDuration:Date.now()-r}}import{writeFileSync as we,existsSync as ve}from"fs";import M from"chalk";var ke=`project: my-project
|
|
747
701
|
urls:
|
|
748
702
|
- http://localhost:3000
|
|
749
703
|
standard: WCAG2.1AA
|
|
@@ -752,537 +706,4 @@ ignore: []
|
|
|
752
706
|
threshold:
|
|
753
707
|
critical: 0
|
|
754
708
|
serious: 5
|
|
755
|
-
`;
|
|
756
|
-
function initConfig() {
|
|
757
|
-
const filename = ".webability.yml";
|
|
758
|
-
if (existsSync(filename)) {
|
|
759
|
-
console.log(chalk.yellow(` ${filename} already exists`));
|
|
760
|
-
return;
|
|
761
|
-
}
|
|
762
|
-
writeFileSync(filename, DEFAULT_CONFIG);
|
|
763
|
-
console.log(chalk.green(` Created ${filename}`));
|
|
764
|
-
console.log(chalk.dim(" Edit it to configure your project URLs and thresholds"));
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
// src/display.ts
|
|
768
|
-
import chalk2 from "chalk";
|
|
769
|
-
var DIM = chalk2.dim;
|
|
770
|
-
var BOLD = chalk2.bold;
|
|
771
|
-
var BLUE = chalk2.blue;
|
|
772
|
-
var GREEN = chalk2.green;
|
|
773
|
-
var RED = chalk2.red;
|
|
774
|
-
var YELLOW = chalk2.yellow;
|
|
775
|
-
var CYAN = chalk2.cyan;
|
|
776
|
-
var WHITE = chalk2.white;
|
|
777
|
-
function header() {
|
|
778
|
-
console.log();
|
|
779
|
-
console.log(` ${BLUE("\u25C6")} ${BOLD("Abilyo")} ${DIM("WCAG Scanner")}`);
|
|
780
|
-
console.log();
|
|
781
|
-
}
|
|
782
|
-
function scoreCard(score, url) {
|
|
783
|
-
const color = score >= 80 ? GREEN : score >= 50 ? YELLOW : RED;
|
|
784
|
-
const icon = score >= 80 ? GREEN("\u2713") : score >= 50 ? YELLOW("\u26A0") : RED("\u2717");
|
|
785
|
-
const label = score >= 80 ? "Passing" : score >= 50 ? "Needs work" : "Failing";
|
|
786
|
-
console.log(` ${DIM("URL")} ${WHITE(url)}`);
|
|
787
|
-
console.log(` ${DIM("Score")} ${color.bold(score + "%")} ${icon} ${DIM(label)}`);
|
|
788
|
-
console.log(` ${DIM(" ")}${renderBar(score)}`);
|
|
789
|
-
console.log();
|
|
790
|
-
}
|
|
791
|
-
function renderBar(score) {
|
|
792
|
-
const w = 32;
|
|
793
|
-
const filled = Math.round(score / 100 * w);
|
|
794
|
-
const empty = w - filled;
|
|
795
|
-
const color = score >= 80 ? GREEN : score >= 50 ? YELLOW : RED;
|
|
796
|
-
return color("\u2501".repeat(filled)) + DIM("\u2501".repeat(empty));
|
|
797
|
-
}
|
|
798
|
-
function summaryLine(s) {
|
|
799
|
-
const parts = [];
|
|
800
|
-
if (s.critical > 0) parts.push(RED.bold(`${s.critical} critical`));
|
|
801
|
-
if (s.serious > 0) parts.push(YELLOW(`${s.serious} serious`));
|
|
802
|
-
if (s.moderate > 0) parts.push(DIM(`${s.moderate} moderate`));
|
|
803
|
-
if (s.minor > 0) parts.push(DIM(`${s.minor} minor`));
|
|
804
|
-
console.log(` ${BOLD(String(s.total))} issues ${parts.join(DIM(" \xB7 "))}`);
|
|
805
|
-
console.log();
|
|
806
|
-
}
|
|
807
|
-
function issueBlock(opts) {
|
|
808
|
-
const icon = opts.severity === "critical" ? RED("\u2717") : opts.severity === "serious" ? YELLOW("\u25B2") : DIM("\u25CF");
|
|
809
|
-
const wcag = DIM(`${opts.wcag}`);
|
|
810
|
-
const count = opts.count && opts.count > 1 ? DIM(` \xD7${opts.count}`) : "";
|
|
811
|
-
console.log(` ${icon} ${wcag}${count} ${opts.problem.slice(0, 72)}`);
|
|
812
|
-
if (opts.contrast) {
|
|
813
|
-
console.log(` ${CYAN("fix")} ${opts.fix.slice(0, 70)}`);
|
|
814
|
-
console.log(` ${DIM(`${opts.contrast.current}:1 \u2192 ${opts.contrast.suggested}:1 (min ${opts.contrast.required}:1)`)}`);
|
|
815
|
-
} else if (opts.before && opts.after) {
|
|
816
|
-
console.log(` ${CYAN("fix")} ${DIM(opts.before)} ${DIM("\u2192")} ${GREEN(opts.after)}`);
|
|
817
|
-
} else {
|
|
818
|
-
console.log(` ${CYAN("fix")} ${opts.fix.slice(0, 70)}`);
|
|
819
|
-
}
|
|
820
|
-
if (opts.html && opts.count === 1) {
|
|
821
|
-
const snippet = opts.html.slice(0, 60).replace(/\n/g, " ");
|
|
822
|
-
console.log(` ${DIM(snippet)}`);
|
|
823
|
-
}
|
|
824
|
-
if (opts.selectors && opts.selectors.length > 0 && opts.count && opts.count > 1) {
|
|
825
|
-
const shown = opts.selectors.slice(0, 2).map((s) => s.slice(0, 30)).join(DIM(", "));
|
|
826
|
-
const more = opts.count > 2 ? DIM(` +${opts.count - 2} more`) : "";
|
|
827
|
-
console.log(` ${DIM(shown + more)}`);
|
|
828
|
-
} else if (opts.selector) {
|
|
829
|
-
console.log(` ${DIM(opts.selector.slice(0, 55))}`);
|
|
830
|
-
}
|
|
831
|
-
console.log();
|
|
832
|
-
}
|
|
833
|
-
function footer(opts) {
|
|
834
|
-
console.log(DIM(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
835
|
-
if (opts?.hasIssues) {
|
|
836
|
-
console.log(` ${DIM("Next steps:")}`);
|
|
837
|
-
console.log(` ${CYAN("abilyo scan " + (opts.url || "<url>") + " --format json")} ${DIM("CI/CD output")}`);
|
|
838
|
-
console.log(` ${CYAN("abilyo scan " + (opts.url || "<url>") + " --format html > report.html")} ${DIM("share report")}`);
|
|
839
|
-
console.log();
|
|
840
|
-
}
|
|
841
|
-
console.log(` ${BLUE("\u25C6")} ${DIM("Auto-fix at")} ${BLUE("abilyo.com")}`);
|
|
842
|
-
console.log();
|
|
843
|
-
}
|
|
844
|
-
function errorMsg(msg) {
|
|
845
|
-
console.log(RED(` \u2717 Error: ${msg}`));
|
|
846
|
-
console.log();
|
|
847
|
-
}
|
|
848
|
-
function jsonOutput(data) {
|
|
849
|
-
console.log(JSON.stringify(data, null, 2));
|
|
850
|
-
}
|
|
851
|
-
function csvOutput(issues) {
|
|
852
|
-
console.log("severity,wcag,message,element");
|
|
853
|
-
for (const i of issues) {
|
|
854
|
-
const msg = i.message.replace(/"/g, '""');
|
|
855
|
-
const el = (i.element || "").replace(/"/g, '""');
|
|
856
|
-
console.log(`${i.severity},${i.wcag},"${msg}","${el}"`);
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
// src/cli.ts
|
|
861
|
-
var program = new Command();
|
|
862
|
-
program.name("abilyo").description("Abilyo by WebAbility \u2014 WCAG accessibility scanner").version("1.0.0");
|
|
863
|
-
program.command("scan <url>").description("Scan a website for accessibility issues").option("-f, --format <format>", "Output format: table, json, csv, sarif, html", "table").option("--wcag <level>", "WCAG level: A, AA, AAA", "AA").option("--deep", "Run a deeper scan with additional checks (Pro plan)").option("--remote", "Run scan on WebAbility servers instead of locally").option("--upload", "Upload results to dashboard (requires login)").option("--exit", "Exit with code 1 if issues found (CI mode)").option("--no-axe", "Skip axe-core (use only WebAbility detectors)").option("--viewport <size>", "Viewport: mobile, tablet, desktop", "desktop").option("--show", "Open browser and highlight issues visually").action(async (url, opts) => {
|
|
864
|
-
if (!url.startsWith("http")) url = `https://${url}`;
|
|
865
|
-
if (opts.deep || opts.upload) {
|
|
866
|
-
const apiKey = getApiKey();
|
|
867
|
-
if (!apiKey) {
|
|
868
|
-
if (opts.format === "table") header();
|
|
869
|
-
errorMsg("--deep and --upload require a Pro plan. Run `abilyo login` first.");
|
|
870
|
-
return process.exit(1);
|
|
871
|
-
}
|
|
872
|
-
try {
|
|
873
|
-
const verify = await fetch("https://api.webability.io/cli/verify", {
|
|
874
|
-
headers: { "Authorization": `Bearer ${apiKey}` }
|
|
875
|
-
});
|
|
876
|
-
if (verify.ok) {
|
|
877
|
-
const data = await verify.json();
|
|
878
|
-
if (!data.active) {
|
|
879
|
-
if (opts.format === "table") header();
|
|
880
|
-
errorMsg("Your account does not have an active Pro plan.");
|
|
881
|
-
console.log(chalk3.dim(" Upgrade at https://abilyo.com/pricing"));
|
|
882
|
-
return process.exit(1);
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
} catch {
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
if (opts.remote) {
|
|
889
|
-
await remoteScan(url, opts);
|
|
890
|
-
return;
|
|
891
|
-
}
|
|
892
|
-
if (opts.format === "table") header();
|
|
893
|
-
const t0 = Date.now();
|
|
894
|
-
const spinner = ora({ text: "Scanning...", prefixText: " " }).start();
|
|
895
|
-
let result;
|
|
896
|
-
try {
|
|
897
|
-
result = await localScan(url, {
|
|
898
|
-
wcag: opts.wcag,
|
|
899
|
-
deep: opts.deep ?? false,
|
|
900
|
-
apiKey: getApiKey() ?? void 0,
|
|
901
|
-
show: opts.show ?? false
|
|
902
|
-
});
|
|
903
|
-
const elapsed = ((Date.now() - t0) / 1e3).toFixed(1);
|
|
904
|
-
spinner.succeed(`Scanned ${result.issues.length} issues in ${elapsed}s`);
|
|
905
|
-
if (opts.format === "json") {
|
|
906
|
-
const sanitized = {
|
|
907
|
-
...result,
|
|
908
|
-
issues: result.issues.map(({ source, ...rest }) => rest)
|
|
909
|
-
};
|
|
910
|
-
console.log(JSON.stringify(sanitized, null, 2));
|
|
911
|
-
} else if (opts.format === "csv") {
|
|
912
|
-
csvOutput(result.issues.map((i) => ({
|
|
913
|
-
severity: i.impact,
|
|
914
|
-
wcag: i.wcag,
|
|
915
|
-
message: i.message,
|
|
916
|
-
element: i.selector
|
|
917
|
-
})));
|
|
918
|
-
} else if (opts.format === "sarif") {
|
|
919
|
-
console.log(JSON.stringify(toSarif(result), null, 2));
|
|
920
|
-
} else if (opts.format === "html") {
|
|
921
|
-
const { toHtmlReport } = await import("@webability/core");
|
|
922
|
-
console.log(toHtmlReport(result));
|
|
923
|
-
} else {
|
|
924
|
-
scoreCard(result.summary.total === 0 ? 100 : Math.max(0, 100 - result.summary.total), url);
|
|
925
|
-
summaryLine(result.summary);
|
|
926
|
-
if (result.issues.length > 0) {
|
|
927
|
-
const { generateDevSuggestion } = await import("@webability/core");
|
|
928
|
-
const fw = result.framework || "plain-css";
|
|
929
|
-
const grouped = [];
|
|
930
|
-
const seen = /* @__PURE__ */ new Map();
|
|
931
|
-
for (const issue of result.issues) {
|
|
932
|
-
const s = generateDevSuggestion(issue, fw);
|
|
933
|
-
const key = `${issue.type}|${s.fix.slice(0, 50)}`;
|
|
934
|
-
const idx = seen.get(key);
|
|
935
|
-
if (idx !== void 0) {
|
|
936
|
-
grouped[idx].count++;
|
|
937
|
-
if (grouped[idx].selectors.length < 3) grouped[idx].selectors.push(issue.selector.slice(0, 50));
|
|
938
|
-
} else {
|
|
939
|
-
seen.set(key, grouped.length);
|
|
940
|
-
grouped.push({ suggestion: s, issue, count: 1, selectors: [issue.selector.slice(0, 50)] });
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
grouped.sort((a, b) => {
|
|
944
|
-
const order = { critical: 0, serious: 1, moderate: 2, minor: 3 };
|
|
945
|
-
return (order[a.issue.impact] ?? 2) - (order[b.issue.impact] ?? 2);
|
|
946
|
-
});
|
|
947
|
-
for (const g of grouped.slice(0, 20)) {
|
|
948
|
-
issueBlock({
|
|
949
|
-
severity: g.issue.impact,
|
|
950
|
-
wcag: g.issue.wcag,
|
|
951
|
-
problem: g.suggestion.problem,
|
|
952
|
-
fix: g.suggestion.fix,
|
|
953
|
-
count: g.count,
|
|
954
|
-
selector: g.count === 1 ? g.issue.selector : void 0,
|
|
955
|
-
selectors: g.count > 1 ? g.selectors : void 0,
|
|
956
|
-
contrast: g.suggestion.contrast,
|
|
957
|
-
before: g.suggestion.before,
|
|
958
|
-
after: g.suggestion.after,
|
|
959
|
-
html: g.issue.html
|
|
960
|
-
});
|
|
961
|
-
}
|
|
962
|
-
if (grouped.length > 20) {
|
|
963
|
-
console.log(chalk3.dim(` ... +${grouped.length - 20} more`));
|
|
964
|
-
console.log();
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
footer({ hasIssues: result.issues.length > 0, url: url.replace("https://", "") });
|
|
968
|
-
}
|
|
969
|
-
if (opts.show && result.issues.length > 0) {
|
|
970
|
-
console.log(chalk3.cyan(" \u25C6 Browser open \u2014 issues highlighted. Press Enter to close."));
|
|
971
|
-
console.log();
|
|
972
|
-
await new Promise((resolve) => {
|
|
973
|
-
process.stdin.resume();
|
|
974
|
-
process.stdin.once("data", () => {
|
|
975
|
-
process.stdin.pause();
|
|
976
|
-
resolve();
|
|
977
|
-
});
|
|
978
|
-
});
|
|
979
|
-
await closeBrowser(result);
|
|
980
|
-
}
|
|
981
|
-
if (opts.exit && result.summary.total > 0) {
|
|
982
|
-
process.exit(1);
|
|
983
|
-
}
|
|
984
|
-
} catch (err) {
|
|
985
|
-
spinner.stop();
|
|
986
|
-
if (result) await closeBrowser(result).catch(() => {
|
|
987
|
-
});
|
|
988
|
-
errorMsg(err.message);
|
|
989
|
-
process.exit(1);
|
|
990
|
-
}
|
|
991
|
-
});
|
|
992
|
-
async function remoteScan(url, opts) {
|
|
993
|
-
const apiKey = getApiKey();
|
|
994
|
-
if (!apiKey) {
|
|
995
|
-
if (opts.format === "table") header();
|
|
996
|
-
errorMsg("--remote requires login. Run `wa login` first.");
|
|
997
|
-
return process.exit(1);
|
|
998
|
-
}
|
|
999
|
-
if (opts.format === "table") header();
|
|
1000
|
-
const spinner = ora({ text: "Scanning (remote)...", prefixText: " " }).start();
|
|
1001
|
-
try {
|
|
1002
|
-
const result = await scan(url, apiKey, (status) => {
|
|
1003
|
-
spinner.text = status;
|
|
1004
|
-
});
|
|
1005
|
-
spinner.text = "Fetching report...";
|
|
1006
|
-
const report = await getReport(result.key, apiKey);
|
|
1007
|
-
spinner.stop();
|
|
1008
|
-
if (!report) {
|
|
1009
|
-
errorMsg("Report not found");
|
|
1010
|
-
return process.exit(1);
|
|
1011
|
-
}
|
|
1012
|
-
const issues = [];
|
|
1013
|
-
if (report.axe?.violations) {
|
|
1014
|
-
for (const v of report.axe.violations) {
|
|
1015
|
-
for (const node of v.nodes || []) {
|
|
1016
|
-
issues.push({ severity: v.impact || "moderate", wcag: v.id, message: v.description, element: node.target?.[0] });
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
if (report.ByFunctions) {
|
|
1021
|
-
for (const fn of report.ByFunctions) {
|
|
1022
|
-
for (const issue of fn.issues || []) {
|
|
1023
|
-
issues.push({ severity: issue.impact || "moderate", wcag: issue.code || "", message: issue.description || "", element: issue.element });
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
if (opts.format === "json") {
|
|
1028
|
-
jsonOutput({ url, score: report.score, totalElements: report.totalElements, issues });
|
|
1029
|
-
} else if (opts.format === "csv") {
|
|
1030
|
-
csvOutput(issues);
|
|
1031
|
-
} else {
|
|
1032
|
-
scoreCard(report.score || 0, url);
|
|
1033
|
-
summaryLine({ critical: 0, serious: issues.filter((i) => i.severity === "serious").length, moderate: issues.filter((i) => i.severity === "moderate").length, minor: 0, total: issues.length });
|
|
1034
|
-
for (const i of issues.slice(0, 20)) {
|
|
1035
|
-
issueBlock({ severity: i.severity, wcag: i.wcag, problem: i.message, fix: "Review this issue", selector: i.element });
|
|
1036
|
-
}
|
|
1037
|
-
footer();
|
|
1038
|
-
}
|
|
1039
|
-
if (opts.exit && issues.length > 0) process.exit(1);
|
|
1040
|
-
} catch (err) {
|
|
1041
|
-
spinner.stop();
|
|
1042
|
-
errorMsg(err.message);
|
|
1043
|
-
process.exit(1);
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
program.command("flow <url>").description("Scan a multi-page user journey \u2014 one consolidated report").option("--wcag <level>", "WCAG level: A, AA, AAA", "AA").action(async (url, opts) => {
|
|
1047
|
-
if (!url.startsWith("http")) url = `https://${url}`;
|
|
1048
|
-
header();
|
|
1049
|
-
console.log(chalk3.bold(" Flow Scan"));
|
|
1050
|
-
console.log();
|
|
1051
|
-
console.log(` Starting at: ${chalk3.cyan(url)}`);
|
|
1052
|
-
console.log(chalk3.dim(" Click through your user journey. Close the browser when done."));
|
|
1053
|
-
console.log();
|
|
1054
|
-
let pagesScanned = 0;
|
|
1055
|
-
const result = await flowScan(url, {
|
|
1056
|
-
wcag: opts.wcag,
|
|
1057
|
-
onPageScan: (pageUrl, count) => {
|
|
1058
|
-
pagesScanned++;
|
|
1059
|
-
console.log(` ${chalk3.green("\u2713")} ${chalk3.dim(String(pagesScanned).padStart(2))} ${pageUrl.replace(/^https?:\/\//, "").slice(0, 60)} ${chalk3.dim("\u2014 " + count + " issues")}`);
|
|
1060
|
-
}
|
|
1061
|
-
});
|
|
1062
|
-
console.log();
|
|
1063
|
-
console.log(chalk3.bold(" \u2500\u2500\u2500 Flow Report \u2500\u2500\u2500"));
|
|
1064
|
-
console.log();
|
|
1065
|
-
console.log(` Pages scanned: ${chalk3.cyan(result.pages.length)}`);
|
|
1066
|
-
console.log(` Unique issues: ${chalk3.cyan(result.uniqueIssues.length)}`);
|
|
1067
|
-
console.log(` Total duration: ${chalk3.dim((result.totalDuration / 1e3).toFixed(1) + "s")}`);
|
|
1068
|
-
console.log();
|
|
1069
|
-
const counts = { critical: 0, serious: 0, moderate: 0, minor: 0 };
|
|
1070
|
-
result.uniqueIssues.forEach((i) => {
|
|
1071
|
-
counts[i.impact]++;
|
|
1072
|
-
});
|
|
1073
|
-
summaryLine({ ...counts, total: result.uniqueIssues.length });
|
|
1074
|
-
const topIssues = result.uniqueIssues.slice(0, 15);
|
|
1075
|
-
for (const issue of topIssues) {
|
|
1076
|
-
const pageStr = issue.pageCount > 1 ? chalk3.yellow(` \xD7${issue.pageCount} pages`) : "";
|
|
1077
|
-
issueBlock({
|
|
1078
|
-
severity: issue.impact,
|
|
1079
|
-
wcag: issue.wcag,
|
|
1080
|
-
problem: issue.message,
|
|
1081
|
-
fix: issue.fix?.suggestedValue ? `Set ${issue.fix.attribute}="${issue.fix.suggestedValue}"` : "Review this issue",
|
|
1082
|
-
selector: issue.selector
|
|
1083
|
-
});
|
|
1084
|
-
if (issue.pageCount > 1) {
|
|
1085
|
-
console.log(chalk3.dim(` Found on${pageStr}:`));
|
|
1086
|
-
for (const url2 of issue.foundOn.slice(0, 3)) {
|
|
1087
|
-
console.log(chalk3.dim(" " + url2.replace(/^https?:\/\//, "")));
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
if (result.uniqueIssues.length > 15) {
|
|
1092
|
-
console.log(chalk3.dim(` ... +${result.uniqueIssues.length - 15} more unique issues`));
|
|
1093
|
-
}
|
|
1094
|
-
console.log();
|
|
1095
|
-
footer({ hasIssues: result.uniqueIssues.length > 0, url: url.replace("https://", "") });
|
|
1096
|
-
});
|
|
1097
|
-
program.command("init").description("Create .webability.yml config file").action(() => {
|
|
1098
|
-
header();
|
|
1099
|
-
initConfig();
|
|
1100
|
-
console.log();
|
|
1101
|
-
});
|
|
1102
|
-
program.command("login").description("Authenticate with your WebAbility account").option("-k, --key <key>", "API key directly (for CI/CD)").action(async (opts) => {
|
|
1103
|
-
header();
|
|
1104
|
-
if (opts.key) {
|
|
1105
|
-
setApiKey(opts.key);
|
|
1106
|
-
console.log(chalk3.green(" \u2713 API key saved."));
|
|
1107
|
-
console.log();
|
|
1108
|
-
return;
|
|
1109
|
-
}
|
|
1110
|
-
const spinner = ora({ text: "Starting login...", prefixText: " " }).start();
|
|
1111
|
-
try {
|
|
1112
|
-
const res = await fetch("https://api.webability.io/cli/device-code", {
|
|
1113
|
-
method: "POST",
|
|
1114
|
-
headers: { "Content-Type": "application/json" }
|
|
1115
|
-
});
|
|
1116
|
-
if (!res.ok) {
|
|
1117
|
-
spinner.stop();
|
|
1118
|
-
const loginUrl = "https://app.webability.io/settings/api-keys";
|
|
1119
|
-
console.log(` Open this URL to get your API key:`);
|
|
1120
|
-
console.log();
|
|
1121
|
-
console.log(` ${chalk3.cyan(loginUrl)}`);
|
|
1122
|
-
console.log();
|
|
1123
|
-
console.log(` Then run: ${chalk3.cyan("abilyo login --key YOUR_KEY")}`);
|
|
1124
|
-
console.log();
|
|
1125
|
-
openUrl(loginUrl);
|
|
1126
|
-
return;
|
|
1127
|
-
}
|
|
1128
|
-
const { deviceCode, userCode, verificationUrl, expiresIn } = await res.json();
|
|
1129
|
-
spinner.stop();
|
|
1130
|
-
console.log(` ${chalk3.bold("Login to WebAbility")}`);
|
|
1131
|
-
console.log();
|
|
1132
|
-
console.log(` Open: ${chalk3.cyan(verificationUrl)}`);
|
|
1133
|
-
console.log(` Code: ${chalk3.bold.yellow(userCode)}`);
|
|
1134
|
-
console.log();
|
|
1135
|
-
openUrl(verificationUrl);
|
|
1136
|
-
const pollSpinner = ora({ text: "Waiting for approval...", prefixText: " " }).start();
|
|
1137
|
-
const pollInterval = 3e3;
|
|
1138
|
-
const maxPolls = Math.floor((expiresIn || 300) * 1e3 / pollInterval);
|
|
1139
|
-
for (let i = 0; i < maxPolls; i++) {
|
|
1140
|
-
await new Promise((r) => setTimeout(r, pollInterval));
|
|
1141
|
-
try {
|
|
1142
|
-
const tokenRes = await fetch("https://api.webability.io/cli/device-token", {
|
|
1143
|
-
method: "POST",
|
|
1144
|
-
headers: { "Content-Type": "application/json" },
|
|
1145
|
-
body: JSON.stringify({ deviceCode })
|
|
1146
|
-
});
|
|
1147
|
-
if (tokenRes.ok) {
|
|
1148
|
-
const { token } = await tokenRes.json();
|
|
1149
|
-
setApiKey(token);
|
|
1150
|
-
pollSpinner.succeed("Logged in!");
|
|
1151
|
-
console.log();
|
|
1152
|
-
return;
|
|
1153
|
-
}
|
|
1154
|
-
const body = await tokenRes.json().catch(() => ({}));
|
|
1155
|
-
if (body.error === "expired") {
|
|
1156
|
-
pollSpinner.fail("Login expired. Run `abilyo login` again.");
|
|
1157
|
-
return process.exit(1);
|
|
1158
|
-
}
|
|
1159
|
-
} catch {
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
pollSpinner.fail("Login timed out.");
|
|
1163
|
-
process.exit(1);
|
|
1164
|
-
} catch {
|
|
1165
|
-
spinner.stop();
|
|
1166
|
-
const loginUrl = "https://app.webability.io/settings/api-keys";
|
|
1167
|
-
console.log(` Open: ${chalk3.cyan(loginUrl)}`);
|
|
1168
|
-
console.log(` Then: ${chalk3.cyan("abilyo login --key YOUR_KEY")}`);
|
|
1169
|
-
console.log();
|
|
1170
|
-
}
|
|
1171
|
-
});
|
|
1172
|
-
function openUrl(url) {
|
|
1173
|
-
try {
|
|
1174
|
-
const { execFile } = __require("child_process");
|
|
1175
|
-
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
1176
|
-
execFile(cmd, [url], () => {
|
|
1177
|
-
});
|
|
1178
|
-
} catch {
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
program.command("whoami").description("Show current authentication status").action(() => {
|
|
1182
|
-
const key = getApiKey();
|
|
1183
|
-
header();
|
|
1184
|
-
if (key) {
|
|
1185
|
-
console.log(chalk3.green(" Authenticated"));
|
|
1186
|
-
console.log(chalk3.dim(` Key: ${key.slice(0, 20)}...`));
|
|
1187
|
-
} else {
|
|
1188
|
-
console.log(chalk3.yellow(" Not authenticated"));
|
|
1189
|
-
console.log(chalk3.dim(" Run `wa login` to authenticate"));
|
|
1190
|
-
}
|
|
1191
|
-
console.log();
|
|
1192
|
-
});
|
|
1193
|
-
program.command("logout").description("Remove saved API key").action(() => {
|
|
1194
|
-
clearConfig();
|
|
1195
|
-
header();
|
|
1196
|
-
console.log(chalk3.green(" Logged out. API key removed."));
|
|
1197
|
-
console.log();
|
|
1198
|
-
});
|
|
1199
|
-
program.command("activate <code>").description("Activate Abilyo with your early access code").action(async (code) => {
|
|
1200
|
-
header();
|
|
1201
|
-
const spinner = ora({ text: "Validating access code...", prefixText: " " }).start();
|
|
1202
|
-
try {
|
|
1203
|
-
const res = await fetch("https://api.webability.io/cli/activate", {
|
|
1204
|
-
method: "POST",
|
|
1205
|
-
headers: { "Content-Type": "application/json" },
|
|
1206
|
-
body: JSON.stringify({ code: code.trim() })
|
|
1207
|
-
});
|
|
1208
|
-
if (res.ok) {
|
|
1209
|
-
setAccessCode(code.trim());
|
|
1210
|
-
spinner.succeed("Abilyo activated!");
|
|
1211
|
-
console.log();
|
|
1212
|
-
console.log(chalk3.dim(" Run `abilyo scan <url>` to get started"));
|
|
1213
|
-
console.log();
|
|
1214
|
-
return;
|
|
1215
|
-
}
|
|
1216
|
-
} catch {
|
|
1217
|
-
}
|
|
1218
|
-
if (code.trim().length >= 8) {
|
|
1219
|
-
setAccessCode(code.trim());
|
|
1220
|
-
spinner.succeed("Abilyo activated!");
|
|
1221
|
-
console.log();
|
|
1222
|
-
console.log(chalk3.dim(" Run `abilyo scan <url>` to get started"));
|
|
1223
|
-
console.log();
|
|
1224
|
-
} else {
|
|
1225
|
-
spinner.fail("Invalid access code");
|
|
1226
|
-
console.log(chalk3.dim(" Request access at https://abilyo.com/early-access"));
|
|
1227
|
-
process.exit(1);
|
|
1228
|
-
}
|
|
1229
|
-
});
|
|
1230
|
-
program.argument("[url]", "URL to scan").action((url) => {
|
|
1231
|
-
if (url) {
|
|
1232
|
-
program.parse(["node", "abilyo", "scan", url, ...process.argv.slice(3)]);
|
|
1233
|
-
} else {
|
|
1234
|
-
header();
|
|
1235
|
-
if (!isActivated()) {
|
|
1236
|
-
console.log(chalk3.yellow(" Abilyo is invite-only during early access."));
|
|
1237
|
-
console.log();
|
|
1238
|
-
console.log(` ${chalk3.cyan("abilyo activate")} <code> Activate with your access code`);
|
|
1239
|
-
console.log();
|
|
1240
|
-
console.log(chalk3.dim(" Request access at https://abilyo.com/early-access"));
|
|
1241
|
-
} else {
|
|
1242
|
-
console.log(chalk3.bold(" Commands:"));
|
|
1243
|
-
console.log();
|
|
1244
|
-
console.log(` ${chalk3.cyan("abilyo scan")} <url> Scan for accessibility issues`);
|
|
1245
|
-
console.log(` ${chalk3.cyan("abilyo scan --deep")} <url> Deeper scan with additional checks (Pro)`);
|
|
1246
|
-
console.log(` ${chalk3.cyan("abilyo scan --exit")} <url> CI mode \u2014 exit 1 if issues found`);
|
|
1247
|
-
console.log(` ${chalk3.cyan("abilyo init")} Create .webability.yml config`);
|
|
1248
|
-
console.log(` ${chalk3.cyan("abilyo login")} Authenticate with your account`);
|
|
1249
|
-
console.log(` ${chalk3.cyan("abilyo whoami")} Show auth status`);
|
|
1250
|
-
console.log();
|
|
1251
|
-
console.log(chalk3.dim(" Example: abilyo scan localhost:3000"));
|
|
1252
|
-
console.log(chalk3.dim(" Example: abilyo scan example.com --format json"));
|
|
1253
|
-
console.log(chalk3.dim(" Example: abilyo scan example.com --deep --exit"));
|
|
1254
|
-
}
|
|
1255
|
-
console.log();
|
|
1256
|
-
}
|
|
1257
|
-
});
|
|
1258
|
-
program.parse();
|
|
1259
|
-
function toSarif(result) {
|
|
1260
|
-
return {
|
|
1261
|
-
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
|
|
1262
|
-
version: "2.1.0",
|
|
1263
|
-
runs: [{
|
|
1264
|
-
tool: {
|
|
1265
|
-
driver: {
|
|
1266
|
-
name: "WebAbility",
|
|
1267
|
-
version: "1.0.0",
|
|
1268
|
-
informationUri: "https://webability.io",
|
|
1269
|
-
rules: result.issues.map((i) => ({
|
|
1270
|
-
id: i.type,
|
|
1271
|
-
shortDescription: { text: i.message }
|
|
1272
|
-
}))
|
|
1273
|
-
}
|
|
1274
|
-
},
|
|
1275
|
-
results: result.issues.map((i) => ({
|
|
1276
|
-
ruleId: i.type,
|
|
1277
|
-
level: i.impact === "critical" || i.impact === "serious" ? "error" : "warning",
|
|
1278
|
-
message: { text: i.message },
|
|
1279
|
-
locations: [{
|
|
1280
|
-
physicalLocation: {
|
|
1281
|
-
artifactLocation: { uri: result.url },
|
|
1282
|
-
region: { snippet: { text: i.html || i.selector } }
|
|
1283
|
-
}
|
|
1284
|
-
}]
|
|
1285
|
-
}))
|
|
1286
|
-
}]
|
|
1287
|
-
};
|
|
1288
|
-
}
|
|
709
|
+
`;function Z(){let e=".webability.yml";if(ve(e)){console.log(M.yellow(` ${e} already exists`));return}we(e,ke),console.log(M.green(` Created ${e}`)),console.log(M.dim(" Edit it to configure your project URLs and thresholds"))}import v from"chalk";var p=v.dim,ee=v.bold,z=v.blue,B=v.green,F=v.red,A=v.yellow,$=v.cyan,Ae=v.white;function m(){console.log(),console.log(` ${z("\u25C6")} ${ee("Abilyo")} ${p("WCAG Scanner")}`),console.log()}function j(e,t){let n=e>=80?B:e>=50?A:F,o=e>=80?B("\u2713"):e>=50?A("\u26A0"):F("\u2717"),r=e>=80?"Passing":e>=50?"Needs work":"Failing";console.log(` ${p("URL")} ${Ae(t)}`),console.log(` ${p("Score")} ${n.bold(e+"%")} ${o} ${p(r)}`),console.log(` ${p(" ")}${Ce(e)}`),console.log()}function Ce(e){let n=Math.round(e/100*32),o=32-n;return(e>=80?B:e>=50?A:F)("\u2501".repeat(n))+p("\u2501".repeat(o))}function N(e){let t=[];e.critical>0&&t.push(F.bold(`${e.critical} critical`)),e.serious>0&&t.push(A(`${e.serious} serious`)),e.moderate>0&&t.push(p(`${e.moderate} moderate`)),e.minor>0&&t.push(p(`${e.minor} minor`)),console.log(` ${ee(String(e.total))} issues ${t.join(p(" \xB7 "))}`),console.log()}function R(e){let t=e.severity==="critical"?F("\u2717"):e.severity==="serious"?A("\u25B2"):p("\u25CF"),n=p(`${e.wcag}`),o=e.count&&e.count>1?p(` \xD7${e.count}`):"";if(console.log(` ${t} ${n}${o} ${e.problem.slice(0,72)}`),e.contrast?(console.log(` ${$("fix")} ${e.fix.slice(0,70)}`),console.log(` ${p(`${e.contrast.current}:1 \u2192 ${e.contrast.suggested}:1 (min ${e.contrast.required}:1)`)}`)):e.before&&e.after?console.log(` ${$("fix")} ${p(e.before)} ${p("\u2192")} ${B(e.after)}`):console.log(` ${$("fix")} ${e.fix.slice(0,70)}`),e.html&&e.count===1){let r=e.html.slice(0,60).replace(/\n/g," ");console.log(` ${p(r)}`)}if(e.selectors&&e.selectors.length>0&&e.count&&e.count>1){let r=e.selectors.slice(0,2).map(s=>s.slice(0,30)).join(p(", ")),i=e.count>2?p(` +${e.count-2} more`):"";console.log(` ${p(r+i)}`)}else e.selector&&console.log(` ${p(e.selector.slice(0,55))}`);console.log()}function te(e){if(e.length){for(let t of e)console.log(` ${A.bold("\u26A0 WARNING")} ${A(t)}`);console.log()}}function L(e){console.log(p(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")),e?.hasIssues&&(console.log(` ${p("Next steps:")}`),console.log(` ${$("abilyo scan "+(e.url||"<url>")+" --format json")} ${p("CI/CD output")}`),console.log(` ${$("abilyo scan "+(e.url||"<url>")+" --format html > report.html")} ${p("share report")}`),console.log()),console.log(` ${z("\u25C6")} ${p("Auto-fix at")} ${z("abilyo.com")}`),console.log()}function k(e){console.log(F(` \u2717 Error: ${e}`)),console.log()}function oe(e){console.log(JSON.stringify(e,null,2))}function W(e){console.log("severity,wcag,message,element");for(let t of e){let n=t.message.replace(/"/g,'""'),o=(t.element||"").replace(/"/g,'""');console.log(`${t.severity},${t.wcag},"${n}","${o}"`)}}var Be=Ee(Se(import.meta.url)),ie=JSON.parse(Fe($e(Be,"../package.json"),"utf-8")),h=new Ie;h.name("abilyo").description("Abilyo by WebAbility \u2014 WCAG accessibility scanner").version(ie.version);h.command("scan <url>").description("Scan a website for accessibility issues").option("-f, --format <format>","Output format: table, json, csv, sarif, html","table").option("--wcag <level>","WCAG level: A, AA, AAA","AA").option("--deep","Run a deeper scan with additional checks (Pro plan)").option("--remote","Run scan on WebAbility servers instead of locally").option("--exit","Exit with code 1 if issues found (CI mode)").option("--no-axe","Skip axe-core (use only WebAbility detectors)").option("--viewport <size>","Viewport: mobile, tablet, desktop","desktop").option("--show","Open browser and highlight issues visually").action(async(e,t)=>{if(e.startsWith("http")||(e=`https://${e}`),t.deep){let i=E();if(!i)return t.format==="table"&&m(),k("--deep requires a Pro plan. Run `abilyo login` first."),process.exit(1);try{let s=await fetch("https://api.webability.io/cli/verify",{headers:{Authorization:`Bearer ${i}`}});if(s.ok){if(!(await s.json()).active)return t.format==="table"&&m(),k("Your account does not have an active Pro plan."),console.log(a.dim(" Upgrade at https://abilyo.com/pricing")),process.exit(1)}else{if(s.status===401||s.status===403)return t.format==="table"&&m(),k("Your API key is invalid or expired. Run `abilyo login` again."),process.exit(1);console.error(a.yellow(` \u26A0 Could not verify your Pro plan (server returned ${s.status}). Continuing, but --deep may be rejected.`))}}catch{console.error(a.yellow(" \u26A0 Could not verify Pro plan status (offline). --deep analysis will be skipped without a network connection."))}}if(t.remote){await Ne(e,t);return}t.format==="table"&&m();let n=Date.now(),o=I({text:"Scanning...",prefixText:" "}).start(),r;try{r=await Y(e,{wcag:t.wcag,deep:t.deep??!1,apiKey:E()??void 0,show:t.show??!1,viewport:t.viewport,includeAxe:t.axe!==!1});let i=((Date.now()-n)/1e3).toFixed(1);if(o.succeed(`Scanned ${r.issues.length} issues in ${i}s`),t.format==="json"){let s={...r,issues:r.issues.map(({source:l,...d})=>d)};console.log(JSON.stringify(s,null,2))}else if(t.format==="csv")W(r.issues.map(s=>({severity:s.impact,wcag:s.wcag,message:s.message,element:s.selector})));else if(t.format==="sarif")console.log(JSON.stringify(Re(r),null,2));else if(t.format==="html"){let{toHtmlReport:s}=await import("@webability/core");console.log(s(r))}else{if(te(r.engineWarnings??[]),j(r.summary.total===0?100:Math.max(0,100-r.summary.total),e),N(r.summary),r.issues.length>0){let{generateDevSuggestion:s}=await import("@webability/core"),l=r.framework||"plain-css",d=[],f=new Map;for(let c of r.issues){let g=s(c,l),b=`${c.type}|${g.fix.slice(0,50)}`,x=f.get(b);x!==void 0?(d[x].count++,d[x].selectors.length<3&&d[x].selectors.push(c.selector.slice(0,50))):(f.set(b,d.length),d.push({suggestion:g,issue:c,count:1,selectors:[c.selector.slice(0,50)]}))}d.sort((c,g)=>{let b={critical:0,serious:1,moderate:2,minor:3};return(b[c.issue.impact]??2)-(b[g.issue.impact]??2)});for(let c of d.slice(0,20))R({severity:c.issue.impact,wcag:c.issue.wcag,problem:c.suggestion.problem,fix:c.suggestion.fix,count:c.count,selector:c.count===1?c.issue.selector:void 0,selectors:c.count>1?c.selectors:void 0,contrast:c.suggestion.contrast,before:c.suggestion.before,after:c.suggestion.after,html:c.issue.html});d.length>20&&(console.log(a.dim(` ... +${d.length-20} more`)),console.log())}L({hasIssues:r.issues.length>0,url:e.replace("https://","")})}t.show&&r.issues.length>0&&(console.log(a.cyan(" \u25C6 Browser open \u2014 issues highlighted. Press Enter to close.")),console.log(),await new Promise(s=>{process.stdin.resume(),process.stdin.once("data",()=>{process.stdin.pause(),s()})}),await _(r)),t.exit&&r.summary.total>0&&process.exit(1)}catch(i){o.stop(),r&&await _(r).catch(()=>{}),k(i.message),process.exit(1)}});async function Ne(e,t){let n=E();if(!n)return t.format==="table"&&m(),k("--remote requires login. Run `wa login` first."),process.exit(1);t.format==="table"&&m();let o=I({text:"Scanning (remote)...",prefixText:" "}).start();try{let r=await K(e,n,l=>{o.text=l});o.text="Fetching report...";let i=await U(r.key,n);if(o.stop(),!i)return k("Report not found"),process.exit(1);let s=[];if(i.axe?.violations)for(let l of i.axe.violations)for(let d of l.nodes||[])s.push({severity:l.impact||"moderate",wcag:l.id,message:l.description,element:d.target?.[0]});if(i.ByFunctions)for(let l of i.ByFunctions)for(let d of l.issues||[])s.push({severity:d.impact||"moderate",wcag:d.code||"",message:d.description||"",element:d.element});if(t.format==="json")oe({url:e,score:i.score,totalElements:i.totalElements,issues:s});else if(t.format==="csv")W(s);else{j(i.score||0,e),N({critical:0,serious:s.filter(l=>l.severity==="serious").length,moderate:s.filter(l=>l.severity==="moderate").length,minor:0,total:s.length});for(let l of s.slice(0,20))R({severity:l.severity,wcag:l.wcag,problem:l.message,fix:"Review this issue",selector:l.element});L()}t.exit&&s.length>0&&process.exit(1)}catch(r){o.stop(),k(r.message),process.exit(1)}}h.command("flow <url>").description("Scan a multi-page user journey \u2014 one consolidated report").option("--wcag <level>","WCAG level: A, AA, AAA","AA").action(async(e,t)=>{e.startsWith("http")||(e=`https://${e}`),m(),console.log(a.bold(" Flow Scan")),console.log(),console.log(` Starting at: ${a.cyan(e)}`),console.log(a.dim(" Click through your user journey. Close the browser when done.")),console.log();let n=0,o=await Q(e,{wcag:t.wcag,onPageScan:(s,l)=>{n++,console.log(` ${a.green("\u2713")} ${a.dim(String(n).padStart(2))} ${s.replace(/^https?:\/\//,"").slice(0,60)} ${a.dim("\u2014 "+l+" issues")}`)}});console.log(),console.log(a.bold(" \u2500\u2500\u2500 Flow Report \u2500\u2500\u2500")),console.log(),console.log(` Pages scanned: ${a.cyan(o.pages.length)}`),console.log(` Unique issues: ${a.cyan(o.uniqueIssues.length)}`),console.log(` Total duration: ${a.dim((o.totalDuration/1e3).toFixed(1)+"s")}`),console.log();let r={critical:0,serious:0,moderate:0,minor:0};o.uniqueIssues.forEach(s=>{r[s.impact]++}),N({...r,total:o.uniqueIssues.length});let i=o.uniqueIssues.slice(0,15);for(let s of i){let l=s.pageCount>1?a.yellow(` \xD7${s.pageCount} pages`):"";if(R({severity:s.impact,wcag:s.wcag,problem:s.message,fix:s.fix?.suggestedValue?`Set ${s.fix.attribute}="${s.fix.suggestedValue}"`:"Review this issue",selector:s.selector}),s.pageCount>1){console.log(a.dim(` Found on${l}:`));for(let d of s.foundOn.slice(0,3))console.log(a.dim(" "+d.replace(/^https?:\/\//,"")))}}o.uniqueIssues.length>15&&console.log(a.dim(` ... +${o.uniqueIssues.length-15} more unique issues`)),console.log(),L({hasIssues:o.uniqueIssues.length>0,url:e.replace("https://","")})});h.command("init").description("Create .webability.yml config file").action(()=>{m(),Z(),console.log()});h.command("login").description("Authenticate with your WebAbility account").option("-k, --key <key>","API key directly (for CI/CD)").action(async e=>{if(m(),e.key){T(e.key),console.log(a.green(" \u2713 API key saved.")),console.log();return}let t=I({text:"Starting login...",prefixText:" "}).start();try{let n=await fetch("https://api.webability.io/cli/device-code",{method:"POST",headers:{"Content-Type":"application/json"}});if(!n.ok){t.stop();let c="https://app.webability.io/settings/api-keys";console.log(" Open this URL to get your API key:"),console.log(),console.log(` ${a.cyan(c)}`),console.log(),console.log(` Then run: ${a.cyan("abilyo login --key YOUR_KEY")}`),console.log(),se(c);return}let{deviceCode:o,userCode:r,verificationUrl:i,expiresIn:s}=await n.json();t.stop(),console.log(` ${a.bold("Login to WebAbility")}`),console.log(),console.log(` Open: ${a.cyan(i)}`),console.log(` Code: ${a.bold.yellow(r)}`),console.log(),se(i);let l=I({text:"Waiting for approval...",prefixText:" "}).start(),d=3e3,f=Math.floor((s||300)*1e3/d);for(let c=0;c<f;c++){await new Promise(g=>setTimeout(g,d));try{let g=await fetch("https://api.webability.io/cli/device-token",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({deviceCode:o})});if(g.ok){let{token:x}=await g.json();T(x),l.succeed("Logged in!"),console.log();return}if((await g.json().catch(()=>({}))).error==="expired")return l.fail("Login expired. Run `abilyo login` again."),process.exit(1)}catch{}}l.fail("Login timed out."),process.exit(1)}catch{t.stop(),console.log(` Open: ${a.cyan("https://app.webability.io/settings/api-keys")}`),console.log(` Then: ${a.cyan("abilyo login --key YOUR_KEY")}`),console.log()}});function se(e){try{let{execFile:t}=ae("child_process"),n=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";t(n,[e],()=>{})}catch{}}h.command("whoami").description("Show current authentication status").action(()=>{let e=E();m(),e?(console.log(a.green(" Authenticated")),console.log(a.dim(` Key: ${e.slice(0,20)}...`))):(console.log(a.yellow(" Not authenticated")),console.log(a.dim(" Run `wa login` to authenticate"))),console.log()});h.command("logout").description("Remove saved API key").action(()=>{J(),m(),console.log(a.green(" Logged out. API key removed.")),console.log()});h.command("activate <code>").description("Activate Abilyo with your early access code").action(async e=>{m();let t=I({text:"Validating access code...",prefixText:" "}).start();try{if((await fetch("https://api.webability.io/cli/activate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:e.trim()})})).ok){O(e.trim()),t.succeed("Abilyo activated!"),console.log(),console.log(a.dim(" Run `abilyo scan <url>` to get started")),console.log();return}}catch{}e.trim().length>=8?(O(e.trim()),t.succeed("Abilyo activated!"),console.log(),console.log(a.dim(" Run `abilyo scan <url>` to get started")),console.log()):(t.fail("Invalid access code"),console.log(a.dim(" Request access at https://abilyo.com/early-access")),process.exit(1))});h.argument("[url]","URL to scan").action(e=>{e?h.parse(["node","abilyo","scan",e,...process.argv.slice(3)]):(m(),G()?(console.log(a.bold(" Commands:")),console.log(),console.log(` ${a.cyan("abilyo scan")} <url> Scan for accessibility issues`),console.log(` ${a.cyan("abilyo scan --deep")} <url> Deeper scan with additional checks (Pro)`),console.log(` ${a.cyan("abilyo scan --exit")} <url> CI mode \u2014 exit 1 if issues found`),console.log(` ${a.cyan("abilyo init")} Create .webability.yml config`),console.log(` ${a.cyan("abilyo login")} Authenticate with your account`),console.log(` ${a.cyan("abilyo whoami")} Show auth status`),console.log(),console.log(a.dim(" Example: abilyo scan localhost:3000")),console.log(a.dim(" Example: abilyo scan example.com --format json")),console.log(a.dim(" Example: abilyo scan example.com --deep --exit"))):(console.log(a.yellow(" Abilyo is invite-only during early access.")),console.log(),console.log(` ${a.cyan("abilyo activate")} <code> Activate with your access code`),console.log(),console.log(a.dim(" Request access at https://abilyo.com/early-access"))),console.log())});h.parse();function Re(e){let t=new Map;for(let o of e.issues)t.has(o.type)||t.set(o.type,{id:o.type,shortDescription:{text:o.message}});let n=e.engineWarnings??[];return{$schema:"https://json.schemastore.org/sarif-2.1.0.json",version:"2.1.0",runs:[{...n.length?{invocations:[{executionSuccessful:!0,toolExecutionNotifications:n.map(o=>({level:"warning",message:{text:o}}))}]}:{},tool:{driver:{name:"WebAbility",version:ie.version,informationUri:"https://webability.io",rules:Array.from(t.values())}},results:e.issues.map(o=>{let i={startLine:Number.isInteger(o.line)&&o.line>0?o.line:1,snippet:{text:o.html||o.selector}};return Number.isInteger(o.column)&&o.column>0&&(i.startColumn=o.column),{ruleId:o.type,level:o.impact==="critical"||o.impact==="serious"?"error":"warning",message:{text:o.message},locations:[{physicalLocation:{artifactLocation:{uri:e.url},region:i}}]}})}]}}
|