@webability/cli 1.1.1 → 1.1.2
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 +8 -810
- package/package.json +3 -3
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 se=(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{Command as xe}from"commander";import i from"chalk";import R from"ora";var ie="https://api.webability.io";async function ae(e,t={},n){let a=await fetch(`${ie}${e}`,{...t,headers:{"Content-Type":"application/json",Authorization:`Bearer ${n}`,Origin:"https://app.webability.io",...t.headers}});if(!a.ok){let o=await a.text().catch(()=>"");throw new Error(`API ${a.status}: ${o.slice(0,200)}`)}return a.json()}async function D(e,t,n){let a=await ae("/graphql",{method:"POST",body:JSON.stringify({query:e,variables:t})},n);if(a.errors?.length)throw new Error(a.errors[0].message);return a.data}async function _(e,t,n){n?.("Starting scan...");let{startAccessibilityReportJob:a}=await D("query($url: String!) { startAccessibilityReportJob(url: $url, use_cache: false) { jobId } }",{url:e},t),o=a.jobId;n?.(`Job ${o.slice(0,8)}... created`);for(let p=0;p<60;p++){await new Promise(r=>setTimeout(r,3e3));let{getAccessibilityReportByJobId:s}=await D("query($jobId: String!) { getAccessibilityReportByJobId(jobId: $jobId) { status error result { savedReport { key } } } }",{jobId:o},t);if(s.status==="done")return{key:s.result.savedReport.key};if(s.status==="error")throw new Error(s.error||"Scan failed");n?.(`Scanning... (${(p+1)*3}s)`)}throw new Error("Scan timed out after 3 minutes")}async function K(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 ne from"conf";var C=new ne({projectName:"webability",schema:{apiKey:{type:"string",default:""},accessCode:{type:"string",default:""},defaultFormat:{type:"string",default:"table",enum:["table","json","csv"]}}});function $(){return process.env.WEBABILITY_API_KEY||C.get("apiKey")||""}function O(e){C.set("apiKey",e)}function re(){return C.get("accessCode")||""}function j(e){C.set("accessCode",e)}function J(){return!!re()}function G(){C.clear()}import{scan as le,detectFramework as ce}from"@webability/core";var Y={A:["wcag2a"],AA:["wcag2a","wcag2aa","wcag21aa","wcag22aa"],AAA:["wcag2a","wcag2aa","wcag2aaa","wcag21aa","wcag22aa"]},pe={critical:"#ef4444",serious:"#f97316",moderate:"#eab308",minor:"#6b7280"},de={critical:"\u2717",serious:"\u25B2",moderate:"\u25CF",minor:"\u25CB"};function ue(e,t,n){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((o,p)=>({id:p,selector:o.selector,impact:o.impact,wcag:o.wcag,message:o.message.slice(0,200),fixDesc:o.fix?.suggestedValue?`Set ${o.fix.attribute}="${o.fix.suggestedValue}"`:"",fixAttr:o.fix?.attribute||"",fixVal:o.fix?.suggestedValue||"",fixCur:o.fix?.currentValue||"",canApply:!!(o.fix?.suggestedValue&&!o.fix?.needsManualReview)})))};
|
|
5
|
+
const colors = ${JSON.stringify(pe)};
|
|
6
|
+
const icons = ${JSON.stringify(de)};
|
|
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
|
|
|
@@ -612,138 +474,7 @@ function buildHighlightScript(issues, apiKey, framework) {
|
|
|
612
474
|
|
|
613
475
|
renderList();
|
|
614
476
|
})();
|
|
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
|
|
477
|
+
`}async function H(e,t){let n=Y[t.wcag.toUpperCase()]??Y.AA,a=["mobile","tablet","desktop"].includes(t.viewport??"")?t.viewport:"desktop",p=await(await import("playwright")).chromium.launch({headless:!t.show}),r=await(await p.newContext({viewport:a==="mobile"?{width:375,height:667}:a==="tablet"?{width:768,height:1024}:{width:1280,height:720}})).newPage(),c=e.startsWith("http")?e:`https://${e}`;await r.goto(c,{waitUntil:"domcontentloaded",timeout:3e4});let m=await ce(r),l=await le(r,{wcagTags:n,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&&l.issues.length>0?(await r.evaluate(ue(l.issues,t.apiKey,m.framework)),await r.evaluate(()=>window.scrollTo(0,0)),{...l,framework:m.framework,_browser:p}):(await p.close(),{...l,framework:m.framework})}async function T(e){e._browser&&(await e._browser.close(),delete e._browser)}import{scan as ge,detectFramework as fe}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,a=e.startsWith("http")?e:`https://${e}`,o=Date.now(),s=await(await import("playwright")).chromium.launch({headless:!1}),c=await(await s.newContext({viewport:{width:1280,height:720}})).newPage(),m=[],l="plain-css",g=!1,b=!1,w=new Set,M=async()=>{if(g){b=!0;return}g=!0;try{await c.waitForLoadState("domcontentloaded",{timeout:5e3}).catch(()=>{}),await c.waitForTimeout(800);let u=c.url();if(w.has(u)){g=!1;return}w.add(u),(!l||l==="plain-css")&&(l=(await fe(c).catch(()=>({framework:"plain-css"}))).framework);let h=await ge(c,{wcagTags:n,includeAxe:!0,dismissModals:!1});m.push({url:u,issues:h.issues,scannedAt:new Date().toISOString()}),t.onPageScan?.(u,h.issues.length)}catch{}finally{g=!1,b&&(b=!1,setTimeout(M,100))}};c.on("framenavigated",u=>{u===c.mainFrame()&&setTimeout(M,600)}),await c.goto(a,{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 m)for(let h of u.issues){let k=`${h.type}::${h.selector}::${h.wcag}`,x=P.get(k);x?x.foundOn.includes(u.url)||(x.foundOn.push(u.url),x.pageCount=x.foundOn.length):P.set(k,{...h,foundOn:[u.url],pageCount:1})}return{pages:m,uniqueIssues:Array.from(P.values()).sort((u,h)=>{let k={critical:0,serious:1,moderate:2,minor:3},x=k[u.impact]??2,V=k[h.impact]??2;return x!==V?x-V:h.pageCount-u.pageCount}),framework:l,totalDuration:Date.now()-o}}import{writeFileSync as me,existsSync as ye}from"fs";import q from"chalk";var be=`project: my-project
|
|
747
478
|
urls:
|
|
748
479
|
- http://localhost:3000
|
|
749
480
|
standard: WCAG2.1AA
|
|
@@ -752,537 +483,4 @@ ignore: []
|
|
|
752
483
|
threshold:
|
|
753
484
|
critical: 0
|
|
754
485
|
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
|
-
}
|
|
486
|
+
`;function Z(){let e=".webability.yml";if(ye(e)){console.log(q.yellow(` ${e} already exists`));return}me(e,be),console.log(q.green(` Created ${e}`)),console.log(q.dim(" Edit it to configure your project URLs and thresholds"))}import v from"chalk";var d=v.dim,ee=v.bold,z=v.blue,N=v.green,S=v.red,I=v.yellow,E=v.cyan,he=v.white;function f(){console.log(),console.log(` ${z("\u25C6")} ${ee("Abilyo")} ${d("WCAG Scanner")}`),console.log()}function W(e,t){let n=e>=80?N:e>=50?I:S,a=e>=80?N("\u2713"):e>=50?I("\u26A0"):S("\u2717"),o=e>=80?"Passing":e>=50?"Needs work":"Failing";console.log(` ${d("URL")} ${he(t)}`),console.log(` ${d("Score")} ${n.bold(e+"%")} ${a} ${d(o)}`),console.log(` ${d(" ")}${we(e)}`),console.log()}function we(e){let n=Math.round(e/100*32),a=32-n;return(e>=80?N:e>=50?I:S)("\u2501".repeat(n))+d("\u2501".repeat(a))}function B(e){let t=[];e.critical>0&&t.push(S.bold(`${e.critical} critical`)),e.serious>0&&t.push(I(`${e.serious} serious`)),e.moderate>0&&t.push(d(`${e.moderate} moderate`)),e.minor>0&&t.push(d(`${e.minor} minor`)),console.log(` ${ee(String(e.total))} issues ${t.join(d(" \xB7 "))}`),console.log()}function F(e){let t=e.severity==="critical"?S("\u2717"):e.severity==="serious"?I("\u25B2"):d("\u25CF"),n=d(`${e.wcag}`),a=e.count&&e.count>1?d(` \xD7${e.count}`):"";if(console.log(` ${t} ${n}${a} ${e.problem.slice(0,72)}`),e.contrast?(console.log(` ${E("fix")} ${e.fix.slice(0,70)}`),console.log(` ${d(`${e.contrast.current}:1 \u2192 ${e.contrast.suggested}:1 (min ${e.contrast.required}:1)`)}`)):e.before&&e.after?console.log(` ${E("fix")} ${d(e.before)} ${d("\u2192")} ${N(e.after)}`):console.log(` ${E("fix")} ${e.fix.slice(0,70)}`),e.html&&e.count===1){let o=e.html.slice(0,60).replace(/\n/g," ");console.log(` ${d(o)}`)}if(e.selectors&&e.selectors.length>0&&e.count&&e.count>1){let o=e.selectors.slice(0,2).map(s=>s.slice(0,30)).join(d(", ")),p=e.count>2?d(` +${e.count-2} more`):"";console.log(` ${d(o+p)}`)}else e.selector&&console.log(` ${d(e.selector.slice(0,55))}`);console.log()}function L(e){console.log(d(" \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(` ${d("Next steps:")}`),console.log(` ${E("abilyo scan "+(e.url||"<url>")+" --format json")} ${d("CI/CD output")}`),console.log(` ${E("abilyo scan "+(e.url||"<url>")+" --format html > report.html")} ${d("share report")}`),console.log()),console.log(` ${z("\u25C6")} ${d("Auto-fix at")} ${z("abilyo.com")}`),console.log()}function A(e){console.log(S(` \u2717 Error: ${e}`)),console.log()}function te(e){console.log(JSON.stringify(e,null,2))}function U(e){console.log("severity,wcag,message,element");for(let t of e){let n=t.message.replace(/"/g,'""'),a=(t.element||"").replace(/"/g,'""');console.log(`${t.severity},${t.wcag},"${n}","${a}"`)}}var y=new xe;y.name("abilyo").description("Abilyo by WebAbility \u2014 WCAG accessibility scanner").version("1.0.0");y.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(e,t)=>{if(e.startsWith("http")||(e=`https://${e}`),t.deep||t.upload){let p=$();if(!p)return t.format==="table"&&f(),A("--deep and --upload require a Pro plan. Run `abilyo login` first."),process.exit(1);try{let s=await fetch("https://api.webability.io/cli/verify",{headers:{Authorization:`Bearer ${p}`}});if(s.ok&&!(await s.json()).active)return t.format==="table"&&f(),A("Your account does not have an active Pro plan."),console.log(i.dim(" Upgrade at https://abilyo.com/pricing")),process.exit(1)}catch{}}if(t.remote){await ve(e,t);return}t.format==="table"&&f();let n=Date.now(),a=R({text:"Scanning...",prefixText:" "}).start(),o;try{o=await H(e,{wcag:t.wcag,deep:t.deep??!1,apiKey:$()??void 0,show:t.show??!1});let p=((Date.now()-n)/1e3).toFixed(1);if(a.succeed(`Scanned ${o.issues.length} issues in ${p}s`),t.format==="json"){let s={...o,issues:o.issues.map(({source:r,...c})=>c)};console.log(JSON.stringify(s,null,2))}else if(t.format==="csv")U(o.issues.map(s=>({severity:s.impact,wcag:s.wcag,message:s.message,element:s.selector})));else if(t.format==="sarif")console.log(JSON.stringify(Ae(o),null,2));else if(t.format==="html"){let{toHtmlReport:s}=await import("@webability/core");console.log(s(o))}else{if(W(o.summary.total===0?100:Math.max(0,100-o.summary.total),e),B(o.summary),o.issues.length>0){let{generateDevSuggestion:s}=await import("@webability/core"),r=o.framework||"plain-css",c=[],m=new Map;for(let l of o.issues){let g=s(l,r),b=`${l.type}|${g.fix.slice(0,50)}`,w=m.get(b);w!==void 0?(c[w].count++,c[w].selectors.length<3&&c[w].selectors.push(l.selector.slice(0,50))):(m.set(b,c.length),c.push({suggestion:g,issue:l,count:1,selectors:[l.selector.slice(0,50)]}))}c.sort((l,g)=>{let b={critical:0,serious:1,moderate:2,minor:3};return(b[l.issue.impact]??2)-(b[g.issue.impact]??2)});for(let l of c.slice(0,20))F({severity:l.issue.impact,wcag:l.issue.wcag,problem:l.suggestion.problem,fix:l.suggestion.fix,count:l.count,selector:l.count===1?l.issue.selector:void 0,selectors:l.count>1?l.selectors:void 0,contrast:l.suggestion.contrast,before:l.suggestion.before,after:l.suggestion.after,html:l.issue.html});c.length>20&&(console.log(i.dim(` ... +${c.length-20} more`)),console.log())}L({hasIssues:o.issues.length>0,url:e.replace("https://","")})}t.show&&o.issues.length>0&&(console.log(i.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 T(o)),t.exit&&o.summary.total>0&&process.exit(1)}catch(p){a.stop(),o&&await T(o).catch(()=>{}),A(p.message),process.exit(1)}});async function ve(e,t){let n=$();if(!n)return t.format==="table"&&f(),A("--remote requires login. Run `wa login` first."),process.exit(1);t.format==="table"&&f();let a=R({text:"Scanning (remote)...",prefixText:" "}).start();try{let o=await _(e,n,r=>{a.text=r});a.text="Fetching report...";let p=await K(o.key,n);if(a.stop(),!p)return A("Report not found"),process.exit(1);let s=[];if(p.axe?.violations)for(let r of p.axe.violations)for(let c of r.nodes||[])s.push({severity:r.impact||"moderate",wcag:r.id,message:r.description,element:c.target?.[0]});if(p.ByFunctions)for(let r of p.ByFunctions)for(let c of r.issues||[])s.push({severity:c.impact||"moderate",wcag:c.code||"",message:c.description||"",element:c.element});if(t.format==="json")te({url:e,score:p.score,totalElements:p.totalElements,issues:s});else if(t.format==="csv")U(s);else{W(p.score||0,e),B({critical:0,serious:s.filter(r=>r.severity==="serious").length,moderate:s.filter(r=>r.severity==="moderate").length,minor:0,total:s.length});for(let r of s.slice(0,20))F({severity:r.severity,wcag:r.wcag,problem:r.message,fix:"Review this issue",selector:r.element});L()}t.exit&&s.length>0&&process.exit(1)}catch(o){a.stop(),A(o.message),process.exit(1)}}y.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}`),f(),console.log(i.bold(" Flow Scan")),console.log(),console.log(` Starting at: ${i.cyan(e)}`),console.log(i.dim(" Click through your user journey. Close the browser when done.")),console.log();let n=0,a=await Q(e,{wcag:t.wcag,onPageScan:(s,r)=>{n++,console.log(` ${i.green("\u2713")} ${i.dim(String(n).padStart(2))} ${s.replace(/^https?:\/\//,"").slice(0,60)} ${i.dim("\u2014 "+r+" issues")}`)}});console.log(),console.log(i.bold(" \u2500\u2500\u2500 Flow Report \u2500\u2500\u2500")),console.log(),console.log(` Pages scanned: ${i.cyan(a.pages.length)}`),console.log(` Unique issues: ${i.cyan(a.uniqueIssues.length)}`),console.log(` Total duration: ${i.dim((a.totalDuration/1e3).toFixed(1)+"s")}`),console.log();let o={critical:0,serious:0,moderate:0,minor:0};a.uniqueIssues.forEach(s=>{o[s.impact]++}),B({...o,total:a.uniqueIssues.length});let p=a.uniqueIssues.slice(0,15);for(let s of p){let r=s.pageCount>1?i.yellow(` \xD7${s.pageCount} pages`):"";if(F({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(i.dim(` Found on${r}:`));for(let c of s.foundOn.slice(0,3))console.log(i.dim(" "+c.replace(/^https?:\/\//,"")))}}a.uniqueIssues.length>15&&console.log(i.dim(` ... +${a.uniqueIssues.length-15} more unique issues`)),console.log(),L({hasIssues:a.uniqueIssues.length>0,url:e.replace("https://","")})});y.command("init").description("Create .webability.yml config file").action(()=>{f(),Z(),console.log()});y.command("login").description("Authenticate with your WebAbility account").option("-k, --key <key>","API key directly (for CI/CD)").action(async e=>{if(f(),e.key){O(e.key),console.log(i.green(" \u2713 API key saved.")),console.log();return}let t=R({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 l="https://app.webability.io/settings/api-keys";console.log(" Open this URL to get your API key:"),console.log(),console.log(` ${i.cyan(l)}`),console.log(),console.log(` Then run: ${i.cyan("abilyo login --key YOUR_KEY")}`),console.log(),oe(l);return}let{deviceCode:a,userCode:o,verificationUrl:p,expiresIn:s}=await n.json();t.stop(),console.log(` ${i.bold("Login to WebAbility")}`),console.log(),console.log(` Open: ${i.cyan(p)}`),console.log(` Code: ${i.bold.yellow(o)}`),console.log(),oe(p);let r=R({text:"Waiting for approval...",prefixText:" "}).start(),c=3e3,m=Math.floor((s||300)*1e3/c);for(let l=0;l<m;l++){await new Promise(g=>setTimeout(g,c));try{let g=await fetch("https://api.webability.io/cli/device-token",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({deviceCode:a})});if(g.ok){let{token:w}=await g.json();O(w),r.succeed("Logged in!"),console.log();return}if((await g.json().catch(()=>({}))).error==="expired")return r.fail("Login expired. Run `abilyo login` again."),process.exit(1)}catch{}}r.fail("Login timed out."),process.exit(1)}catch{t.stop(),console.log(` Open: ${i.cyan("https://app.webability.io/settings/api-keys")}`),console.log(` Then: ${i.cyan("abilyo login --key YOUR_KEY")}`),console.log()}});function oe(e){try{let{execFile:t}=se("child_process"),n=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";t(n,[e],()=>{})}catch{}}y.command("whoami").description("Show current authentication status").action(()=>{let e=$();f(),e?(console.log(i.green(" Authenticated")),console.log(i.dim(` Key: ${e.slice(0,20)}...`))):(console.log(i.yellow(" Not authenticated")),console.log(i.dim(" Run `wa login` to authenticate"))),console.log()});y.command("logout").description("Remove saved API key").action(()=>{G(),f(),console.log(i.green(" Logged out. API key removed.")),console.log()});y.command("activate <code>").description("Activate Abilyo with your early access code").action(async e=>{f();let t=R({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){j(e.trim()),t.succeed("Abilyo activated!"),console.log(),console.log(i.dim(" Run `abilyo scan <url>` to get started")),console.log();return}}catch{}e.trim().length>=8?(j(e.trim()),t.succeed("Abilyo activated!"),console.log(),console.log(i.dim(" Run `abilyo scan <url>` to get started")),console.log()):(t.fail("Invalid access code"),console.log(i.dim(" Request access at https://abilyo.com/early-access")),process.exit(1))});y.argument("[url]","URL to scan").action(e=>{e?y.parse(["node","abilyo","scan",e,...process.argv.slice(3)]):(f(),J()?(console.log(i.bold(" Commands:")),console.log(),console.log(` ${i.cyan("abilyo scan")} <url> Scan for accessibility issues`),console.log(` ${i.cyan("abilyo scan --deep")} <url> Deeper scan with additional checks (Pro)`),console.log(` ${i.cyan("abilyo scan --exit")} <url> CI mode \u2014 exit 1 if issues found`),console.log(` ${i.cyan("abilyo init")} Create .webability.yml config`),console.log(` ${i.cyan("abilyo login")} Authenticate with your account`),console.log(` ${i.cyan("abilyo whoami")} Show auth status`),console.log(),console.log(i.dim(" Example: abilyo scan localhost:3000")),console.log(i.dim(" Example: abilyo scan example.com --format json")),console.log(i.dim(" Example: abilyo scan example.com --deep --exit"))):(console.log(i.yellow(" Abilyo is invite-only during early access.")),console.log(),console.log(` ${i.cyan("abilyo activate")} <code> Activate with your access code`),console.log(),console.log(i.dim(" Request access at https://abilyo.com/early-access"))),console.log())});y.parse();function Ae(e){return{$schema:"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",version:"2.1.0",runs:[{tool:{driver:{name:"WebAbility",version:"1.0.0",informationUri:"https://webability.io",rules:e.issues.map(t=>({id:t.type,shortDescription:{text:t.message}}))}},results:e.issues.map(t=>({ruleId:t.type,level:t.impact==="critical"||t.impact==="serious"?"error":"warning",message:{text:t.message},locations:[{physicalLocation:{artifactLocation:{uri:e.url},region:{snippet:{text:t.html||t.selector}}}}]}))}]}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webability/cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Abilyo by WebAbility — WCAG accessibility scanner for your terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"ora": "^8.1.0",
|
|
32
32
|
"playwright": "^1.49.0",
|
|
33
33
|
"yaml": "^2.6.0",
|
|
34
|
-
"@webability/core": "1.1.
|
|
34
|
+
"@webability/core": "1.1.2"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^25.6.0",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
"homepage": "https://abilyo.com",
|
|
46
46
|
"scripts": {
|
|
47
|
-
"build": "tsup src/cli.ts --format esm --dts --clean",
|
|
47
|
+
"build": "tsup src/cli.ts --format esm --dts --minify --clean",
|
|
48
48
|
"dev": "tsx src/cli.ts",
|
|
49
49
|
"typecheck": "tsc --noEmit",
|
|
50
50
|
"clean": "rm -rf dist"
|