@webability/cli 1.1.3 → 1.3.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 +8 -8
- package/dist/cli.js +11 -7
- package/package.json +6 -2
- package/visual-dogfood.mjs +0 -55
package/README.md
CHANGED
|
@@ -11,13 +11,13 @@ npm install -g @webability/cli
|
|
|
11
11
|
## Quick start
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
webability scan https://example.com
|
|
15
|
+
webability scan localhost:3000
|
|
16
|
+
webability scan example.com --format json
|
|
17
|
+
webability scan example.com --format html > report.html
|
|
18
|
+
webability scan example.com --format sarif > results.sarif
|
|
19
|
+
webability scan example.com --exit # CI mode — exit 1 if issues found
|
|
20
|
+
webability scan example.com --viewport mobile
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
## CI/CD
|
|
@@ -33,7 +33,7 @@ abilyo scan example.com --viewport mobile
|
|
|
33
33
|
## Config
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
|
|
36
|
+
webability init # creates .webability.yml
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
```yaml
|
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var
|
|
2
|
+
var Fe=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,o)=>(typeof require<"u"?require:t)[o]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});import{readFileSync as Ze}from"fs";import{fileURLToPath as et}from"url";import{dirname as tt,join as ot}from"path";import{basename as Ee}from"path";var Ie=Ee(process.argv[1]??""),c=Ie==="abilyo"?"abilyo":"webability";import{Command as st}from"commander";import i from"chalk";import B from"ora";var Ne="https://api.webability.io";async function Be(e,t={},o){let s=await fetch(`${Ne}${e}`,{...t,headers:{"Content-Type":"application/json",Authorization:`Bearer ${o}`,Origin:"https://app.webability.io",...t.headers}});if(!s.ok){let n=await s.text().catch(()=>"");throw new Error(`API ${s.status}: ${n.slice(0,200)}`)}return s.json()}async function W(e,t,o){let s=await Be("/graphql",{method:"POST",body:JSON.stringify({query:e,variables:t})},o);if(s.errors?.length)throw new Error(s.errors[0].message);return s.data}async function ie(e,t,o){o?.("Starting scan...");let{startAccessibilityReportJob:s}=await W("query($url: String!) { startAccessibilityReportJob(url: $url, use_cache: false) { jobId } }",{url:e},t),n=s.jobId;o?.(`Job ${n.slice(0,8)}... created`);for(let a=0;a<60;a++){await new Promise(l=>setTimeout(l,3e3));let{getAccessibilityReportByJobId:r}=await W("query($jobId: String!) { getAccessibilityReportByJobId(jobId: $jobId) { status error result { savedReport { key } } } }",{jobId:n},t);if(r.status==="done")return{key:r.result.savedReport.key};if(r.status==="error")throw new Error(r.error||"Scan failed");o?.(`Scanning... (${(a+1)*3}s)`)}throw new Error("Scan timed out after 3 minutes")}async function ne(e,t){let{fetchReportByR2Key:o}=await W("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 o}import Re from"conf";var R=new Re({projectName:"webability",schema:{apiKey:{type:"string",default:""},accessCode:{type:"string",default:""},defaultFormat:{type:"string",default:"table",enum:["table","json","csv"]}}});function C(){return process.env.WEBABILITY_API_KEY||R.get("apiKey")||""}function q(e){R.set("apiKey",e)}function Pe(){return R.get("accessCode")||""}function V(e){R.set("accessCode",e)}function ae(){return!!Pe()||!!C()}function re(){R.clear()}import{scan as Me,detectFramework as Le,generateDevSuggestion as Te}from"@webability/core";var De={weak_link_name:"Screen-reader users navigate by pulling up a links list (VoiceOver Rotor / NVDA Insert+F7). They hear: 'Learn more, Learn more, Learn more' \u2014 every link sounds identical. They can't tell where each goes without context.",weak_button_name:"Same problem as weak links \u2014 assistive tech announces 'Submit' or 'Click here' with no context, so users can't tell what each button does.",missing_alt:`Screen readers say 'image' (or skip it entirely) when there's no alt text. A photo of your CEO becomes 'image, image' to a blind user. Decorative images need alt="" so they're skipped intentionally.`,decorative_icon:"Decorative icons next to labelled text get announced twice: 'home icon, Home'. Wastes the user's time and creates audio clutter.",missing_label:"Form inputs without labels are invisible to screen readers \u2014 users hear 'edit text' with no idea what to type. They abandon the form.",contrast_insufficient:"Low-contrast text excludes users with low vision (1 in 12 men have some color-blindness). It's also unreadable on glare-prone screens, in sunlight, and for older eyes.",non_text_contrast_insufficient:"If a button's border or icon doesn't have 3:1 contrast against its background, low-vision users can't see the boundary or recognize the icon at all.",target_too_small:"Touch targets under 24\xD724px are hard to hit accurately for users with motor impairments (Parkinson's, MS, arthritis). Mis-taps trigger wrong actions and frustrate keyboard-only users too.",missing_skip_link:"Keyboard and screen-reader users have to Tab through your entire header (logo, nav, CTAs) on every single page. A skip link lets them jump straight to main content \u2014 a 30-second fix that saves disabled users minutes of navigation per visit.",motion_without_reduced_motion:"Vestibular disorders (~35% of adults over 40) can be triggered by parallax, auto-rotation, and rapid animations \u2014 causing nausea, dizziness, and migraines. Respecting prefers-reduced-motion is non-optional.",heading_skip:"Screen-reader users navigate by heading hierarchy \u2014 they jump h1 \u2192 h2 \u2192 h3 to scan structure. Skipping levels (h2 \u2192 h4) makes the page feel disorganized and content gets missed.",multiple_h1:"Multiple h1 elements confuse the page structure model that screen readers and SEO tools use. Each page should have exactly one h1 \u2014 the page title.",redundant_role:`Adding role="navigation" to a <nav> element is redundant \u2014 screen readers already know it's navigation. Redundant ARIA can cause double-announcements.`,duplicate_id:"Duplicate IDs break aria-labelledby, aria-describedby, and label[for] references \u2014 screen readers may read the wrong content or none at all.",missing_required_indicator:"If users don't know a field is required until they submit, they miss the requirement (especially screen-reader users who skip placeholder text). They get an error after the fact and have to re-enter data.",new_window_link:"Opening links in a new tab without warning disorients screen-reader users \u2014 they think the back button is broken. WCAG requires you announce 'opens in new tab' via aria-label or visible text.",color_only_meaning:"Using only color to indicate state (red = error, green = success) excludes the ~8% of men who are color-blind. Always pair color with an icon, label, or pattern.",insufficient_navigation:"Inconsistent nav across pages forces users to relearn the structure on every page. Especially hard on cognitive disabilities and screen-reader users who memorize tab order.",focus_not_visible:"Without a visible focus ring, keyboard users have no idea which element is selected. Tabbing through your site becomes guesswork \u2014 they hit Enter and trigger the wrong action.",aria_hidden_focusable:`An element with aria-hidden="true" but tabindex="0" creates a 'ghost' focus stop \u2014 keyboard users can focus it but screen readers don't announce it. They press Enter on something invisible to them.`,svg_missing_name:"SVG icons that convey meaning (warning triangle, X close button) need an accessible name. Without it, blind users hear 'graphic' or nothing at all."};function Oe(e){return De[e.type]||""}var le={A:["wcag2a"],AA:["wcag2a","wcag2aa","wcag21aa","wcag22aa"],AAA:["wcag2a","wcag2aa","wcag2aaa","wcag21aa","wcag22aa"]},_e={critical:"#ef4444",serious:"#f97316",moderate:"#eab308",minor:"#6b7280"},ze={critical:"\u2717",serious:"\u25B2",moderate:"\u25CF",minor:"\u25CB"};function je(e,t,o){let s=o||"plain-css";return`
|
|
3
3
|
(function() {
|
|
4
|
-
const issues = ${JSON.stringify(e.map((
|
|
5
|
-
const colors = ${JSON.stringify(
|
|
6
|
-
const icons = ${JSON.stringify(
|
|
4
|
+
const issues = ${JSON.stringify(e.map((a,r)=>{let l=(()=>{try{return Te(a,s)}catch{return null}})();return{id:r,selector:a.selector,impact:a.impact,wcag:a.wcag,type:a.type,message:a.message.slice(0,200),problem:l?.problem||"",recommendedFix:l?.fix||"",wcagLink:l?.wcagLink||`https://www.w3.org/WAI/WCAG21/Understanding/${a.wcag.replace(/\./g,"")}`,whyMatters:Oe(a),fixDesc:a.fix?.suggestedValue?`Set ${a.fix.attribute}="${a.fix.suggestedValue}"`:"",fixAttr:a.fix?.attribute||"",fixVal:a.fix?.suggestedValue||"",fixCur:a.fix?.currentValue||"",canApply:!!(a.fix?.suggestedValue&&!a.fix?.needsManualReview)}}))};
|
|
5
|
+
const colors = ${JSON.stringify(_e)};
|
|
6
|
+
const icons = ${JSON.stringify(ze)};
|
|
7
7
|
const API_KEY = ${JSON.stringify(t||"")};
|
|
8
8
|
const API_URL = 'https://api.webability.io';
|
|
9
|
-
const FRAMEWORK = ${JSON.stringify(
|
|
9
|
+
const FRAMEWORK = ${JSON.stringify(o||"plain-css")};
|
|
10
10
|
const activeFilters = { critical: true, serious: true, moderate: true, minor: true };
|
|
11
11
|
const overlays = [];
|
|
12
12
|
|
|
@@ -697,7 +697,7 @@ var ae=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,n)=>(
|
|
|
697
697
|
|
|
698
698
|
renderList();
|
|
699
699
|
})();
|
|
700
|
-
`}async function
|
|
700
|
+
`}async function ce(e,t){let o=le[t.wcag.toUpperCase()]??le.AA,s=["mobile","tablet","desktop"].includes(t.viewport??"")?t.viewport:"desktop",a=await(await import("playwright")).chromium.launch({headless:!t.show,ignoreDefaultArgs:t.show?["--enable-automation"]:void 0,args:t.show?["--disable-blink-features=AutomationControlled","--no-default-browser-check","--disable-infobars","--no-first-run","--silent-launch","--disable-features=DialMediaRouteProvider"]:void 0}),l=await(await a.newContext({viewport:s==="mobile"?{width:390,height:844}:s==="tablet"?{width:820,height:1180}:{width:1280,height:720}})).newPage(),p=e.startsWith("http")?e:`https://${e}`;await l.goto(p,{waitUntil:"domcontentloaded",timeout:3e4});let g=await Le(l),h=await Me(l,{wcagTags:o,includeAxe:t.includeAxe??!0,dismissModals:!0,deep:t.deep,deepApiUrl:t.deep?"https://api.webability.io":void 0,deepApiKey:t.deep?t.apiKey:void 0});return t.show&&h.issues.length>0?(await l.evaluate(je(h.issues,t.apiKey,g.framework)),await l.evaluate(()=>window.scrollTo(0,0)),{...h,framework:g.framework,_browser:a}):(await a.close(),{...h,framework:g.framework})}async function U(e){e._browser&&(await e._browser.close(),delete e._browser)}import{scan as We,detectFramework as qe}from"@webability/core";var de={A:["wcag2a"],AA:["wcag2a","wcag2aa","wcag21aa","wcag22aa"],AAA:["wcag2a","wcag2aa","wcag2aaa","wcag21aa","wcag22aa"]};async function pe(e,t={}){let o=de[(t.wcag||"AA").toUpperCase()]??de.AA,s=e.startsWith("http")?e:`https://${e}`,n=Date.now(),r=await(await import("playwright")).chromium.launch({headless:!1}),p=await(await r.newContext({viewport:{width:1280,height:720}})).newPage(),g=[],h="plain-css",d=!1,k=!1,w=new Set,$=async()=>{if(d){k=!0;return}d=!0;try{await p.waitForLoadState("domcontentloaded",{timeout:5e3}).catch(()=>{}),await p.waitForTimeout(800);let m=p.url();if(w.has(m)){d=!1;return}w.add(m),(!h||h==="plain-css")&&(h=(await qe(p).catch(()=>({framework:"plain-css"}))).framework);let v=await We(p,{wcagTags:o,includeAxe:!0,dismissModals:!1});g.push({url:m,issues:v.issues,scannedAt:new Date().toISOString()}),t.onPageScan?.(m,v.issues.length)}catch{}finally{d=!1,k&&(k=!1,setTimeout($,100))}};p.on("framenavigated",m=>{m===p.mainFrame()&&setTimeout($,600)}),await p.goto(s,{waitUntil:"domcontentloaded",timeout:3e4}),await new Promise(m=>{r.on("disconnected",()=>m()),process.once("SIGINT",()=>m())}),await r.close().catch(()=>{});let j=new Map;for(let m of g)for(let v of m.issues){let E=`${v.type}::${v.selector}::${v.wcag}`,A=j.get(E);A?A.foundOn.includes(m.url)||(A.foundOn.push(m.url),A.pageCount=A.foundOn.length):j.set(E,{...v,foundOn:[m.url],pageCount:1})}return{pages:g,uniqueIssues:Array.from(j.values()).sort((m,v)=>{let E={critical:0,serious:1,moderate:2,minor:3},A=E[m.impact]??2,se=E[v.impact]??2;return A!==se?A-se:v.pageCount-m.pageCount}),framework:h,totalDuration:Date.now()-n}}import{writeFileSync as Ve,existsSync as Ue}from"fs";import K from"chalk";var Ke=`project: my-project
|
|
701
701
|
urls:
|
|
702
702
|
- http://localhost:3000
|
|
703
703
|
standard: WCAG2.1AA
|
|
@@ -706,4 +706,8 @@ ignore: []
|
|
|
706
706
|
threshold:
|
|
707
707
|
critical: 0
|
|
708
708
|
serious: 5
|
|
709
|
-
`;function Z(){let e=".webability.yml";if(ve(e)){console.log(M.yellow(` ${e} already exists`));return}we(e,ke),console.log(M.green(` Created ${e}`)),console.log(M.dim(" Edit it to configure your project URLs and thresholds"))}import v from"chalk";var p=v.dim,ee=v.bold,z=v.blue,B=v.green,F=v.red,A=v.yellow,$=v.cyan,Ae=v.white;function m(){console.log(),console.log(` ${z("\u25C6")} ${ee("Abilyo")} ${p("WCAG Scanner")}`),console.log()}function j(e,t){let n=e>=80?B:e>=50?A:F,o=e>=80?B("\u2713"):e>=50?A("\u26A0"):F("\u2717"),r=e>=80?"Passing":e>=50?"Needs work":"Failing";console.log(` ${p("URL")} ${Ae(t)}`),console.log(` ${p("Score")} ${n.bold(e+"%")} ${o} ${p(r)}`),console.log(` ${p(" ")}${Ce(e)}`),console.log()}function Ce(e){let n=Math.round(e/100*32),o=32-n;return(e>=80?B:e>=50?A:F)("\u2501".repeat(n))+p("\u2501".repeat(o))}function N(e){let t=[];e.critical>0&&t.push(F.bold(`${e.critical} critical`)),e.serious>0&&t.push(A(`${e.serious} serious`)),e.moderate>0&&t.push(p(`${e.moderate} moderate`)),e.minor>0&&t.push(p(`${e.minor} minor`)),console.log(` ${ee(String(e.total))} issues ${t.join(p(" \xB7 "))}`),console.log()}function R(e){let t=e.severity==="critical"?F("\u2717"):e.severity==="serious"?A("\u25B2"):p("\u25CF"),n=p(`${e.wcag}`),o=e.count&&e.count>1?p(` \xD7${e.count}`):"";if(console.log(` ${t} ${n}${o} ${e.problem.slice(0,72)}`),e.contrast?(console.log(` ${$("fix")} ${e.fix.slice(0,70)}`),console.log(` ${p(`${e.contrast.current}:1 \u2192 ${e.contrast.suggested}:1 (min ${e.contrast.required}:1)`)}`)):e.before&&e.after?console.log(` ${$("fix")} ${p(e.before)} ${p("\u2192")} ${B(e.after)}`):console.log(` ${$("fix")} ${e.fix.slice(0,70)}`),e.html&&e.count===1){let r=e.html.slice(0,60).replace(/\n/g," ");console.log(` ${p(r)}`)}if(e.selectors&&e.selectors.length>0&&e.count&&e.count>1){let r=e.selectors.slice(0,2).map(s=>s.slice(0,30)).join(p(", ")),i=e.count>2?p(` +${e.count-2} more`):"";console.log(` ${p(r+i)}`)}else e.selector&&console.log(` ${p(e.selector.slice(0,55))}`);console.log()}function te(e){if(e.length){for(let t of e)console.log(` ${A.bold("\u26A0 WARNING")} ${A(t)}`);console.log()}}function L(e){console.log(p(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")),e?.hasIssues&&(console.log(` ${p("Next steps:")}`),console.log(` ${$("abilyo scan "+(e.url||"<url>")+" --format json")} ${p("CI/CD output")}`),console.log(` ${$("abilyo scan "+(e.url||"<url>")+" --format html > report.html")} ${p("share report")}`),console.log()),console.log(` ${z("\u25C6")} ${p("Auto-fix at")} ${z("abilyo.com")}`),console.log()}function k(e){console.log(F(` \u2717 Error: ${e}`)),console.log()}function oe(e){console.log(JSON.stringify(e,null,2))}function W(e){console.log("severity,wcag,message,element");for(let t of e){let n=t.message.replace(/"/g,'""'),o=(t.element||"").replace(/"/g,'""');console.log(`${t.severity},${t.wcag},"${n}","${o}"`)}}var Be=Ee(Se(import.meta.url)),ie=JSON.parse(Fe($e(Be,"../package.json"),"utf-8")),h=new Ie;h.name("abilyo").description("Abilyo by WebAbility \u2014 WCAG accessibility scanner").version(ie.version);h.command("scan <url>").description("Scan a website for accessibility issues").option("-f, --format <format>","Output format: table, json, csv, sarif, html","table").option("--wcag <level>","WCAG level: A, AA, AAA","AA").option("--deep","Run a deeper scan with additional checks (Pro plan)").option("--remote","Run scan on WebAbility servers instead of locally").option("--exit","Exit with code 1 if issues found (CI mode)").option("--no-axe","Skip axe-core (use only WebAbility detectors)").option("--viewport <size>","Viewport: mobile, tablet, desktop","desktop").option("--show","Open browser and highlight issues visually").action(async(e,t)=>{if(e.startsWith("http")||(e=`https://${e}`),t.deep){let i=E();if(!i)return t.format==="table"&&m(),k("--deep requires a Pro plan. Run `abilyo login` first."),process.exit(1);try{let s=await fetch("https://api.webability.io/cli/verify",{headers:{Authorization:`Bearer ${i}`}});if(s.ok){if(!(await s.json()).active)return t.format==="table"&&m(),k("Your account does not have an active Pro plan."),console.log(a.dim(" Upgrade at https://abilyo.com/pricing")),process.exit(1)}else{if(s.status===401||s.status===403)return t.format==="table"&&m(),k("Your API key is invalid or expired. Run `abilyo login` again."),process.exit(1);console.error(a.yellow(` \u26A0 Could not verify your Pro plan (server returned ${s.status}). Continuing, but --deep may be rejected.`))}}catch{console.error(a.yellow(" \u26A0 Could not verify Pro plan status (offline). --deep analysis will be skipped without a network connection."))}}if(t.remote){await Ne(e,t);return}t.format==="table"&&m();let n=Date.now(),o=I({text:"Scanning...",prefixText:" "}).start(),r;try{r=await Y(e,{wcag:t.wcag,deep:t.deep??!1,apiKey:E()??void 0,show:t.show??!1,viewport:t.viewport,includeAxe:t.axe!==!1});let i=((Date.now()-n)/1e3).toFixed(1);if(o.succeed(`Scanned ${r.issues.length} issues in ${i}s`),t.format==="json"){let s={...r,issues:r.issues.map(({source:l,...d})=>d)};console.log(JSON.stringify(s,null,2))}else if(t.format==="csv")W(r.issues.map(s=>({severity:s.impact,wcag:s.wcag,message:s.message,element:s.selector})));else if(t.format==="sarif")console.log(JSON.stringify(Re(r),null,2));else if(t.format==="html"){let{toHtmlReport:s}=await import("@webability/core");console.log(s(r))}else{if(te(r.engineWarnings??[]),j(r.summary.total===0?100:Math.max(0,100-r.summary.total),e),N(r.summary),r.issues.length>0){let{generateDevSuggestion:s}=await import("@webability/core"),l=r.framework||"plain-css",d=[],f=new Map;for(let c of r.issues){let g=s(c,l),b=`${c.type}|${g.fix.slice(0,50)}`,x=f.get(b);x!==void 0?(d[x].count++,d[x].selectors.length<3&&d[x].selectors.push(c.selector.slice(0,50))):(f.set(b,d.length),d.push({suggestion:g,issue:c,count:1,selectors:[c.selector.slice(0,50)]}))}d.sort((c,g)=>{let b={critical:0,serious:1,moderate:2,minor:3};return(b[c.issue.impact]??2)-(b[g.issue.impact]??2)});for(let c of d.slice(0,20))R({severity:c.issue.impact,wcag:c.issue.wcag,problem:c.suggestion.problem,fix:c.suggestion.fix,count:c.count,selector:c.count===1?c.issue.selector:void 0,selectors:c.count>1?c.selectors:void 0,contrast:c.suggestion.contrast,before:c.suggestion.before,after:c.suggestion.after,html:c.issue.html});d.length>20&&(console.log(a.dim(` ... +${d.length-20} more`)),console.log())}L({hasIssues:r.issues.length>0,url:e.replace("https://","")})}t.show&&r.issues.length>0&&(console.log(a.cyan(" \u25C6 Browser open \u2014 issues highlighted. Press Enter to close.")),console.log(),await new Promise(s=>{process.stdin.resume(),process.stdin.once("data",()=>{process.stdin.pause(),s()})}),await _(r)),t.exit&&r.summary.total>0&&process.exit(1)}catch(i){o.stop(),r&&await _(r).catch(()=>{}),k(i.message),process.exit(1)}});async function Ne(e,t){let n=E();if(!n)return t.format==="table"&&m(),k("--remote requires login. Run `wa login` first."),process.exit(1);t.format==="table"&&m();let o=I({text:"Scanning (remote)...",prefixText:" "}).start();try{let r=await K(e,n,l=>{o.text=l});o.text="Fetching report...";let i=await U(r.key,n);if(o.stop(),!i)return k("Report not found"),process.exit(1);let s=[];if(i.axe?.violations)for(let l of i.axe.violations)for(let d of l.nodes||[])s.push({severity:l.impact||"moderate",wcag:l.id,message:l.description,element:d.target?.[0]});if(i.ByFunctions)for(let l of i.ByFunctions)for(let d of l.issues||[])s.push({severity:d.impact||"moderate",wcag:d.code||"",message:d.description||"",element:d.element});if(t.format==="json")oe({url:e,score:i.score,totalElements:i.totalElements,issues:s});else if(t.format==="csv")W(s);else{j(i.score||0,e),N({critical:0,serious:s.filter(l=>l.severity==="serious").length,moderate:s.filter(l=>l.severity==="moderate").length,minor:0,total:s.length});for(let l of s.slice(0,20))R({severity:l.severity,wcag:l.wcag,problem:l.message,fix:"Review this issue",selector:l.element});L()}t.exit&&s.length>0&&process.exit(1)}catch(r){o.stop(),k(r.message),process.exit(1)}}h.command("flow <url>").description("Scan a multi-page user journey \u2014 one consolidated report").option("--wcag <level>","WCAG level: A, AA, AAA","AA").action(async(e,t)=>{e.startsWith("http")||(e=`https://${e}`),m(),console.log(a.bold(" Flow Scan")),console.log(),console.log(` Starting at: ${a.cyan(e)}`),console.log(a.dim(" Click through your user journey. Close the browser when done.")),console.log();let n=0,o=await Q(e,{wcag:t.wcag,onPageScan:(s,l)=>{n++,console.log(` ${a.green("\u2713")} ${a.dim(String(n).padStart(2))} ${s.replace(/^https?:\/\//,"").slice(0,60)} ${a.dim("\u2014 "+l+" issues")}`)}});console.log(),console.log(a.bold(" \u2500\u2500\u2500 Flow Report \u2500\u2500\u2500")),console.log(),console.log(` Pages scanned: ${a.cyan(o.pages.length)}`),console.log(` Unique issues: ${a.cyan(o.uniqueIssues.length)}`),console.log(` Total duration: ${a.dim((o.totalDuration/1e3).toFixed(1)+"s")}`),console.log();let r={critical:0,serious:0,moderate:0,minor:0};o.uniqueIssues.forEach(s=>{r[s.impact]++}),N({...r,total:o.uniqueIssues.length});let i=o.uniqueIssues.slice(0,15);for(let s of i){let l=s.pageCount>1?a.yellow(` \xD7${s.pageCount} pages`):"";if(R({severity:s.impact,wcag:s.wcag,problem:s.message,fix:s.fix?.suggestedValue?`Set ${s.fix.attribute}="${s.fix.suggestedValue}"`:"Review this issue",selector:s.selector}),s.pageCount>1){console.log(a.dim(` Found on${l}:`));for(let d of s.foundOn.slice(0,3))console.log(a.dim(" "+d.replace(/^https?:\/\//,"")))}}o.uniqueIssues.length>15&&console.log(a.dim(` ... +${o.uniqueIssues.length-15} more unique issues`)),console.log(),L({hasIssues:o.uniqueIssues.length>0,url:e.replace("https://","")})});h.command("init").description("Create .webability.yml config file").action(()=>{m(),Z(),console.log()});h.command("login").description("Authenticate with your WebAbility account").option("-k, --key <key>","API key directly (for CI/CD)").action(async e=>{if(m(),e.key){T(e.key),console.log(a.green(" \u2713 API key saved.")),console.log();return}let t=I({text:"Starting login...",prefixText:" "}).start();try{let n=await fetch("https://api.webability.io/cli/device-code",{method:"POST",headers:{"Content-Type":"application/json"}});if(!n.ok){t.stop();let c="https://app.webability.io/settings/api-keys";console.log(" Open this URL to get your API key:"),console.log(),console.log(` ${a.cyan(c)}`),console.log(),console.log(` Then run: ${a.cyan("abilyo login --key YOUR_KEY")}`),console.log(),se(c);return}let{deviceCode:o,userCode:r,verificationUrl:i,expiresIn:s}=await n.json();t.stop(),console.log(` ${a.bold("Login to WebAbility")}`),console.log(),console.log(` Open: ${a.cyan(i)}`),console.log(` Code: ${a.bold.yellow(r)}`),console.log(),se(i);let l=I({text:"Waiting for approval...",prefixText:" "}).start(),d=3e3,f=Math.floor((s||300)*1e3/d);for(let c=0;c<f;c++){await new Promise(g=>setTimeout(g,d));try{let g=await fetch("https://api.webability.io/cli/device-token",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({deviceCode:o})});if(g.ok){let{token:x}=await g.json();T(x),l.succeed("Logged in!"),console.log();return}if((await g.json().catch(()=>({}))).error==="expired")return l.fail("Login expired. Run `abilyo login` again."),process.exit(1)}catch{}}l.fail("Login timed out."),process.exit(1)}catch{t.stop(),console.log(` Open: ${a.cyan("https://app.webability.io/settings/api-keys")}`),console.log(` Then: ${a.cyan("abilyo login --key YOUR_KEY")}`),console.log()}});function se(e){try{let{execFile:t}=ae("child_process"),n=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";t(n,[e],()=>{})}catch{}}h.command("whoami").description("Show current authentication status").action(()=>{let e=E();m(),e?(console.log(a.green(" Authenticated")),console.log(a.dim(` Key: ${e.slice(0,20)}...`))):(console.log(a.yellow(" Not authenticated")),console.log(a.dim(" Run `wa login` to authenticate"))),console.log()});h.command("logout").description("Remove saved API key").action(()=>{J(),m(),console.log(a.green(" Logged out. API key removed.")),console.log()});h.command("activate <code>").description("Activate Abilyo with your early access code").action(async e=>{m();let t=I({text:"Validating access code...",prefixText:" "}).start();try{if((await fetch("https://api.webability.io/cli/activate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:e.trim()})})).ok){O(e.trim()),t.succeed("Abilyo activated!"),console.log(),console.log(a.dim(" Run `abilyo scan <url>` to get started")),console.log();return}}catch{}e.trim().length>=8?(O(e.trim()),t.succeed("Abilyo activated!"),console.log(),console.log(a.dim(" Run `abilyo scan <url>` to get started")),console.log()):(t.fail("Invalid access code"),console.log(a.dim(" Request access at https://abilyo.com/early-access")),process.exit(1))});h.argument("[url]","URL to scan").action(e=>{e?h.parse(["node","abilyo","scan",e,...process.argv.slice(3)]):(m(),G()?(console.log(a.bold(" Commands:")),console.log(),console.log(` ${a.cyan("abilyo scan")} <url> Scan for accessibility issues`),console.log(` ${a.cyan("abilyo scan --deep")} <url> Deeper scan with additional checks (Pro)`),console.log(` ${a.cyan("abilyo scan --exit")} <url> CI mode \u2014 exit 1 if issues found`),console.log(` ${a.cyan("abilyo init")} Create .webability.yml config`),console.log(` ${a.cyan("abilyo login")} Authenticate with your account`),console.log(` ${a.cyan("abilyo whoami")} Show auth status`),console.log(),console.log(a.dim(" Example: abilyo scan localhost:3000")),console.log(a.dim(" Example: abilyo scan example.com --format json")),console.log(a.dim(" Example: abilyo scan example.com --deep --exit"))):(console.log(a.yellow(" Abilyo is invite-only during early access.")),console.log(),console.log(` ${a.cyan("abilyo activate")} <code> Activate with your access code`),console.log(),console.log(a.dim(" Request access at https://abilyo.com/early-access"))),console.log())});h.parse();function Re(e){let t=new Map;for(let o of e.issues)t.has(o.type)||t.set(o.type,{id:o.type,shortDescription:{text:o.message}});let n=e.engineWarnings??[];return{$schema:"https://json.schemastore.org/sarif-2.1.0.json",version:"2.1.0",runs:[{...n.length?{invocations:[{executionSuccessful:!0,toolExecutionNotifications:n.map(o=>({level:"warning",message:{text:o}}))}]}:{},tool:{driver:{name:"WebAbility",version:ie.version,informationUri:"https://webability.io",rules:Array.from(t.values())}},results:e.issues.map(o=>{let i={startLine:Number.isInteger(o.line)&&o.line>0?o.line:1,snippet:{text:o.html||o.selector}};return Number.isInteger(o.column)&&o.column>0&&(i.startColumn=o.column),{ruleId:o.type,level:o.impact==="critical"||o.impact==="serious"?"error":"warning",message:{text:o.message},locations:[{physicalLocation:{artifactLocation:{uri:e.url},region:i}}]}})}]}}
|
|
709
|
+
`;function ue(){let e=".webability.yml";if(Ue(e)){console.log(K.yellow(` ${e} already exists`));return}Ve(e,Ke),console.log(K.green(` Created ${e}`)),console.log(K.dim(" Edit it to configure your project URLs and thresholds"))}import{execFileSync as P}from"child_process";import{chmodSync as fe,existsSync as G,mkdirSync as Ge,readFileSync as he,writeFileSync as ye}from"fs";import{homedir as J}from"os";import{dirname as Je,join as H}from"path";var y="webability",I="https://mcp.webability.io/mcp",be="x-webability-token";function Y(e){return{url:I,headers:{[be]:e}}}function X(){let e=process.platform==="win32"?["claude.cmd","claude"]:["claude"];for(let t of e)try{return P(t,["--version"],{stdio:"ignore"}),t}catch{}return null}function He(){return X()!==null}function xe(e){try{return P(e,["mcp","get",y],{encoding:"utf-8",stdio:["ignore","pipe","ignore"]})}catch{return null}}function Ye(){return G(H(J(),".cursor"))}function ge(e){let t=X();if(!t)return{client:"claude",ok:!1,detail:"Claude Code CLI not found on PATH"};try{let o=xe(t);if(o!==null&&!o.includes(I))return{client:"claude",ok:!1,detail:`Claude Code already has an MCP server named "${y}" that isn't ours \u2014 left untouched. Remove it (claude mcp remove -s user ${y}) and re-run \`${c} mcp install\` to switch to the hosted server.`};if(o!==null)try{P(t,["mcp","remove","-s","user",y],{stdio:"ignore"})}catch{}return P(t,["mcp","add","-s","user","--transport","http",y,I,"--header",`${be}: ${e}`],{stdio:"ignore"}),{client:"claude",ok:!0,detail:"Claude Code"}}catch(o){return{client:"claude",ok:!1,detail:`claude mcp add failed: ${o.message}`}}}function me(e){let t=H(J(),".cursor","mcp.json");try{let o={};if(G(t)){let n=he(t,"utf-8").trim();n&&(o=JSON.parse(n))}let s=(o.mcpServers||{})[y];return s&&s.url!==I?{client:"cursor",ok:!1,detail:`~/.cursor/mcp.json already has an MCP server named "${y}" that isn't ours \u2014 left untouched. Remove that entry and re-run \`${c} mcp install\` to switch to the hosted server.`}:(o.mcpServers={...o.mcpServers||{},[y]:Y(e)},Ge(Je(t),{recursive:!0}),ye(t,`${JSON.stringify(o,null,2)}
|
|
710
|
+
`,{mode:384}),fe(t,384),{client:"cursor",ok:!0,detail:"Cursor (~/.cursor/mcp.json)"})}catch(o){return{client:"cursor",ok:!1,detail:`could not write ${t}: ${o.message}`}}}function we(){let e=[],t=X();if(t){let s=xe(t);if(s!==null)if(!s.includes(I))e.push({client:"claude",ok:!1,detail:`left "${y}" in Claude Code alone \u2014 it isn't ours`});else try{P(t,["mcp","remove","-s","user",y],{stdio:"ignore"}),e.push({client:"claude",ok:!0,detail:"Claude Code"})}catch(n){e.push({client:"claude",ok:!1,detail:`claude mcp remove failed: ${n.message}`})}}let o=H(J(),".cursor","mcp.json");if(G(o))try{let s=he(o,"utf-8").trim(),n=s?JSON.parse(s):{},a=n.mcpServers?.[y];a&&a.url===I?(delete n.mcpServers[y],ye(o,`${JSON.stringify(n,null,2)}
|
|
711
|
+
`,{mode:384}),fe(o,384),e.push({client:"cursor",ok:!0,detail:"Cursor (~/.cursor/mcp.json)"})):a&&e.push({client:"cursor",ok:!1,detail:`left "${y}" in ~/.cursor/mcp.json alone \u2014 it isn't ours`})}catch(s){e.push({client:"cursor",ok:!1,detail:`could not update ${o}: ${s.message}`})}return e}function L(e,t){if(t==="claude")return[ge(e)];if(t==="cursor")return[me(e)];let o=[];return He()&&o.push(ge(e)),Ye()&&o.push(me(e)),o}import S from"chalk";var u=S.dim,ve=S.bold,Q=S.blue,T=S.green,N=S.red,F=S.yellow,M=S.cyan,Xe=S.white;function f(){console.log(),console.log(` ${Q("\u25C6")} ${ve("Abilyo")} ${u("WCAG Scanner")}`),console.log()}function Z(e,t){let o=e>=80?T:e>=50?F:N,s=e>=80?T("\u2713"):e>=50?F("\u26A0"):N("\u2717"),n=e>=80?"Passing":e>=50?"Needs work":"Failing";console.log(` ${u("URL")} ${Xe(t)}`),console.log(` ${u("Score")} ${o.bold(e+"%")} ${s} ${u(n)}`),console.log(` ${u(" ")}${Qe(e)}`),console.log()}function Qe(e){let o=Math.round(e/100*32),s=32-o;return(e>=80?T:e>=50?F:N)("\u2501".repeat(o))+u("\u2501".repeat(s))}function D(e){let t=[];e.critical>0&&t.push(N.bold(`${e.critical} critical`)),e.serious>0&&t.push(F(`${e.serious} serious`)),e.moderate>0&&t.push(u(`${e.moderate} moderate`)),e.minor>0&&t.push(u(`${e.minor} minor`)),console.log(` ${ve(String(e.total))} issues ${t.join(u(" \xB7 "))}`),console.log()}function O(e){let t=e.severity==="critical"?N("\u2717"):e.severity==="serious"?F("\u25B2"):u("\u25CF"),o=u(`${e.wcag}`),s=e.count&&e.count>1?` \xD7${e.count}`:"",n=s?u(s):"",a=80-(6+e.wcag.length+s.length);if(console.log(` ${t} ${o}${n} ${e.problem.slice(0,a)}`),e.contrast?(console.log(` ${M("fix")} ${e.fix.slice(0,70)}`),console.log(` ${u(`${e.contrast.current}:1 \u2192 ${e.contrast.suggested}:1 (min ${e.contrast.required}:1)`)}`)):e.before&&e.after?console.log(` ${M("fix")} ${u(e.before)} ${u("\u2192")} ${T(e.after)}`):console.log(` ${M("fix")} ${e.fix.slice(0,70)}`),e.html&&e.count===1){let r=e.html.slice(0,60).replace(/\n/g," ");console.log(` ${u(r)}`)}if(e.selectors&&e.selectors.length>0&&e.count&&e.count>1){let r=e.selectors.slice(0,2).map(p=>p.slice(0,30)).join(u(", ")),l=e.count>2?u(` +${e.count-2} more`):"";console.log(` ${u(r+l)}`)}else e.selector&&console.log(` ${u(e.selector.slice(0,55))}`);console.log()}function ke(e){if(e.length){for(let t of e)console.log(` ${F.bold("\u26A0 WARNING")} ${F(t)}`);console.log()}}function _(e){console.log(u(" \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(` ${u("Next steps:")}`),console.log(` ${M(c+" scan "+(e.url||"<url>")+" --format json")} ${u("CI/CD output")}`),console.log(` ${M(c+" scan "+(e.url||"<url>")+" --format html > report.html")} ${u("share report")}`),console.log()),console.log(` ${Q("\u25C6")} ${u("Auto-fix at")} ${Q("abilyo.com")}`),console.log()}function b(e,t){console.error(N(` \u2717 Error: ${e}`)),t&&console.error(u(` ${t}`)),console.error()}function Ce(e){console.log(JSON.stringify(e,null,2))}function ee(e){console.log("severity,wcag,message,element");for(let t of e){let o=t.message.replace(/"/g,'""'),s=(t.element||"").replace(/"/g,'""');console.log(`${t.severity},${t.wcag},"${o}","${s}"`)}}var it=tt(et(import.meta.url)),Ae=JSON.parse(Ze(ot(it,"../package.json"),"utf-8")),x=new st;x.name(c).description("Abilyo by WebAbility \u2014 WCAG accessibility scanner").version(Ae.version);x.command("scan <url>").description("Scan a website for accessibility issues").option("-f, --format <format>","Output format: table, json, csv, sarif, html","table").option("--wcag <level>","WCAG level: A, AA, AAA","AA").option("--deep","Run a deeper scan with additional checks (Pro plan)").option("--remote","Run scan on WebAbility servers instead of locally").option("--exit","Exit with code 1 if issues found (CI mode)").option("--no-axe","Skip axe-core (use only WebAbility detectors)").option("--viewport <size>","Viewport: mobile, tablet, desktop","desktop").option("--show","Open browser and highlight issues visually").action(async(e,t)=>{if(e.startsWith("http")||(e=`https://${e}`),t.deep){let a=C();if(!a)return t.format==="table"&&f(),b(`--deep needs Abilyo Pro. Run \`${c} login\`, then \`${c} upgrade\`.`),process.exit(1);try{let r=await fetch("https://api.webability.io/cli/verify",{headers:{Authorization:`Bearer ${a}`}});if(r.ok){if(!(await r.json()).active)return t.format==="table"&&f(),b("--deep needs Abilyo Pro."),console.log(` Unlock it now: ${i.cyan(`${c} upgrade`)}`),console.log(i.dim(" Pro adds dynamic-content checks, keyboard-trap detection and AI fixes.")),process.exit(1)}else{if(r.status===401||r.status===403)return t.format==="table"&&f(),b(`Your API key is invalid or expired. Run \`${c} login\` again.`),process.exit(1);console.error(i.yellow(` \u26A0 Could not verify your Pro plan (server returned ${r.status}). Continuing, but --deep may be rejected.`))}}catch{console.error(i.yellow(" \u26A0 Could not verify Pro plan status (offline). --deep analysis will be skipped without a network connection."))}}if(t.remote){await nt(e,t);return}t.format==="table"&&f();let o=Date.now(),s=B({text:"Scanning...",prefixText:" "}).start(),n;try{n=await ce(e,{wcag:t.wcag,deep:t.deep??!1,apiKey:C()??void 0,show:t.show??!1,viewport:t.viewport,includeAxe:t.axe!==!1});let a=((Date.now()-o)/1e3).toFixed(1),r=n.issues.length;if(s.succeed(r===0?`No issues found in ${a}s`:`Found ${r} issue${r===1?"":"s"} in ${a}s`),t.format==="json"){let l={...n,issues:n.issues.map(({source:p,...g})=>g)};console.log(JSON.stringify(l,null,2))}else if(t.format==="csv")ee(n.issues.map(l=>({severity:l.impact,wcag:l.wcag,message:l.message,element:l.selector})));else if(t.format==="sarif")console.log(JSON.stringify(rt(n),null,2));else if(t.format==="html"){let{toHtmlReport:l}=await import("@webability/core");console.log(l(n))}else{if(ke(n.engineWarnings??[]),Z(n.summary.total===0?100:Math.max(0,100-n.summary.total),e),D(n.summary),n.issues.length>0){let{generateDevSuggestion:l}=await import("@webability/core"),p=n.framework||"plain-css",g=[],h=new Map;for(let d of n.issues){let k=l(d,p),w=`${d.type}|${k.fix.slice(0,50)}`,$=h.get(w);$!==void 0?(g[$].count++,g[$].selectors.length<3&&g[$].selectors.push(d.selector.slice(0,50))):(h.set(w,g.length),g.push({suggestion:k,issue:d,count:1,selectors:[d.selector.slice(0,50)]}))}g.sort((d,k)=>{let w={critical:0,serious:1,moderate:2,minor:3};return(w[d.issue.impact]??2)-(w[k.issue.impact]??2)});for(let d of g.slice(0,20))O({severity:d.issue.impact,wcag:d.issue.wcag,problem:d.suggestion.problem,fix:d.suggestion.fix,count:d.count,selector:d.count===1?d.issue.selector:void 0,selectors:d.count>1?d.selectors:void 0,contrast:d.suggestion.contrast,before:d.suggestion.before,after:d.suggestion.after,html:d.issue.html});g.length>20&&(console.log(i.dim(` ... +${g.length-20} more`)),console.log())}_({hasIssues:n.issues.length>0,url:e.replace("https://","")})}t.show&&n.issues.length>0&&(console.log(i.cyan(" \u25C6 Browser open \u2014 issues highlighted. Press Enter to close.")),console.log(),await new Promise(l=>{process.stdin.resume(),process.stdin.once("data",()=>{process.stdin.pause(),l()})}),await U(n)),t.exit&&n.summary.total>0&&process.exit(1)}catch(a){s.stop(),n&&await U(n).catch(()=>{}),b(a.message.split(`
|
|
712
|
+
`)[0],"Check the URL loads in a browser, then run the scan again."),process.exit(1)}});async function nt(e,t){let o=C();if(!o)return t.format==="table"&&f(),b(`--remote requires login. Run \`${c} login\` first.`),process.exit(1);t.format==="table"&&f();let s=B({text:"Scanning (remote)...",prefixText:" "}).start();try{let n=await ie(e,o,l=>{s.text=l});s.text="Fetching report...";let a=await ne(n.key,o);if(s.stop(),!a)return b("Report not found.","Try again, or drop --remote to scan locally."),process.exit(1);let r=[];if(a.axe?.violations)for(let l of a.axe.violations)for(let p of l.nodes||[])r.push({severity:l.impact||"moderate",wcag:l.id,message:l.description,element:p.target?.[0]});if(a.ByFunctions)for(let l of a.ByFunctions)for(let p of l.issues||[])r.push({severity:p.impact||"moderate",wcag:p.code||"",message:p.description||"",element:p.element});if(t.format==="json")Ce({url:e,score:a.score,totalElements:a.totalElements,issues:r});else if(t.format==="csv")ee(r);else{Z(a.score||0,e),D({critical:0,serious:r.filter(l=>l.severity==="serious").length,moderate:r.filter(l=>l.severity==="moderate").length,minor:0,total:r.length});for(let l of r.slice(0,20))O({severity:l.severity,wcag:l.wcag,problem:l.message,fix:"Review this issue",selector:l.element});_()}t.exit&&r.length>0&&process.exit(1)}catch(n){s.stop(),b(n.message),process.exit(1)}}x.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 o=0,s;try{s=await pe(e,{wcag:t.wcag,onPageScan:(r,l)=>{o++,console.log(` ${i.green("\u2713")} ${i.dim(String(o).padStart(2))} ${r.replace(/^https?:\/\//,"").slice(0,60)} ${i.dim("\u2014 "+l+" issues")}`)}})}catch(r){return b(r.message.split(`
|
|
713
|
+
`)[0],`Check the URL loads in a browser, then run \`${c} flow\` again.`),process.exit(1)}console.log(),console.log(i.bold(" \u2500\u2500\u2500 Flow Report \u2500\u2500\u2500")),console.log(),console.log(` Pages scanned: ${i.cyan(s.pages.length)}`),console.log(` Unique issues: ${i.cyan(s.uniqueIssues.length)}`),console.log(` Total duration: ${i.dim((s.totalDuration/1e3).toFixed(1)+"s")}`),console.log();let n={critical:0,serious:0,moderate:0,minor:0};s.uniqueIssues.forEach(r=>{n[r.impact]++}),D({...n,total:s.uniqueIssues.length});let a=s.uniqueIssues.slice(0,15);for(let r of a){let l=r.pageCount>1?i.yellow(` \xD7${r.pageCount} pages`):"";if(O({severity:r.impact,wcag:r.wcag,problem:r.message,fix:r.fix?.suggestedValue?`Set ${r.fix.attribute}="${r.fix.suggestedValue}"`:"Review this issue",selector:r.selector}),r.pageCount>1){console.log(i.dim(` Found on${l}:`));for(let p of r.foundOn.slice(0,3))console.log(i.dim(" "+p.replace(/^https?:\/\//,"")))}}s.uniqueIssues.length>15&&console.log(i.dim(` ... +${s.uniqueIssues.length-15} more unique issues`)),console.log(),_({hasIssues:s.uniqueIssues.length>0,url:e.replace("https://","")})});x.command("init").description("Create .webability.yml config file").action(()=>{f(),ue(),console.log()});x.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){q(e.key),console.log(i.green(" \u2713 API key saved.")),console.log(),te(L(e.key)),await oe(e.key);return}let t=B({text:"Starting login...",prefixText:" "}).start();try{let o=await fetch("https://api.webability.io/cli/device-code",{method:"POST",headers:{"Content-Type":"application/json"}});if(!o.ok)return t.fail(`Could not start login (server returned ${o.status}).`),console.log(i.dim(` Try again in a moment: \`${c} login\``)),console.log(i.dim(" Still stuck? support@webability.io")),console.log(),process.exit(1);let{deviceCode:s,userCode:n,verificationUrl:a,expiresIn:r}=await o.json();t.stop(),console.log(` ${i.bold("Login to WebAbility")}`),console.log(),console.log(` Open: ${i.cyan(a)}`),console.log(` Code: ${i.bold.yellow(n)}`),console.log(),Se(a);let l=B({text:"Waiting for approval...",prefixText:" "}).start(),p=3e3,g=Math.floor((r||300)*1e3/p);for(let h=0;h<g;h++){await new Promise(d=>setTimeout(d,p));try{let d=await fetch("https://api.webability.io/cli/device-token",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({deviceCode:s})});if(d.ok){let{token:w}=await d.json();q(w),l.succeed("Logged in."),console.log(),te(L(w)),await oe(w);return}if((await d.json().catch(()=>({}))).error==="expired")return l.fail(`Login expired. Run \`${c} login\` again.`),process.exit(1)}catch{}}l.fail("Login timed out."),process.exit(1)}catch{return t.fail("Could not reach api.webability.io."),console.log(i.dim(` Check your connection, then run \`${c} login\` again.`)),console.log(),process.exit(1)}});var z=i.dim(" "+"\u2500".repeat(58)),at=["scan webability.io and list the critical issues","fix the colour-contrast failures on this page","check this component for keyboard traps","is this form accessible to a screen reader?"];function te(e){let t=e.filter(o=>o.ok);if(!t.length){console.log(i.yellow(" No supported editor found for automatic setup.")),console.log(` Install Claude Code or Cursor, then run ${i.cyan(`${c} mcp install`)}.`),console.log(i.dim(` Any other MCP client: ${i.cyan(`${c} mcp print`)} gives you the config block.`)),console.log();for(let o of e.filter(s=>!s.ok))console.log(i.dim(` \u26A0 ${o.client}: ${o.detail}`));e.some(o=>!o.ok)&&console.log();return}console.log(z),console.log(i.bold(" Abilyo is now inside your editor")),console.log();for(let o of t)console.log(` ${i.green("\u2713")} ${o.detail}`);console.log(),console.log(i.dim(" Restart the editor, then ask it:"));for(let o of at)console.log(` ${i.cyan(`"${o}"`)}`);console.log(z),console.log();for(let o of e.filter(s=>!s.ok))console.log(i.dim(` \u26A0 ${o.client}: ${o.detail}`))}async function oe(e){let t="free",o="";try{let s=await fetch("https://api.webability.io/cli/verify",{headers:{Authorization:`Bearer ${e}`}});if(s.ok){let n=await s.json();t=n.plan||"free",o=n.email||""}}catch{}if(t==="pro"){console.log(` ${i.green("\u2726 Abilyo Pro")}${o?i.dim(` \u2014 ${o}`):""}`),console.log(i.dim(" Deep scans, AI fixes and unlimited MCP calls are unlocked.")),console.log();return}console.log(z),console.log(` ${i.yellow("You are on Free.")} Free finds the issues. ${i.bold("Pro fixes them.")}`),console.log(),console.log(` ${i.cyan(`${c} scan --deep`)} dynamic content, focus order, keyboard traps`),console.log(` ${i.cyan("AI fixes")} real code edits in your editor, not a to-do list`),console.log(` ${i.cyan("CI reports")} SARIF + HTML, unlimited MCP calls`),console.log(),console.log(` Upgrade in under a minute: ${i.bold.cyan(`${c} upgrade`)}`),console.log(z),console.log()}function Se(e){try{let{execFile:t}=Fe("child_process"),o=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";t(o,[e],()=>{})}catch{}}var $e=x.command("mcp").description("Connect Abilyo to your AI editor (Claude Code, Cursor)");$e.command("install").description("Register the Abilyo MCP server with every editor found").option("-c, --client <client>","Only one client: claude, cursor").action(e=>{f();let t=C();if(!t)return b(`Not logged in. Run \`${c} login\` \u2014 it connects your editor for you.`),process.exit(1);if(e.client&&e.client!=="claude"&&e.client!=="cursor")return b(`Unknown client "${e.client}". Use claude or cursor.`),process.exit(1);te(L(t,e.client))});$e.command("print").description("Print the MCP config block (contains your token \u2014 do not share)").action(()=>{let e=C();if(!e)return f(),b(`Not logged in. Run \`${c} login\` first.`),process.exit(1);console.log(JSON.stringify({mcpServers:{[y]:Y(e)}},null,2))});x.command("upgrade").description("Upgrade to Abilyo Pro \u2014 deep scans and AI fixes").action(async()=>{f();let e=C();if(!e)return b(`Log in first: \`${c} login\``),process.exit(1);let t=B({text:"Opening checkout...",prefixText:" "}).start();try{let o=await fetch("https://api.webability.io/cli/checkout",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e}`},body:JSON.stringify({returnUrl:"https://abilyo.com"})});if(!o.ok)return t.fail(`Could not start checkout (server returned ${o.status}).`),console.log(i.dim(" Upgrade in the browser instead: https://webability.io/pricing")),console.log(),process.exit(1);let{url:s}=await o.json();t.succeed("Checkout ready \u2014 opening your browser."),console.log(),console.log(` ${i.cyan(s)}`),console.log(),Se(s)}catch{return t.fail("Could not reach api.webability.io."),console.log(i.dim(" Upgrade in the browser instead: https://webability.io/pricing")),console.log(),process.exit(1)}});x.command("whoami").description("Show auth status and plan").action(async()=>{let e=C();if(f(),!e){console.log(i.yellow(" Not signed in.")),console.log(` Run ${i.cyan(`${c} login`)} \u2014 it signs you in and connects your editor.`),console.log();return}console.log(i.green(" Signed in")),console.log(i.dim(` Key: ${e.slice(0,12)}\u2026`)),console.log(),await oe(e)});x.command("logout").description("Remove saved API key and disconnect editors").action(()=>{let e=we();re(),f(),console.log(i.green(" Logged out. API key removed."));for(let t of e)console.log(t.ok?i.dim(` Disconnected ${t.detail}`):i.yellow(` ${t.detail}`));console.log(i.dim(` Sign back in any time: \`${c} login\``)),console.log()});x.command("activate <code>").description("Activate Abilyo with your early access code").action(async e=>{f();let t=B({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){V(e.trim()),t.succeed("Abilyo activated."),console.log(),console.log(` Next: ${i.cyan(`${c} scan <url>`)} ${i.dim("\u2014 your first scan takes seconds")}`),console.log();return}}catch{}e.trim().length>=8?(V(e.trim()),t.succeed("Abilyo activated."),console.log(),console.log(` Next: ${i.cyan(`${c} scan <url>`)} ${i.dim("\u2014 your first scan takes seconds")}`),console.log()):(t.fail("Invalid access code."),console.log(i.dim(" Request access at https://abilyo.com")),console.log(),process.exit(1))});x.argument("[url]","URL to scan").action(e=>{e?x.parse(["node",c,"scan",e,...process.argv.slice(3)]):(f(),ae()?(console.log(i.bold(" Commands:")),console.log(),console.log(` ${i.cyan(`${c} scan`)} <url> Scan a page for accessibility issues`),console.log(` ${i.cyan(`${c} scan --deep`)} <url> Dynamic content, focus order, keyboard traps ${i.dim("(Pro)")}`),console.log(` ${i.cyan(`${c} scan --exit`)} <url> CI mode \u2014 exit 1 if issues found`),console.log(` ${i.cyan(`${c} flow`)} <url> Scan a whole journey, one report`),console.log(` ${i.cyan(`${c} login`)} Sign in and connect your AI editor`),console.log(` ${i.cyan(`${c} mcp install`)} Re-connect Claude Code / Cursor`),console.log(` ${i.cyan(`${c} init`)} Create .webability.yml config`),console.log(` ${i.cyan(`${c} whoami`)} Show auth status and plan`),console.log(` ${i.cyan(`${c} upgrade`)} Unlock deep scans + AI fixes ${i.dim("(Pro)")}`),console.log(),console.log(i.bold(" Examples:")),console.log(),console.log(i.dim(` ${c} scan localhost:3000 your dev server`)),console.log(i.dim(` ${c} scan example.com --show highlight issues in a browser`)),console.log(i.dim(` ${c} scan example.com --format sarif upload to GitHub code scanning`)),console.log(i.dim(` ${c} scan example.com --exit fail the build on issues`)),console.log(i.dim(` ${c} scan example.com --deep the checks axe-core cannot do`)),console.log(),console.log(i.dim(` In your editor, after \`${c} login\`:`)),console.log(i.dim(' "scan staging.example.com and fix the contrast failures"'))):(console.log(i.yellow(" Abilyo is invite-only during early access.")),console.log(),console.log(` ${i.cyan(`${c} login`)} Already have a WebAbility account? Start here`),console.log(` ${i.cyan(`${c} activate`)} <code> Activate with an early-access code`),console.log(),console.log(i.dim(" Request access at https://abilyo.com"))),console.log())});x.parse();function rt(e){let t=new Map;for(let s of e.issues)t.has(s.type)||t.set(s.type,{id:s.type,shortDescription:{text:s.message}});let o=e.engineWarnings??[];return{$schema:"https://json.schemastore.org/sarif-2.1.0.json",version:"2.1.0",runs:[{...o.length?{invocations:[{executionSuccessful:!0,toolExecutionNotifications:o.map(s=>({level:"warning",message:{text:s}}))}]}:{},tool:{driver:{name:"WebAbility",version:Ae.version,informationUri:"https://webability.io",rules:Array.from(t.values())}},results:e.issues.map(s=>{let a={startLine:Number.isInteger(s.line)&&s.line>0?s.line:1,snippet:{text:s.html||s.selector}};return Number.isInteger(s.column)&&s.column>0&&(a.startColumn=s.column),{ruleId:s.type,level:s.impact==="critical"||s.impact==="serious"?"error":"warning",message:{text:s.message},locations:[{physicalLocation:{artifactLocation:{uri:e.url},region:a}}]}})}]}}
|
package/package.json
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webability/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Abilyo by WebAbility — WCAG accessibility scanner for your terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
+
"webability": "./dist/cli.js",
|
|
7
8
|
"abilyo": "./dist/cli.js"
|
|
8
9
|
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
9
13
|
"keywords": [
|
|
10
14
|
"a11y",
|
|
11
15
|
"abilyo",
|
|
@@ -31,7 +35,7 @@
|
|
|
31
35
|
"ora": "^8.1.0",
|
|
32
36
|
"playwright": "^1.49.0",
|
|
33
37
|
"yaml": "^2.6.0",
|
|
34
|
-
"@webability/core": "1.
|
|
38
|
+
"@webability/core": "1.3.1"
|
|
35
39
|
},
|
|
36
40
|
"devDependencies": {
|
|
37
41
|
"@types/node": "^25.6.0",
|
package/visual-dogfood.mjs
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
// Visual dogfood test — runs structural scan + visual audit on webability.io
|
|
2
|
-
// to compare what each catches.
|
|
3
|
-
|
|
4
|
-
import { chromium } from 'playwright'
|
|
5
|
-
import fs from 'fs/promises'
|
|
6
|
-
|
|
7
|
-
const URL = 'https://webability.io'
|
|
8
|
-
const API_URL = 'https://api.webability.io'
|
|
9
|
-
|
|
10
|
-
const browser = await chromium.launch({ headless: true })
|
|
11
|
-
const ctx = await browser.newContext({ viewport: { width: 1280, height: 720 }, deviceScaleFactor: 1 })
|
|
12
|
-
const page = await ctx.newPage()
|
|
13
|
-
console.log(`Loading ${URL}...`)
|
|
14
|
-
await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 30000 })
|
|
15
|
-
await page.waitForTimeout(1500) // let JS settle
|
|
16
|
-
|
|
17
|
-
console.log('Capturing screenshot...')
|
|
18
|
-
const buf = await page.screenshot({ type: 'png', fullPage: false })
|
|
19
|
-
const screenshot = buf.toString('base64')
|
|
20
|
-
console.log(` Size: ${(buf.length / 1024).toFixed(0)}KB`)
|
|
21
|
-
|
|
22
|
-
console.log('Calling /cli/visual-audit...')
|
|
23
|
-
const t0 = Date.now()
|
|
24
|
-
const res = await fetch(`${API_URL}/cli/visual-audit`, {
|
|
25
|
-
method: 'POST',
|
|
26
|
-
headers: { 'Content-Type': 'application/json' },
|
|
27
|
-
body: JSON.stringify({
|
|
28
|
-
screenshot,
|
|
29
|
-
platform: 'web',
|
|
30
|
-
screenName: URL,
|
|
31
|
-
brandColors: ['#0052CC', '#47FFF7', '#1B3DB4'], // WebAbility brand
|
|
32
|
-
knownIssues: [], // empty so vision model gets full scope
|
|
33
|
-
}),
|
|
34
|
-
})
|
|
35
|
-
const elapsed = ((Date.now() - t0) / 1000).toFixed(1)
|
|
36
|
-
console.log(` HTTP ${res.status} in ${elapsed}s`)
|
|
37
|
-
|
|
38
|
-
if (!res.ok) {
|
|
39
|
-
console.error('Error:', await res.text())
|
|
40
|
-
await browser.close()
|
|
41
|
-
process.exit(1)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const data = await res.json()
|
|
45
|
-
console.log(`\n=== Visual issues found: ${data.count} ===\n`)
|
|
46
|
-
for (const issue of data.issues) {
|
|
47
|
-
const region = issue.region ? `[${issue.region.x},${issue.region.y} ${issue.region.width}x${issue.region.height}]` : ''
|
|
48
|
-
console.log(` ${issue.severity.toUpperCase().padEnd(8)} WCAG ${issue.wcag} — ${issue.message}`)
|
|
49
|
-
if (issue.fix?.suggestedValue) console.log(` fix: ${issue.fix.suggestedValue}`)
|
|
50
|
-
if (region) console.log(` region: ${region}`)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
await fs.writeFile('/tmp/visual-dogfood-result.json', JSON.stringify(data, null, 2))
|
|
54
|
-
console.log('\nFull result saved to /tmp/visual-dogfood-result.json')
|
|
55
|
-
await browser.close()
|