@webability/cli 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/cli.js +871 -122
- package/package.json +9 -6
package/README.md
CHANGED
|
@@ -47,6 +47,12 @@ threshold:
|
|
|
47
47
|
serious: 5
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
## Links
|
|
51
|
+
|
|
52
|
+
- [WebAbility](https://webability.io) — AI-powered web accessibility platform
|
|
53
|
+
- [Abilyo](https://abilyo.com) — Developer tools for accessibility testing
|
|
54
|
+
- [Documentation](https://abilyo.com/docs)
|
|
55
|
+
|
|
50
56
|
## License
|
|
51
57
|
|
|
52
58
|
MIT
|
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
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
|
+
});
|
|
2
8
|
|
|
3
9
|
// src/cli.ts
|
|
4
10
|
import { Command } from "commander";
|
|
@@ -67,15 +73,6 @@ async function getReport(r2Key, apiKey) {
|
|
|
67
73
|
);
|
|
68
74
|
return fetchReportByR2Key;
|
|
69
75
|
}
|
|
70
|
-
async function login(email, password) {
|
|
71
|
-
const { login: result } = await graphql(
|
|
72
|
-
`mutation($email: String!, $password: String!) { login(email: $email, password: $password) { token } }`,
|
|
73
|
-
{ email, password },
|
|
74
|
-
"public"
|
|
75
|
-
// login doesn't need a key
|
|
76
|
-
);
|
|
77
|
-
return result.token;
|
|
78
|
-
}
|
|
79
76
|
|
|
80
77
|
// src/config.ts
|
|
81
78
|
import Conf from "conf";
|
|
@@ -107,25 +104,640 @@ function clearConfig() {
|
|
|
107
104
|
}
|
|
108
105
|
|
|
109
106
|
// src/local-scan.ts
|
|
110
|
-
import { scan as scan2 } from "@webability/core";
|
|
107
|
+
import { scan as scan2, detectFramework } from "@webability/core";
|
|
111
108
|
var WCAG_TAG_MAP = {
|
|
112
109
|
"A": ["wcag2a"],
|
|
113
110
|
"AA": ["wcag2a", "wcag2aa", "wcag21aa", "wcag22aa"],
|
|
114
111
|
"AAA": ["wcag2a", "wcag2aa", "wcag2aaa", "wcag21aa", "wcag22aa"]
|
|
115
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 `
|
|
141
|
+
(function() {
|
|
142
|
+
const issues = ${serialized};
|
|
143
|
+
const colors = ${JSON.stringify(IMPACT_COLORS)};
|
|
144
|
+
const icons = ${JSON.stringify(IMPACT_ICONS)};
|
|
145
|
+
const API_KEY = ${JSON.stringify(apiKey || "")};
|
|
146
|
+
const API_URL = 'https://api.webability.io';
|
|
147
|
+
const FRAMEWORK = ${JSON.stringify(framework || "plain-css")};
|
|
148
|
+
const activeFilters = { critical: true, serious: true, moderate: true, minor: true };
|
|
149
|
+
const overlays = [];
|
|
150
|
+
|
|
151
|
+
function h(tag, props, children) {
|
|
152
|
+
const el = document.createElement(tag);
|
|
153
|
+
if (props) {
|
|
154
|
+
for (const [k, v] of Object.entries(props)) {
|
|
155
|
+
if (k === 'style' && typeof v === 'object') Object.assign(el.style, v);
|
|
156
|
+
else if (k === 'className') el.className = v;
|
|
157
|
+
else if (k.startsWith('on')) el.addEventListener(k.slice(2).toLowerCase(), v);
|
|
158
|
+
else el.setAttribute(k, v);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (typeof children === 'string') el.textContent = children;
|
|
162
|
+
else if (Array.isArray(children)) children.forEach(c => { if (c) el.appendChild(c); });
|
|
163
|
+
return el;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const style = document.createElement('style');
|
|
167
|
+
style.textContent = [
|
|
168
|
+
'@keyframes abilyo-reveal{from{opacity:0;transform:scale(.97)}to{opacity:1;transform:scale(1)}}',
|
|
169
|
+
'.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}',
|
|
170
|
+
'.abilyo-overlay:hover{outline-width:3px;outline-offset:1px}',
|
|
171
|
+
'.abilyo-overlay.abilyo-active{outline:3px solid #445AE7!important;outline-offset:2px}',
|
|
172
|
+
'.abilyo-overlay.abilyo-hidden{opacity:0;pointer-events:none}',
|
|
173
|
+
'.abilyo-overlay.abilyo-dimmed{opacity:.12;filter:blur(1px)}',
|
|
174
|
+
'.abilyo-spotlight{position:fixed;inset:0;z-index:2147483639;background:rgba(0,0,0,.35);backdrop-filter:blur(2px);pointer-events:none;transition:opacity .2s}',
|
|
175
|
+
'.abilyo-label{position:absolute;top:-22px;left:-2px;font:bold 11px/14px system-ui,sans-serif;color:#fff;padding:2px 6px;border-radius:3px 3px 0 0;white-space:nowrap;pointer-events:none;z-index:2147483641}',
|
|
176
|
+
'#abilyo-panel{position:fixed;top:12px;right:12px;z-index:2147483647;width:380px;max-height:calc(100vh - 24px);background:#fff;border-radius:12px;box-shadow:0 8px 32px rgba(0,0,0,.18),0 0 0 1px rgba(0,0,0,.05);font:13px system-ui,-apple-system,sans-serif;color:#1e1e1e;display:flex;flex-direction:column;overflow:hidden}',
|
|
177
|
+
'#abilyo-panel *{box-sizing:border-box}',
|
|
178
|
+
'.ap-header{display:flex;align-items:center;gap:10px;padding:14px 16px;border-bottom:1px solid #eee;flex-shrink:0}',
|
|
179
|
+
'.ap-logo{width:20px;height:20px;background:#445AE7;border-radius:4px;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:800;font-size:12px}',
|
|
180
|
+
'.ap-title{font-weight:700;font-size:14px;flex:1}',
|
|
181
|
+
'.ap-count{font-size:12px;color:#fff;background:#445AE7;padding:2px 8px;border-radius:10px;font-weight:600}',
|
|
182
|
+
'.ap-close{width:28px;height:28px;border:none;background:#f3f3f3;border-radius:6px;cursor:pointer;font-size:14px;color:#666;display:flex;align-items:center;justify-content:center}',
|
|
183
|
+
'.ap-close:hover{background:#e5e5e5}',
|
|
184
|
+
'.ap-toolbar{display:flex;gap:6px;padding:8px 16px;border-bottom:1px solid #eee;flex-shrink:0}',
|
|
185
|
+
'.ap-tbtn{padding:4px 10px;border-radius:6px;border:1px solid #ddd;background:#fff;cursor:pointer;font:500 11px system-ui;color:#555;transition:all .15s;display:flex;align-items:center;gap:4px}',
|
|
186
|
+
'.ap-tbtn:hover{background:#f5f5f5;border-color:#bbb}',
|
|
187
|
+
'.ap-tbtn.active{background:#445AE7;color:#fff;border-color:#445AE7}',
|
|
188
|
+
'.ap-tbtn-icon{font-size:13px}',
|
|
189
|
+
'.ap-score{margin-left:auto;font:700 13px system-ui;color:#445AE7}',
|
|
190
|
+
'.ap-filters{display:flex;gap:6px;padding:10px 16px;border-bottom:1px solid #eee;flex-shrink:0}',
|
|
191
|
+
'.ap-fbtn{display:flex;align-items:center;gap:4px;padding:4px 10px;border-radius:6px;border:1.5px solid #ddd;background:#fff;cursor:pointer;font:600 12px system-ui;transition:all .15s}',
|
|
192
|
+
'.ap-fbtn.active{border-color:currentColor;background:currentColor;color:#fff!important}',
|
|
193
|
+
'.ap-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}',
|
|
194
|
+
'.ap-list{flex:1;overflow-y:auto;padding:4px 0;scrollbar-width:thin}',
|
|
195
|
+
'.ap-row{display:flex;align-items:flex-start;gap:10px;padding:10px 16px;cursor:pointer;border-bottom:1px solid #f5f5f5;transition:background .1s}',
|
|
196
|
+
'.ap-row:hover{background:#f8f9ff}',
|
|
197
|
+
'.ap-row.active{background:#eef1ff}',
|
|
198
|
+
'.ap-icon{width:22px;height:22px;border-radius:5px;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:800;color:#fff;flex-shrink:0;margin-top:1px}',
|
|
199
|
+
'.ap-content{flex:1;min-width:0}',
|
|
200
|
+
'.ap-wcag{font-size:11px;font-weight:700;color:#445AE7;background:#eef1ff;padding:1px 6px;border-radius:3px;margin-right:6px}',
|
|
201
|
+
'.ap-sev{font-size:12px;font-weight:600}',
|
|
202
|
+
'.ap-msg{font-size:12px;color:#555;margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
|
|
203
|
+
'.ap-fix{font-size:11px;color:#16a34a;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
|
|
204
|
+
'#ap-detail{display:none;flex-shrink:0;border-top:1px solid #eee;padding:14px 16px;background:#fafbfc;max-height:200px;overflow-y:auto}',
|
|
205
|
+
'.apd-head{display:flex;align-items:center;gap:8px;margin-bottom:8px}',
|
|
206
|
+
'.apd-wcag{font-weight:700;color:#445AE7;font-size:14px}',
|
|
207
|
+
'.apd-badge{font-size:11px;font-weight:700;padding:2px 8px;border-radius:4px;color:#fff;text-transform:uppercase}',
|
|
208
|
+
'.apd-msg{font-size:13px;color:#333;line-height:1.4;margin-bottom:6px}',
|
|
209
|
+
'.apd-fix{font-size:12px;color:#16a34a;background:#f0fdf4;padding:6px 10px;border-radius:6px;margin-top:6px}',
|
|
210
|
+
'.apd-sel{font-size:11px;color:#888;font-family:monospace;margin-top:6px;word-break:break-all}',
|
|
211
|
+
'.apd-actions{display:flex;gap:8px;margin-top:10px}',
|
|
212
|
+
'.apd-btn{padding:6px 14px;border-radius:6px;border:none;cursor:pointer;font:600 12px system-ui;transition:all .15s}',
|
|
213
|
+
'.apd-preview{background:#eef1ff;color:#445AE7}',
|
|
214
|
+
'.apd-preview:hover{background:#dde5ff}',
|
|
215
|
+
'.apd-preview.active{background:#445AE7;color:#fff}',
|
|
216
|
+
'.apd-apply{background:#16a34a;color:#fff}',
|
|
217
|
+
'.apd-apply:hover{background:#15803d}',
|
|
218
|
+
'.apd-apply.applied{background:#6b7280;cursor:default}',
|
|
219
|
+
'.apd-before-after{display:flex;gap:12px;margin-top:8px;font-size:12px}',
|
|
220
|
+
'.apd-ba-col{flex:1;padding:8px;border-radius:6px}',
|
|
221
|
+
'.apd-ba-before{background:#fef2f2;border:1px solid #fecaca}',
|
|
222
|
+
'.apd-ba-after{background:#f0fdf4;border:1px solid #bbf7d0}',
|
|
223
|
+
'.apd-ba-label{font-weight:700;font-size:10px;text-transform:uppercase;letter-spacing:.05em;margin-bottom:4px;color:#888}',
|
|
224
|
+
'.apd-ba-value{font-family:monospace;font-size:11px;word-break:break-all}',
|
|
225
|
+
'.apd-ai{background:linear-gradient(135deg,#8b5cf6,#6366f1);color:#fff;position:relative;overflow:hidden}',
|
|
226
|
+
'.apd-ai:hover{background:linear-gradient(135deg,#7c3aed,#4f46e5)}',
|
|
227
|
+
'.apd-ai:disabled{opacity:.7;cursor:wait}',
|
|
228
|
+
'.apd-ai-sparkle{display:inline-block;margin-right:4px}',
|
|
229
|
+
'@keyframes apd-shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}',
|
|
230
|
+
'.apd-ai-loading{background:linear-gradient(90deg,#8b5cf6 25%,#c4b5fd 50%,#8b5cf6 75%);background-size:200% 100%;animation:apd-shimmer 1.5s infinite}',
|
|
231
|
+
'.apd-ai-result{margin-top:8px;padding:10px;background:linear-gradient(135deg,#f5f3ff,#ede9fe);border:1px solid #c4b5fd;border-radius:8px}',
|
|
232
|
+
'.apd-ai-explain{font-size:12px;color:#5b21b6;margin-bottom:6px;line-height:1.4}',
|
|
233
|
+
'.apd-ai-code{font-size:11px;font-family:monospace;color:#6d28d9;background:#ede9fe;padding:4px 8px;border-radius:4px;word-break:break-all}',
|
|
234
|
+
'#abilyo-pill{display:none;position:fixed;top:12px;right:12px;z-index:2147483647;background:#445AE7;color:#fff;border-radius:10px;padding:8px 14px;cursor:pointer;font:700 13px/1 system-ui;box-shadow:0 4px 16px rgba(68,90,231,.35);transition:transform .15s}',
|
|
235
|
+
'#abilyo-pill:hover{transform:scale(1.05)}',
|
|
236
|
+
].join('\\n');
|
|
237
|
+
document.head.appendChild(style);
|
|
238
|
+
|
|
239
|
+
// --- Create overlays with staggered reveal ---
|
|
240
|
+
const seen = new Set();
|
|
241
|
+
const elements = [];
|
|
242
|
+
let visibleIdx = 0;
|
|
243
|
+
for (const issue of issues) {
|
|
244
|
+
if (seen.has(issue.selector)) { overlays.push(null); elements.push(null); continue; }
|
|
245
|
+
seen.add(issue.selector);
|
|
246
|
+
try {
|
|
247
|
+
const el = document.querySelector(issue.selector);
|
|
248
|
+
if (!el) { overlays.push(null); elements.push(null); continue; }
|
|
249
|
+
const rect = el.getBoundingClientRect();
|
|
250
|
+
if (rect.width === 0 && rect.height === 0) { overlays.push(null); elements.push(null); continue; }
|
|
251
|
+
|
|
252
|
+
const color = colors[issue.impact] || '#ef4444';
|
|
253
|
+
const delay = Math.min(visibleIdx * 30, 600);
|
|
254
|
+
const overlay = h('div', {
|
|
255
|
+
className: 'abilyo-overlay',
|
|
256
|
+
'data-impact': issue.impact,
|
|
257
|
+
'data-id': String(issue.id),
|
|
258
|
+
style: { top: (rect.top+window.scrollY)+'px', left: (rect.left+window.scrollX)+'px', width: rect.width+'px', height: rect.height+'px', outlineColor: color, background: color+'0d', animationDelay: delay+'ms' },
|
|
259
|
+
onClick: () => selectIssue(issue.id),
|
|
260
|
+
}, [
|
|
261
|
+
h('div', { className: 'abilyo-label', style: { background: color, animationDelay: delay+'ms' } }, icons[issue.impact]+' '+issue.wcag)
|
|
262
|
+
]);
|
|
263
|
+
document.body.appendChild(overlay);
|
|
264
|
+
overlays.push(overlay);
|
|
265
|
+
elements.push(el);
|
|
266
|
+
visibleIdx++;
|
|
267
|
+
} catch { overlays.push(null); elements.push(null); }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// --- Reposition overlays on scroll/resize ---
|
|
271
|
+
let repositionRaf = 0;
|
|
272
|
+
function repositionOverlays() {
|
|
273
|
+
for (let i = 0; i < overlays.length; i++) {
|
|
274
|
+
const ov = overlays[i]; const el = elements[i];
|
|
275
|
+
if (!ov || !el) continue;
|
|
276
|
+
const rect = el.getBoundingClientRect();
|
|
277
|
+
ov.style.top = (rect.top + window.scrollY) + 'px';
|
|
278
|
+
ov.style.left = (rect.left + window.scrollX) + 'px';
|
|
279
|
+
ov.style.width = rect.width + 'px';
|
|
280
|
+
ov.style.height = rect.height + 'px';
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function scheduleReposition() {
|
|
284
|
+
cancelAnimationFrame(repositionRaf);
|
|
285
|
+
repositionRaf = requestAnimationFrame(repositionOverlays);
|
|
286
|
+
}
|
|
287
|
+
window.addEventListener('scroll', scheduleReposition, { passive: true });
|
|
288
|
+
window.addEventListener('resize', scheduleReposition);
|
|
289
|
+
|
|
290
|
+
// --- Counts ---
|
|
291
|
+
const counts = { critical: 0, serious: 0, moderate: 0, minor: 0 };
|
|
292
|
+
issues.forEach(i => counts[i.impact]++);
|
|
293
|
+
|
|
294
|
+
// --- Dedup for list ---
|
|
295
|
+
const deduped = [];
|
|
296
|
+
const seenList = new Set();
|
|
297
|
+
for (const issue of issues) {
|
|
298
|
+
if (seenList.has(issue.selector)) continue;
|
|
299
|
+
seenList.add(issue.selector);
|
|
300
|
+
deduped.push(issue);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// --- Build panel ---
|
|
304
|
+
const listEl = h('div', { className: 'ap-list', id: 'ap-list' });
|
|
305
|
+
const detailEl = h('div', { id: 'ap-detail' });
|
|
306
|
+
const filtersEl = h('div', { className: 'ap-filters' });
|
|
307
|
+
const toolbarEl = h('div', { className: 'ap-toolbar' });
|
|
308
|
+
|
|
309
|
+
// Toggle overlays button
|
|
310
|
+
let overlaysVisible = true;
|
|
311
|
+
const toggleBtn = h('button', { className: 'ap-tbtn active', onClick: () => {
|
|
312
|
+
overlaysVisible = !overlaysVisible;
|
|
313
|
+
toggleBtn.classList.toggle('active', overlaysVisible);
|
|
314
|
+
document.querySelectorAll('.abilyo-overlay').forEach(o => { (o).style.display = overlaysVisible ? '' : 'none'; });
|
|
315
|
+
} }, [h('span', { className: 'ap-tbtn-icon' }, '\\u25CF'), document.createTextNode('Overlays')]);
|
|
316
|
+
toolbarEl.appendChild(toggleBtn);
|
|
317
|
+
|
|
318
|
+
// Score display
|
|
319
|
+
const score = issues.length === 0 ? 100 : Math.max(0, 100 - issues.length);
|
|
320
|
+
const scoreColor = score >= 90 ? '#16a34a' : score >= 50 ? '#eab308' : '#ef4444';
|
|
321
|
+
toolbarEl.appendChild(h('div', { className: 'ap-score', style: { color: scoreColor } }, score + '%'));
|
|
322
|
+
|
|
323
|
+
const pill = h('div', { id: 'abilyo-pill', onClick: () => { panel.style.display = 'flex'; pill.style.display = 'none'; } }, 'A \\u00B7 ' + issues.length + ' issues');
|
|
324
|
+
|
|
325
|
+
const minBtn = h('button', { className: 'ap-close', title: 'Minimize', onClick: () => { panel.style.display = 'none'; pill.style.display = 'block'; } }, '\\u2014');
|
|
326
|
+
|
|
327
|
+
const panel = h('div', { id: 'abilyo-panel' }, [
|
|
328
|
+
h('div', { className: 'ap-header' }, [
|
|
329
|
+
h('div', { className: 'ap-logo' }, 'A'),
|
|
330
|
+
h('div', { className: 'ap-title' }, 'Abilyo'),
|
|
331
|
+
h('div', { className: 'ap-count' }, String(issues.length) + ' issues'),
|
|
332
|
+
minBtn,
|
|
333
|
+
]),
|
|
334
|
+
toolbarEl,
|
|
335
|
+
filtersEl,
|
|
336
|
+
listEl,
|
|
337
|
+
detailEl,
|
|
338
|
+
]);
|
|
339
|
+
document.body.appendChild(panel);
|
|
340
|
+
document.body.appendChild(pill);
|
|
341
|
+
|
|
342
|
+
// --- Filter buttons ---
|
|
343
|
+
for (const sev of ['critical','serious','moderate','minor']) {
|
|
344
|
+
if (counts[sev] === 0) continue;
|
|
345
|
+
const dot = h('span', { className: 'ap-dot', style: { background: '#fff' } });
|
|
346
|
+
const btn = h('button', {
|
|
347
|
+
className: 'ap-fbtn active',
|
|
348
|
+
style: { color: colors[sev] },
|
|
349
|
+
title: sev,
|
|
350
|
+
onClick: () => {
|
|
351
|
+
activeFilters[sev] = !activeFilters[sev];
|
|
352
|
+
btn.classList.toggle('active', activeFilters[sev]);
|
|
353
|
+
dot.style.background = activeFilters[sev] ? '#fff' : colors[sev];
|
|
354
|
+
applyFilters();
|
|
355
|
+
},
|
|
356
|
+
}, [dot, h('span', { style: { fontWeight: '700' } }, String(counts[sev]))]);
|
|
357
|
+
filtersEl.appendChild(btn);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// --- Rendering ---
|
|
361
|
+
let activeId = -1;
|
|
362
|
+
|
|
363
|
+
function renderList() {
|
|
364
|
+
listEl.replaceChildren();
|
|
365
|
+
for (const issue of deduped) {
|
|
366
|
+
if (!activeFilters[issue.impact]) continue;
|
|
367
|
+
const contentChildren = [
|
|
368
|
+
h('span', { className: 'ap-wcag' }, issue.wcag),
|
|
369
|
+
h('span', { className: 'ap-sev' }, issue.impact.toUpperCase()),
|
|
370
|
+
h('div', { className: 'ap-msg' }, issue.message),
|
|
371
|
+
];
|
|
372
|
+
if (issue.fixDesc) contentChildren.push(h('div', { className: 'ap-fix' }, 'Fix: ' + issue.fixDesc));
|
|
373
|
+
|
|
374
|
+
const row = h('div', {
|
|
375
|
+
className: 'ap-row' + (issue.id === activeId ? ' active' : ''),
|
|
376
|
+
onClick: () => selectIssue(issue.id),
|
|
377
|
+
onMouseenter: () => { const o = overlays[issue.id]; if (o) { o.style.outlineWidth = '3px'; o.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } },
|
|
378
|
+
onMouseleave: () => { const o = overlays[issue.id]; if (o && issue.id !== activeId) o.style.outlineWidth = '2px'; },
|
|
379
|
+
}, [
|
|
380
|
+
h('div', { className: 'ap-icon', style: { background: colors[issue.impact] } }, icons[issue.impact]),
|
|
381
|
+
h('div', { className: 'ap-content' }, contentChildren),
|
|
382
|
+
]);
|
|
383
|
+
listEl.appendChild(row);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const appliedFixes = new Set();
|
|
388
|
+
const previewState = { id: -1, attr: '', hadAttr: false, original: '' };
|
|
389
|
+
|
|
390
|
+
function revertPreview() {
|
|
391
|
+
if (previewState.id < 0) return;
|
|
392
|
+
const prevIssue = issues[previewState.id];
|
|
393
|
+
const prevEl = prevIssue ? document.querySelector(prevIssue.selector) : null;
|
|
394
|
+
if (prevEl && previewState.attr) {
|
|
395
|
+
if (previewState.hadAttr) prevEl.setAttribute(previewState.attr, previewState.original);
|
|
396
|
+
else prevEl.removeAttribute(previewState.attr);
|
|
397
|
+
}
|
|
398
|
+
previewState.id = -1; previewState.attr = ''; previewState.hadAttr = false; previewState.original = '';
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function previewFix(issue) {
|
|
402
|
+
const el = document.querySelector(issue.selector);
|
|
403
|
+
if (!el) return false;
|
|
404
|
+
if (previewState.id === issue.id) { revertPreview(); return false; }
|
|
405
|
+
revertPreview();
|
|
406
|
+
const attr = issue.fixAttr || 'style';
|
|
407
|
+
previewState.attr = attr;
|
|
408
|
+
previewState.hadAttr = el.hasAttribute(attr);
|
|
409
|
+
previewState.original = el.getAttribute(attr) || '';
|
|
410
|
+
previewState.id = issue.id;
|
|
411
|
+
if (attr === 'style') {
|
|
412
|
+
el.setAttribute('style', (previewState.original ? previewState.original + ';' : '') + issue.fixVal);
|
|
413
|
+
} else {
|
|
414
|
+
el.setAttribute(attr, issue.fixVal);
|
|
415
|
+
}
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function applyFix(issue) {
|
|
420
|
+
if (appliedFixes.has(issue.id)) return;
|
|
421
|
+
const el = document.querySelector(issue.selector);
|
|
422
|
+
if (!el) return;
|
|
423
|
+
if (issue.fixAttr === 'style') {
|
|
424
|
+
const cur = el.getAttribute('style') || '';
|
|
425
|
+
el.setAttribute('style', (cur ? cur + ';' : '') + issue.fixVal);
|
|
426
|
+
} else if (issue.fixAttr) {
|
|
427
|
+
el.setAttribute(issue.fixAttr, issue.fixVal);
|
|
428
|
+
}
|
|
429
|
+
appliedFixes.add(issue.id);
|
|
430
|
+
previewState.id = -1;
|
|
431
|
+
const ov = overlays[issue.id];
|
|
432
|
+
if (ov) { ov.style.outlineColor = '#16a34a'; ov.querySelector('.abilyo-label').style.background = '#16a34a'; ov.querySelector('.abilyo-label').textContent = '\\u2713 Fixed'; }
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function selectIssue(id) {
|
|
436
|
+
activeId = id;
|
|
437
|
+
const issue = issues[id];
|
|
438
|
+
if (!issue) return;
|
|
439
|
+
|
|
440
|
+
if (previewState.id >= 0 && previewState.id !== id) revertPreview();
|
|
441
|
+
|
|
442
|
+
document.querySelectorAll('.abilyo-overlay').forEach(o => {
|
|
443
|
+
o.classList.remove('abilyo-active');
|
|
444
|
+
o.classList.add('abilyo-dimmed');
|
|
445
|
+
});
|
|
446
|
+
const ov = overlays[id];
|
|
447
|
+
if (ov) {
|
|
448
|
+
ov.classList.remove('abilyo-dimmed');
|
|
449
|
+
ov.classList.add('abilyo-active');
|
|
450
|
+
ov.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
detailEl.style.display = 'block';
|
|
454
|
+
detailEl.replaceChildren();
|
|
455
|
+
detailEl.appendChild(h('div', { className: 'apd-head' }, [
|
|
456
|
+
h('span', { className: 'apd-wcag' }, 'WCAG ' + issue.wcag),
|
|
457
|
+
h('span', { className: 'apd-badge', style: { background: colors[issue.impact] } }, issue.impact),
|
|
458
|
+
]));
|
|
459
|
+
detailEl.appendChild(h('div', { className: 'apd-msg' }, issue.message));
|
|
460
|
+
|
|
461
|
+
if (issue.canApply && !appliedFixes.has(issue.id)) {
|
|
462
|
+
const ba = h('div', { className: 'apd-before-after' }, [
|
|
463
|
+
h('div', { className: 'apd-ba-col apd-ba-before' }, [
|
|
464
|
+
h('div', { className: 'apd-ba-label' }, 'Before'),
|
|
465
|
+
h('div', { className: 'apd-ba-value' }, issue.fixCur || '(none)'),
|
|
466
|
+
]),
|
|
467
|
+
h('div', { className: 'apd-ba-col apd-ba-after' }, [
|
|
468
|
+
h('div', { className: 'apd-ba-label' }, 'After'),
|
|
469
|
+
h('div', { className: 'apd-ba-value' }, issue.fixVal),
|
|
470
|
+
]),
|
|
471
|
+
]);
|
|
472
|
+
detailEl.appendChild(ba);
|
|
473
|
+
|
|
474
|
+
const previewBtn = h('button', { className: 'apd-btn apd-preview' }, '\\uD83D\\uDD0D Preview');
|
|
475
|
+
const applyBtn = h('button', { className: 'apd-btn apd-apply' }, '\\u2713 Apply Fix');
|
|
476
|
+
|
|
477
|
+
previewBtn.addEventListener('click', () => {
|
|
478
|
+
const on = previewFix(issue);
|
|
479
|
+
previewBtn.classList.toggle('active', on);
|
|
480
|
+
previewBtn.textContent = on ? '\\u21A9 Revert' : '\\uD83D\\uDD0D Preview';
|
|
481
|
+
});
|
|
482
|
+
applyBtn.addEventListener('click', () => {
|
|
483
|
+
applyFix(issue);
|
|
484
|
+
applyBtn.classList.add('applied');
|
|
485
|
+
applyBtn.textContent = '\\u2713 Applied';
|
|
486
|
+
previewBtn.style.display = 'none';
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
detailEl.appendChild(h('div', { className: 'apd-actions' }, [previewBtn, applyBtn]));
|
|
490
|
+
} else if (appliedFixes.has(issue.id)) {
|
|
491
|
+
detailEl.appendChild(h('div', { className: 'apd-fix' }, '\\u2713 Fix applied'));
|
|
492
|
+
} else if (issue.fixDesc) {
|
|
493
|
+
detailEl.appendChild(h('div', { className: 'apd-fix' }, '\\uD83D\\uDCA1 ' + issue.fixDesc));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// AI Fix button \u2014 available to everyone, no login needed
|
|
497
|
+
if (!appliedFixes.has(issue.id)) {
|
|
498
|
+
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
|
+
]);
|
|
503
|
+
aiBtn.addEventListener('click', async () => {
|
|
504
|
+
aiBtn.disabled = true;
|
|
505
|
+
aiBtn.classList.add('apd-ai-loading');
|
|
506
|
+
aiBtn.textContent = '\\u2728 Generating...';
|
|
507
|
+
try {
|
|
508
|
+
const el = document.querySelector(issue.selector);
|
|
509
|
+
const elHtml = el ? el.outerHTML.slice(0, 600) : '';
|
|
510
|
+
const parent = el && el.parentElement ? el.parentElement.outerHTML.slice(0, 400) : '';
|
|
511
|
+
// Detect brand colors from the page
|
|
512
|
+
const allEls = document.querySelectorAll('a, button, h1, h2, .bg-primary, [class*="brand"], [class*="primary"]');
|
|
513
|
+
const colorSet = new Set();
|
|
514
|
+
allEls.forEach(e => {
|
|
515
|
+
const s = getComputedStyle(e);
|
|
516
|
+
if (s.color && s.color !== 'rgb(0, 0, 0)') colorSet.add(s.color);
|
|
517
|
+
if (s.backgroundColor && s.backgroundColor !== 'rgba(0, 0, 0, 0)') colorSet.add(s.backgroundColor);
|
|
518
|
+
});
|
|
519
|
+
const brandCols = Array.from(colorSet).slice(0, 8);
|
|
520
|
+
|
|
521
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
522
|
+
if (API_KEY) headers['Authorization'] = 'Bearer ' + API_KEY;
|
|
523
|
+
const res = await fetch(API_URL + '/cli/ai-fix', {
|
|
524
|
+
method: 'POST', headers,
|
|
525
|
+
body: JSON.stringify({
|
|
526
|
+
issue: { type: issue.wcag, wcag: issue.wcag, impact: issue.impact, message: issue.message, selector: issue.selector, fixCur: issue.fixCur },
|
|
527
|
+
html: elHtml, context: parent, url: window.location.href,
|
|
528
|
+
framework: FRAMEWORK, brandColors: brandCols,
|
|
529
|
+
}),
|
|
530
|
+
});
|
|
531
|
+
if (!res.ok) throw new Error('API error');
|
|
532
|
+
const data = await res.json();
|
|
533
|
+
aiBtn.classList.remove('apd-ai-loading');
|
|
534
|
+
aiBtn.textContent = '\\u2728 AI Fix';
|
|
535
|
+
aiBtn.disabled = false;
|
|
536
|
+
|
|
537
|
+
const alts = data.alternatives || [];
|
|
538
|
+
aiResultContainer.replaceChildren();
|
|
539
|
+
|
|
540
|
+
for (let ai = 0; ai < alts.length; ai++) {
|
|
541
|
+
const alt = alts[ai];
|
|
542
|
+
const altBox = h('div', { className: 'apd-ai-result', style: ai > 0 ? { marginTop: '6px' } : {} }, [
|
|
543
|
+
h('div', { style: { fontWeight: '700', fontSize: '12px', color: '#5b21b6', marginBottom: '4px' } }, alt.label || 'Option ' + (ai+1)),
|
|
544
|
+
h('div', { className: 'apd-ai-explain' }, alt.explanation || ''),
|
|
545
|
+
h('div', { className: 'apd-ai-code' }, alt.frameworkCode || (alt.attribute + '="' + alt.value + '"')),
|
|
546
|
+
]);
|
|
547
|
+
|
|
548
|
+
const altActions = h('div', { className: 'apd-actions', style: { marginTop: '6px' } });
|
|
549
|
+
const pBtn = h('button', { className: 'apd-btn apd-preview', style: { fontSize: '11px', padding: '4px 10px' } }, '\\uD83D\\uDD0D Preview');
|
|
550
|
+
const aBtn = h('button', { className: 'apd-btn apd-apply', style: { fontSize: '11px', padding: '4px 10px' } }, '\\u2713 Apply');
|
|
551
|
+
|
|
552
|
+
pBtn.addEventListener('click', () => {
|
|
553
|
+
const t = document.querySelector(issue.selector);
|
|
554
|
+
if (!t) return;
|
|
555
|
+
if (previewState.id === issue.id) { revertPreview(); pBtn.textContent = '\\uD83D\\uDD0D Preview'; pBtn.classList.remove('active'); return; }
|
|
556
|
+
revertPreview();
|
|
557
|
+
const previewAttr = (alt.attribute === 'style' || alt.previewCss) ? 'style' : (alt.attribute || 'style');
|
|
558
|
+
previewState.attr = previewAttr;
|
|
559
|
+
previewState.hadAttr = t.hasAttribute(previewAttr);
|
|
560
|
+
previewState.original = t.getAttribute(previewAttr) || '';
|
|
561
|
+
previewState.id = issue.id;
|
|
562
|
+
if (alt.attribute === 'style') t.setAttribute('style', (previewState.original ? previewState.original + ';' : '') + alt.value);
|
|
563
|
+
else if (alt.previewCss) t.setAttribute('style', (previewState.original ? previewState.original + ';' : '') + alt.previewCss);
|
|
564
|
+
else if (alt.attribute) t.setAttribute(alt.attribute, alt.value);
|
|
565
|
+
pBtn.textContent = '\\u21A9 Revert'; pBtn.classList.add('active');
|
|
566
|
+
});
|
|
567
|
+
aBtn.addEventListener('click', () => {
|
|
568
|
+
const t = document.querySelector(issue.selector);
|
|
569
|
+
if (!t) return;
|
|
570
|
+
if (alt.attribute === 'style') { const c = t.getAttribute('style') || ''; t.setAttribute('style', (c ? c + ';' : '') + alt.value); }
|
|
571
|
+
else if (alt.attribute) t.setAttribute(alt.attribute, alt.value);
|
|
572
|
+
appliedFixes.add(issue.id); previewState.id = -1;
|
|
573
|
+
const o = overlays[issue.id];
|
|
574
|
+
if (o) { o.style.outlineColor = '#8b5cf6'; const l = o.querySelector('.abilyo-label'); if (l) { l.style.background = '#8b5cf6'; l.textContent = '\\u2728 AI Fixed'; } }
|
|
575
|
+
aBtn.classList.add('applied'); aBtn.textContent = '\\u2713 Applied'; pBtn.style.display = 'none';
|
|
576
|
+
});
|
|
577
|
+
altActions.appendChild(pBtn);
|
|
578
|
+
altActions.appendChild(aBtn);
|
|
579
|
+
altBox.appendChild(altActions);
|
|
580
|
+
aiResultContainer.appendChild(altBox);
|
|
581
|
+
}
|
|
582
|
+
} catch {
|
|
583
|
+
aiBtn.classList.remove('apd-ai-loading');
|
|
584
|
+
aiBtn.textContent = '\\u2728 AI Fix (retry)';
|
|
585
|
+
aiBtn.disabled = false;
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
detailEl.appendChild(h('div', { className: 'apd-actions' }, [aiBtn]));
|
|
589
|
+
detailEl.appendChild(aiResultContainer);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
detailEl.appendChild(h('div', { className: 'apd-sel' }, issue.selector));
|
|
593
|
+
renderList();
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function clearSpotlight() {
|
|
597
|
+
activeId = -1;
|
|
598
|
+
document.querySelectorAll('.abilyo-overlay').forEach(o => { o.classList.remove('abilyo-active','abilyo-dimmed'); });
|
|
599
|
+
detailEl.style.display = 'none';
|
|
600
|
+
renderList();
|
|
601
|
+
}
|
|
602
|
+
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') clearSpotlight(); });
|
|
603
|
+
|
|
604
|
+
function applyFilters() {
|
|
605
|
+
overlays.forEach((ov, i) => {
|
|
606
|
+
if (!ov) return;
|
|
607
|
+
const impact = issues[i] && issues[i].impact;
|
|
608
|
+
ov.classList.toggle('abilyo-hidden', !activeFilters[impact]);
|
|
609
|
+
});
|
|
610
|
+
renderList();
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
renderList();
|
|
614
|
+
})();
|
|
615
|
+
`;
|
|
616
|
+
}
|
|
116
617
|
async function localScan(url, options) {
|
|
117
618
|
const wcagTags = WCAG_TAG_MAP[options.wcag.toUpperCase()] ?? WCAG_TAG_MAP["AA"];
|
|
118
619
|
const viewport = ["mobile", "tablet", "desktop"].includes(options.viewport ?? "") ? options.viewport : "desktop";
|
|
119
|
-
|
|
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, {
|
|
120
628
|
wcagTags,
|
|
121
629
|
includeAxe: true,
|
|
122
630
|
dismissModals: true,
|
|
123
631
|
deep: options.deep,
|
|
124
632
|
deepApiUrl: options.deep ? "https://api.webability.io" : void 0,
|
|
125
|
-
deepApiKey: options.deep ? options.apiKey : void 0
|
|
126
|
-
viewport,
|
|
127
|
-
browser: { headless: true, timeout: 3e4 }
|
|
633
|
+
deepApiKey: options.deep ? options.apiKey : void 0
|
|
128
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
|
+
};
|
|
129
741
|
}
|
|
130
742
|
|
|
131
743
|
// src/init.ts
|
|
@@ -154,83 +766,83 @@ function initConfig() {
|
|
|
154
766
|
|
|
155
767
|
// src/display.ts
|
|
156
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;
|
|
157
777
|
function header() {
|
|
158
778
|
console.log();
|
|
159
|
-
console.log(
|
|
160
|
-
console.log(chalk2.dim(" https://abilyo.com"));
|
|
779
|
+
console.log(` ${BLUE("\u25C6")} ${BOLD("Abilyo")} ${DIM("WCAG Scanner")}`);
|
|
161
780
|
console.log();
|
|
162
781
|
}
|
|
163
782
|
function scoreCard(score, url) {
|
|
164
|
-
const color = score >= 80 ?
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
console.log(
|
|
168
|
-
console.log();
|
|
169
|
-
console.log(`
|
|
170
|
-
console.log(` ${bar}`);
|
|
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)}`);
|
|
171
789
|
console.log();
|
|
172
790
|
}
|
|
173
791
|
function renderBar(score) {
|
|
174
|
-
const
|
|
175
|
-
const filled = Math.round(score / 100 *
|
|
176
|
-
const empty =
|
|
177
|
-
const color = score >= 80 ?
|
|
178
|
-
return color("\
|
|
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));
|
|
179
797
|
}
|
|
180
|
-
function
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
case "high":
|
|
188
|
-
case "critical":
|
|
189
|
-
case "serious":
|
|
190
|
-
return chalk2.red;
|
|
191
|
-
case "medium":
|
|
192
|
-
case "moderate":
|
|
193
|
-
return chalk2.yellow;
|
|
194
|
-
case "low":
|
|
195
|
-
case "minor":
|
|
196
|
-
return chalk2.dim;
|
|
197
|
-
default:
|
|
198
|
-
return chalk2.white;
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
const high = issues.filter((i) => ["high", "critical", "serious"].includes(i.severity.toLowerCase()));
|
|
202
|
-
const medium = issues.filter((i) => ["medium", "moderate"].includes(i.severity.toLowerCase()));
|
|
203
|
-
const low = issues.filter((i) => ["low", "minor"].includes(i.severity.toLowerCase()));
|
|
204
|
-
console.log(chalk2.bold(" Issues"));
|
|
205
|
-
console.log(` ${chalk2.red.bold(high.length + " High")} ${chalk2.yellow.bold(medium.length + " Medium")} ${chalk2.dim.bold(low.length + " Low")}`);
|
|
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 "))}`);
|
|
206
805
|
console.log();
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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)}`);
|
|
213
819
|
}
|
|
214
|
-
if (
|
|
215
|
-
|
|
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))}`);
|
|
216
830
|
}
|
|
217
831
|
console.log();
|
|
218
832
|
}
|
|
219
|
-
function
|
|
220
|
-
console.log(
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
console.log(
|
|
228
|
-
console.log(chalk2.bold.blue(" Fix these issues automatically"));
|
|
229
|
-
console.log(chalk2.dim(" https://abilyo.com"));
|
|
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")}`);
|
|
230
842
|
console.log();
|
|
231
843
|
}
|
|
232
844
|
function errorMsg(msg) {
|
|
233
|
-
console.log(
|
|
845
|
+
console.log(RED(` \u2717 Error: ${msg}`));
|
|
234
846
|
console.log();
|
|
235
847
|
}
|
|
236
848
|
function jsonOutput(data) {
|
|
@@ -248,13 +860,7 @@ function csvOutput(issues) {
|
|
|
248
860
|
// src/cli.ts
|
|
249
861
|
var program = new Command();
|
|
250
862
|
program.name("abilyo").description("Abilyo by WebAbility \u2014 WCAG accessibility scanner").version("1.0.0");
|
|
251
|
-
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").action(async (url, opts) => {
|
|
252
|
-
if (!isActivated()) {
|
|
253
|
-
header();
|
|
254
|
-
errorMsg("Abilyo is currently invite-only. Run `abilyo activate <code>` with your access code.");
|
|
255
|
-
console.log(chalk3.dim(" Request access at https://abilyo.com/early-access"));
|
|
256
|
-
return process.exit(1);
|
|
257
|
-
}
|
|
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) => {
|
|
258
864
|
if (!url.startsWith("http")) url = `https://${url}`;
|
|
259
865
|
if (opts.deep || opts.upload) {
|
|
260
866
|
const apiKey = getApiKey();
|
|
@@ -284,14 +890,18 @@ program.command("scan <url>").description("Scan a website for accessibility issu
|
|
|
284
890
|
return;
|
|
285
891
|
}
|
|
286
892
|
if (opts.format === "table") header();
|
|
893
|
+
const t0 = Date.now();
|
|
287
894
|
const spinner = ora({ text: "Scanning...", prefixText: " " }).start();
|
|
895
|
+
let result;
|
|
288
896
|
try {
|
|
289
|
-
|
|
897
|
+
result = await localScan(url, {
|
|
290
898
|
wcag: opts.wcag,
|
|
291
899
|
deep: opts.deep ?? false,
|
|
292
|
-
apiKey: getApiKey() ?? void 0
|
|
900
|
+
apiKey: getApiKey() ?? void 0,
|
|
901
|
+
show: opts.show ?? false
|
|
293
902
|
});
|
|
294
|
-
|
|
903
|
+
const elapsed = ((Date.now() - t0) / 1e3).toFixed(1);
|
|
904
|
+
spinner.succeed(`Scanned ${result.issues.length} issues in ${elapsed}s`);
|
|
295
905
|
if (opts.format === "json") {
|
|
296
906
|
const sanitized = {
|
|
297
907
|
...result,
|
|
@@ -312,29 +922,69 @@ program.command("scan <url>").description("Scan a website for accessibility issu
|
|
|
312
922
|
console.log(toHtmlReport(result));
|
|
313
923
|
} else {
|
|
314
924
|
scoreCard(result.summary.total === 0 ? 100 : Math.max(0, 100 - result.summary.total), url);
|
|
925
|
+
summaryLine(result.summary);
|
|
315
926
|
if (result.issues.length > 0) {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
+
}
|
|
322
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."));
|
|
323
971
|
console.log();
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
972
|
+
await new Promise((resolve) => {
|
|
973
|
+
process.stdin.resume();
|
|
974
|
+
process.stdin.once("data", () => {
|
|
975
|
+
process.stdin.pause();
|
|
976
|
+
resolve();
|
|
977
|
+
});
|
|
330
978
|
});
|
|
331
|
-
|
|
979
|
+
await closeBrowser(result);
|
|
332
980
|
}
|
|
333
981
|
if (opts.exit && result.summary.total > 0) {
|
|
334
982
|
process.exit(1);
|
|
335
983
|
}
|
|
336
984
|
} catch (err) {
|
|
337
985
|
spinner.stop();
|
|
986
|
+
if (result) await closeBrowser(result).catch(() => {
|
|
987
|
+
});
|
|
338
988
|
errorMsg(err.message);
|
|
339
989
|
process.exit(1);
|
|
340
990
|
}
|
|
@@ -380,15 +1030,11 @@ async function remoteScan(url, opts) {
|
|
|
380
1030
|
csvOutput(issues);
|
|
381
1031
|
} else {
|
|
382
1032
|
scoreCard(report.score || 0, url);
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
notices: 0,
|
|
389
|
-
advanced: (report.ByFunctions || []).reduce((sum, f) => sum + (f.count || 0), 0)
|
|
390
|
-
});
|
|
391
|
-
ctaBox();
|
|
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();
|
|
392
1038
|
}
|
|
393
1039
|
if (opts.exit && issues.length > 0) process.exit(1);
|
|
394
1040
|
} catch (err) {
|
|
@@ -397,38 +1043,141 @@ async function remoteScan(url, opts) {
|
|
|
397
1043
|
process.exit(1);
|
|
398
1044
|
}
|
|
399
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
|
+
});
|
|
400
1097
|
program.command("init").description("Create .webability.yml config file").action(() => {
|
|
401
1098
|
header();
|
|
402
1099
|
initConfig();
|
|
403
1100
|
console.log();
|
|
404
1101
|
});
|
|
405
|
-
program.command("login").description("Authenticate with your WebAbility account").option("-k, --key <key>", "API key (
|
|
1102
|
+
program.command("login").description("Authenticate with your WebAbility account").option("-k, --key <key>", "API key directly (for CI/CD)").action(async (opts) => {
|
|
406
1103
|
header();
|
|
407
1104
|
if (opts.key) {
|
|
408
1105
|
setApiKey(opts.key);
|
|
409
|
-
console.log(chalk3.green(" API key saved."));
|
|
1106
|
+
console.log(chalk3.green(" \u2713 API key saved."));
|
|
410
1107
|
console.log();
|
|
411
1108
|
return;
|
|
412
1109
|
}
|
|
413
|
-
const
|
|
414
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
415
|
-
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
416
|
-
console.log(chalk3.bold(" Login to WebAbility"));
|
|
417
|
-
console.log();
|
|
418
|
-
const email = await ask(" Email: ");
|
|
419
|
-
const password = await ask(" Password: ");
|
|
420
|
-
rl.close();
|
|
421
|
-
const spinner = ora({ text: "Authenticating...", prefixText: " " }).start();
|
|
1110
|
+
const spinner = ora({ text: "Starting login...", prefixText: " " }).start();
|
|
422
1111
|
try {
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
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")}`);
|
|
426
1131
|
console.log();
|
|
427
|
-
|
|
428
|
-
|
|
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.");
|
|
429
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();
|
|
430
1170
|
}
|
|
431
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
|
+
}
|
|
432
1181
|
program.command("whoami").description("Show current authentication status").action(() => {
|
|
433
1182
|
const key = getApiKey();
|
|
434
1183
|
header();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webability/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Abilyo by WebAbility — WCAG accessibility scanner for your terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,19 +13,21 @@
|
|
|
13
13
|
"clean": "rm -rf dist"
|
|
14
14
|
},
|
|
15
15
|
"keywords": [
|
|
16
|
+
"a11y",
|
|
17
|
+
"abilyo",
|
|
16
18
|
"accessibility",
|
|
17
|
-
"wcag",
|
|
18
19
|
"ada",
|
|
19
|
-
"a11y",
|
|
20
20
|
"audit",
|
|
21
|
+
"cli",
|
|
21
22
|
"scanner",
|
|
22
|
-
"
|
|
23
|
+
"wcag",
|
|
24
|
+
"webability"
|
|
23
25
|
],
|
|
24
26
|
"author": "WebAbility <support@webability.io>",
|
|
25
27
|
"license": "MIT",
|
|
26
28
|
"repository": {
|
|
27
29
|
"type": "git",
|
|
28
|
-
"url": "https://github.com/snayyar00/
|
|
30
|
+
"url": "https://github.com/snayyar00/abilyo",
|
|
29
31
|
"directory": "cli"
|
|
30
32
|
},
|
|
31
33
|
"dependencies": {
|
|
@@ -45,5 +47,6 @@
|
|
|
45
47
|
},
|
|
46
48
|
"engines": {
|
|
47
49
|
"node": ">=18"
|
|
48
|
-
}
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://abilyo.com"
|
|
49
52
|
}
|