@webability/cli 1.5.0 → 1.5.1

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.
Files changed (3) hide show
  1. package/README.md +3 -3
  2. package/dist/cli.js +46 -45
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -28,7 +28,7 @@ webability scan example.com --viewport mobile
28
28
  | `flow <url>` | Scan a multi-page user journey into one consolidated report |
29
29
  | `review [paths...]` | Static code review of changed files — offline, free, no login |
30
30
  | `init` | Create `.webability.yml` config |
31
- | `mcp` | Connect Abilyo to your AI editor (Claude Code, Cursor) |
31
+ | `mcp` | Connect WebAbility to your AI editor (Claude Code, Cursor) |
32
32
  | `login` / `logout` / `whoami` | Account auth and status |
33
33
 
34
34
  ```bash
@@ -68,8 +68,8 @@ threshold:
68
68
  ## Links
69
69
 
70
70
  - [WebAbility](https://webability.io) — AI-powered web accessibility platform
71
- - [Abilyo](https://abilyo.com) — Developer tools for accessibility testing
72
- - [Documentation](https://abilyo.com/docs)
71
+ - [WebAbility](https://webability.io) — Developer tools for accessibility testing
72
+ - [Documentation](https://webability.io/docs)
73
73
 
74
74
  ## License
75
75
 
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- var ft=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,n)=>(typeof require<"u"?require:t)[n]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});import{readFileSync as dn}from"fs";import{fileURLToPath as un}from"url";import{dirname as pn,join as gn}from"path";import{basename as mt}from"path";var ht=mt(process.argv[1]??""),m=ht==="abilyo"?"abilyo":"webability";import{Command as fn}from"commander";import d from"chalk";import J from"ora";var yt=process.env.WEBABILITY_API_BASE||"https://api.webability.io";async function we(e,t={},n){let o=await fetch(`${yt}${e}`,{...t,headers:{"Content-Type":"application/json",Authorization:`Bearer ${n}`,Origin:"https://app.webability.io",...t.headers}});if(!o.ok){let i=await o.text().catch(()=>"");throw new Error(`API ${o.status}: ${i.slice(0,200)}`)}return o.json()}async function oe(e,t,n){let o=await we("/graphql",{method:"POST",body:JSON.stringify({query:e,variables:t})},n);if(o.errors?.length)throw new Error(o.errors[0].message);return o.data}var q=class extends Error{constructor(n){super(`Scan is taking longer than 3 minutes (job ${n}). It's still running \u2014 check back with --job ${n}.`);this.jobId=n;this.name="ScanTimeoutError"}jobId};async function xe(e,t){return we("/cli/review",{method:"POST",body:JSON.stringify(e)},t)}async function ie(e,t,n,o=60){for(let i=0;i<o;i++){let{getAccessibilityReportByJobId:s}=await oe("query($jobId: String!) { getAccessibilityReportByJobId(jobId: $jobId) { status error result { savedReport { key } } } }",{jobId:e},t);if(s.status==="done")return{key:s.result.savedReport.key};if(s.status==="error"||s.status==="not_found")throw new Error(s.error||"Scan failed");n?.(`Scanning... (${(i+1)*3}s)`),await new Promise(r=>setTimeout(r,3e3))}throw new q(e)}async function Ce(e,t,n){n?.("Starting scan...");let{startAccessibilityReportJob:o}=await oe("query($url: String!) { startAccessibilityReportJob(url: $url, use_cache: false) { jobId } }",{url:e},t),i=o.jobId;return n?.(`Job ${i.slice(0,8)}... created`),ie(i,t,n)}async function Ae(e,t){let{fetchReportByR2Key:n}=await oe("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 bt from"conf";var V=new bt({projectName:"webability",schema:{apiKey:{type:"string",default:""},accessCode:{type:"string",default:""},defaultFormat:{type:"string",default:"table",enum:["table","json","csv"]}}});function P(){return process.env.WEBABILITY_API_KEY||V.get("apiKey")||""}function se(e){V.set("apiKey",e)}function vt(){return V.get("accessCode")||""}function re(e){V.set("accessCode",e)}function ke(){return!!vt()||!!P()}function Se(){V.clear()}import{scan as wt,detectFramework as xt,generateDevSuggestion as Ct}from"@webability/core";var At={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 kt(e){return At[e.type]||""}var $e={A:["wcag2a"],AA:["wcag2a","wcag2aa","wcag21aa","wcag22aa"],AAA:["wcag2a","wcag2aa","wcag2aaa","wcag21aa","wcag22aa"]},St={critical:"#ef4444",serious:"#f97316",moderate:"#eab308",minor:"#6b7280"},$t={critical:"\u2717",serious:"\u25B2",moderate:"\u25CF",minor:"\u25CB"};function Ft(e,t,n){let o=n||"plain-css";return`
2
+ import{execFile as dn}from"child_process";import{readFileSync as un}from"fs";import{fileURLToPath as pn}from"url";import{dirname as gn,join as fn}from"path";import{basename as mt}from"path";var ht=mt(process.argv[1]??""),h=ht==="abilyo"?"abilyo":"webability";import{Command as mn}from"commander";import d from"chalk";import J from"ora";var bt=process.env.WEBABILITY_API_BASE||"https://api.webability.io";async function ve(e,t={},n){let i=await fetch(`${bt}${e}`,{...t,headers:{"Content-Type":"application/json",Authorization:`Bearer ${n}`,Origin:"https://app.webability.io",...t.headers}});if(!i.ok){let o=await i.text().catch(()=>"");throw new Error(`API ${i.status}: ${o.slice(0,200)}`)}return i.json()}async function ie(e,t,n){let i=await ve("/graphql",{method:"POST",body:JSON.stringify({query:e,variables:t})},n);if(i.errors?.length)throw new Error(i.errors[0].message);return i.data}var q=class extends Error{constructor(n){super(`Scan is taking longer than 3 minutes (job ${n}). It's still running \u2014 check back with --job ${n}.`);this.jobId=n;this.name="ScanTimeoutError"}jobId};async function xe(e,t){return ve("/cli/review",{method:"POST",body:JSON.stringify(e)},t)}async function oe(e,t,n,i=60){for(let o=0;o<i;o++){let{getAccessibilityReportByJobId:s}=await ie("query($jobId: String!) { getAccessibilityReportByJobId(jobId: $jobId) { status error result { savedReport { key } } } }",{jobId:e},t);if(s.status==="done")return{key:s.result.savedReport.key};if(s.status==="error"||s.status==="not_found")throw new Error(s.error||"Scan failed");n?.(`Scanning... (${(o+1)*3}s)`),await new Promise(r=>setTimeout(r,3e3))}throw new q(e)}async function Ce(e,t,n){n?.("Starting scan...");let{startAccessibilityReportJob:i}=await ie("query($url: String!) { startAccessibilityReportJob(url: $url, use_cache: false) { jobId } }",{url:e},t),o=i.jobId;return n?.(`Job ${o.slice(0,8)}... created`),oe(o,t,n)}async function Ae(e,t){let{fetchReportByR2Key:n}=await ie("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 yt from"conf";var V=new yt({projectName:"webability",schema:{apiKey:{type:"string",default:""},accessCode:{type:"string",default:""},defaultFormat:{type:"string",default:"table",enum:["table","json","csv"]}}});function P(){return process.env.WEBABILITY_API_KEY||V.get("apiKey")||""}function se(e){V.set("apiKey",e)}function wt(){return V.get("accessCode")||""}function re(e){V.set("accessCode",e)}function ke(){return!!wt()||!!P()}function Se(){V.clear()}import{scan as vt,detectFramework as xt,generateDevSuggestion as Ct}from"@webability/core";var At={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 kt(e){return At[e.type]||""}var $e={A:["wcag2a"],AA:["wcag2a","wcag2aa","wcag21aa","wcag22aa"],AAA:["wcag2a","wcag2aa","wcag2aaa","wcag21aa","wcag22aa"]},St={critical:"#ef4444",serious:"#f97316",moderate:"#eab308",minor:"#6b7280"},$t={critical:"\u2717",serious:"\u25B2",moderate:"\u25CF",minor:"\u25CB"};function Ft(e,t,n){let i=n||"plain-css";return`
3
3
  (function() {
4
- const issues = ${JSON.stringify(e.map((s,r)=>{let c=(()=>{try{return Ct(s,o)}catch{return null}})();return{id:r,selector:s.selector,impact:s.impact,wcag:s.wcag,type:s.type,message:s.message.slice(0,200),problem:c?.problem||"",recommendedFix:c?.fix||"",wcagLink:c?.wcagLink||`https://www.w3.org/WAI/WCAG21/Understanding/${s.wcag.replace(/\./g,"")}`,whyMatters:kt(s),fixDesc:s.fix?.suggestedValue?`Set ${s.fix.attribute}="${s.fix.suggestedValue}"`:"",fixAttr:s.fix?.attribute||"",fixVal:s.fix?.suggestedValue||"",fixCur:s.fix?.currentValue||"",canApply:!!(s.fix?.suggestedValue&&!s.fix?.needsManualReview)}}))};
4
+ const issues = ${JSON.stringify(e.map((s,r)=>{let c=(()=>{try{return Ct(s,i)}catch{return null}})();return{id:r,selector:s.selector,impact:s.impact,wcag:s.wcag,type:s.type,message:s.message.slice(0,200),problem:c?.problem||"",recommendedFix:c?.fix||"",wcagLink:c?.wcagLink||`https://www.w3.org/WAI/WCAG21/Understanding/${s.wcag.replace(/\./g,"")}`,whyMatters:kt(s),fixDesc:s.fix?.suggestedValue?`Set ${s.fix.attribute}="${s.fix.suggestedValue}"`:"",fixAttr:s.fix?.attribute||"",fixVal:s.fix?.suggestedValue||"",fixCur:s.fix?.currentValue||"",canApply:!!(s.fix?.suggestedValue&&!s.fix?.needsManualReview)}}))};
5
5
  const colors = ${JSON.stringify(St)};
6
6
  const icons = ${JSON.stringify($t)};
7
7
  const API_KEY = ${JSON.stringify(t||"")};
@@ -29,26 +29,26 @@ var ft=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,n)=>(
29
29
  style.textContent = [
30
30
  // Brand tokens (matches .impeccable.md)
31
31
  ":root{--ab-primary:#0052CC;--ab-primary-dark:#0033ED;--ab-accent:#47FFF7;--ab-navy:#1B3DB4;--ab-bg:#FFFFFF;--ab-surface:#F9FAFB;--ab-text:#111827;--ab-text-muted:#4B5563;--ab-border:#E5E7EB;--ab-success:#15803D;--ab-warning:#CA8A04;--ab-danger:#DC2626;--ab-bg-tint:#F4F8FF}",
32
- "@keyframes abilyo-reveal{from{opacity:0;transform:scale(.97)}to{opacity:1;transform:scale(1)}}",
32
+ "@keyframes webability-reveal{from{opacity:0;transform:scale(.97)}to{opacity:1;transform:scale(1)}}",
33
33
 
34
34
  // Overlays on the page
35
- ".abilyo-overlay{position:absolute;pointer-events:auto;z-index:2147483640;outline:2px solid red;outline-offset:2px;border-radius:3px;box-sizing:border-box;cursor:pointer;transition:opacity .2s,outline-color .2s;animation:abilyo-reveal .25s ease-out backwards}",
36
- ".abilyo-overlay:hover{outline-width:3px;outline-offset:1px}",
37
- ".abilyo-overlay.abilyo-active{outline:3px solid #0052CC!important;outline-offset:2px}",
38
- ".abilyo-overlay.abilyo-hidden{opacity:0;pointer-events:none}",
39
- ".abilyo-overlay.abilyo-dimmed{opacity:.12;filter:blur(1px)}",
40
- ".abilyo-label{position:absolute;top:-22px;left:-2px;font:600 11px/14px Inter,system-ui,sans-serif;color:#fff;padding:2px 6px;border-radius:3px 3px 0 0;white-space:nowrap;pointer-events:none;z-index:2147483641}",
35
+ ".webability-overlay{position:absolute;pointer-events:auto;z-index:2147483640;outline:2px solid red;outline-offset:2px;border-radius:3px;box-sizing:border-box;cursor:pointer;transition:opacity .2s,outline-color .2s;animation:webability-reveal .25s ease-out backwards}",
36
+ ".webability-overlay:hover{outline-width:3px;outline-offset:1px}",
37
+ ".webability-overlay.webability-active{outline:3px solid #0052CC!important;outline-offset:2px}",
38
+ ".webability-overlay.webability-hidden{opacity:0;pointer-events:none}",
39
+ ".webability-overlay.webability-dimmed{opacity:.12;filter:blur(1px)}",
40
+ ".webability-label{position:absolute;top:-22px;left:-2px;font:600 11px/14px Inter,system-ui,sans-serif;color:#fff;padding:2px 6px;border-radius:3px 3px 0 0;white-space:nowrap;pointer-events:none;z-index:2147483641}",
41
41
 
42
42
  // Panel container
43
- "#abilyo-panel{position:fixed;top:12px;right:12px;z-index:2147483647;width:400px;max-width:calc(100vw - 24px);max-height:calc(100vh - 24px);background:#fff;border-radius:14px;box-shadow:0 12px 40px rgba(0,82,204,.12),0 0 0 1px rgba(0,82,204,.08);font:13px Inter,system-ui,-apple-system,sans-serif;color:#111827;display:flex;flex-direction:column;overflow:hidden}",
44
- "#abilyo-panel *{box-sizing:border-box;min-width:0}",
43
+ "#webability-panel{position:fixed;top:12px;right:12px;z-index:2147483647;width:400px;max-width:calc(100vw - 24px);max-height:calc(100vh - 24px);background:#fff;border-radius:14px;box-shadow:0 12px 40px rgba(0,82,204,.12),0 0 0 1px rgba(0,82,204,.08);font:13px Inter,system-ui,-apple-system,sans-serif;color:#111827;display:flex;flex-direction:column;overflow:hidden}",
44
+ "#webability-panel *{box-sizing:border-box;min-width:0}",
45
45
 
46
46
  // Header with hero score
47
47
  ".ap-header{padding:18px 20px 14px;border-bottom:1px solid #E5E7EB;flex-shrink:0}",
48
48
  ".ap-header-top{display:flex;align-items:center;gap:8px;margin-bottom:10px}",
49
49
  ".ap-brand{display:flex;align-items:center;gap:8px;flex:1}",
50
50
  ".ap-logo{width:18px;height:18px;background:#0052CC;border-radius:4px;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:800;font-size:11px;font-family:Inter,system-ui}",
51
- ".ap-title{font-weight:600;font-size:13px;color:#4B5563;letter-spacing:-.01em}",
51
+ ".wa-title{font-weight:600;font-size:13px;color:#4B5563;letter-spacing:-.01em}",
52
52
  ".ap-close{width:26px;height:26px;border:none;background:transparent;border-radius:6px;cursor:pointer;font-size:14px;color:#6B7280;display:flex;align-items:center;justify-content:center}",
53
53
  ".ap-close:hover{background:#F3F4F6;color:#111827}",
54
54
  ".ap-score-row{display:flex;align-items:baseline;gap:10px}",
@@ -145,9 +145,9 @@ var ft=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,n)=>(
145
145
  ".apd-ai-code{font-size:12px;font-family:'SF Mono',Menlo,Consolas,monospace;color:#1B3DB4;background:#fff;padding:7px 10px;border-radius:5px;word-break:break-all;border:1px solid #DBEAFE}",
146
146
 
147
147
  // Minimized pill \u2014 branded
148
- "#abilyo-pill{display:none;position:fixed;top:12px;right:12px;z-index:2147483647;background:#0052CC;color:#fff;border-radius:10px;padding:9px 14px;cursor:pointer;font:700 13px/1 Inter,system-ui;box-shadow:0 4px 16px rgba(0,82,204,.35);transition:transform .15s}",
149
- "#abilyo-pill:hover{transform:scale(1.05)}",
150
- "#abilyo-pill .ap-pill-num{font-weight:800;font-feature-settings:'tnum';margin-right:4px}",
148
+ "#webability-pill{display:none;position:fixed;top:12px;right:12px;z-index:2147483647;background:#0052CC;color:#fff;border-radius:10px;padding:9px 14px;cursor:pointer;font:700 13px/1 Inter,system-ui;box-shadow:0 4px 16px rgba(0,82,204,.35);transition:transform .15s}",
149
+ "#webability-pill:hover{transform:scale(1.05)}",
150
+ "#webability-pill .wa-pill-num{font-weight:800;font-feature-settings:'tnum';margin-right:4px}",
151
151
  ].join('\\n');
152
152
  document.head.appendChild(style);
153
153
 
@@ -167,13 +167,13 @@ var ft=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,n)=>(
167
167
  const color = colors[issue.impact] || '#ef4444';
168
168
  const delay = Math.min(visibleIdx * 30, 600);
169
169
  const overlay = h('div', {
170
- className: 'abilyo-overlay',
170
+ className: 'webability-overlay',
171
171
  'data-impact': issue.impact,
172
172
  'data-id': String(issue.id),
173
173
  style: { top: (rect.top+window.scrollY)+'px', left: (rect.left+window.scrollX)+'px', width: rect.width+'px', height: rect.height+'px', outlineColor: color, background: color+'0d', animationDelay: delay+'ms' },
174
174
  onClick: () => selectIssue(issue.id),
175
175
  }, [
176
- h('div', { className: 'abilyo-label', style: { background: color, animationDelay: delay+'ms' } }, icons[issue.impact]+' '+issue.wcag)
176
+ h('div', { className: 'webability-label', style: { background: color, animationDelay: delay+'ms' } }, icons[issue.impact]+' '+issue.wcag)
177
177
  ]);
178
178
  document.body.appendChild(overlay);
179
179
  overlays.push(overlay);
@@ -256,7 +256,7 @@ var ft=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,n)=>(
256
256
  overlayToggle.classList.toggle('active', overlaysVisible);
257
257
  overlayToggle.setAttribute('aria-checked', String(overlaysVisible));
258
258
  overlayMeta.textContent = overlaysVisible ? 'on' : 'off';
259
- document.querySelectorAll('.abilyo-overlay').forEach(o => { (o).style.display = overlaysVisible ? '' : 'none'; });
259
+ document.querySelectorAll('.webability-overlay').forEach(o => { (o).style.display = overlaysVisible ? '' : 'none'; });
260
260
  };
261
261
  const overlayToggle = h('div', {
262
262
  className: 'ap-toggle active',
@@ -281,7 +281,7 @@ var ft=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,n)=>(
281
281
  labelsToggle.classList.toggle('active', labelsVisible);
282
282
  labelsToggle.setAttribute('aria-checked', String(labelsVisible));
283
283
  labelsMeta.textContent = labelsVisible ? 'on' : 'off';
284
- document.querySelectorAll('.abilyo-label').forEach(l => { (l).style.display = labelsVisible ? '' : 'none'; });
284
+ document.querySelectorAll('.webability-label').forEach(l => { (l).style.display = labelsVisible ? '' : 'none'; });
285
285
  };
286
286
  const labelsToggle = h('div', {
287
287
  className: 'ap-toggle active',
@@ -301,8 +301,8 @@ var ft=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,n)=>(
301
301
  const minBtn = h('button', { className: 'ap-close', title: 'Minimize', onClick: () => { panel.style.display = 'none'; pill.style.display = 'block'; } }, '\\u2014');
302
302
 
303
303
  // Branded minimized pill
304
- const pillNum = h('span', { className: 'ap-pill-num' }, String(issues.length));
305
- const pill = h('div', { id: 'abilyo-pill', onClick: () => { panel.style.display = 'flex'; pill.style.display = 'none'; } }, [
304
+ const pillNum = h('span', { className: 'wa-pill-num' }, String(issues.length));
305
+ const pill = h('div', { id: 'webability-pill', onClick: () => { panel.style.display = 'flex'; pill.style.display = 'none'; } }, [
306
306
  pillNum, document.createTextNode(' issues')
307
307
  ]);
308
308
 
@@ -310,7 +310,7 @@ var ft=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,n)=>(
310
310
  const headerTop = h('div', { className: 'ap-header-top' }, [
311
311
  h('div', { className: 'ap-brand' }, [
312
312
  h('div', { className: 'ap-logo' }, 'A'),
313
- h('div', { className: 'ap-title' }, 'Abilyo'),
313
+ h('div', { className: 'wa-title' }, 'WebAbility'),
314
314
  ]),
315
315
  minBtn,
316
316
  ]);
@@ -322,7 +322,7 @@ var ft=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,n)=>(
322
322
  ]),
323
323
  ]);
324
324
 
325
- const panel = h('div', { id: 'abilyo-panel' }, [
325
+ const panel = h('div', { id: 'webability-panel' }, [
326
326
  h('div', { className: 'ap-header' }, [headerTop, scoreRow]),
327
327
  toolbarEl,
328
328
  filtersEl,
@@ -485,7 +485,7 @@ var ft=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,n)=>(
485
485
  appliedFixes.add(issue.id);
486
486
  previewState.id = -1;
487
487
  const ov = overlays[issue.id];
488
- if (ov) { ov.style.outlineColor = '#15803D'; ov.querySelector('.abilyo-label').style.background = '#15803D'; ov.querySelector('.abilyo-label').textContent = 'Fixed'; }
488
+ if (ov) { ov.style.outlineColor = '#15803D'; ov.querySelector('.webability-label').style.background = '#15803D'; ov.querySelector('.webability-label').textContent = 'Fixed'; }
489
489
  }
490
490
 
491
491
  function selectIssue(id) {
@@ -495,14 +495,14 @@ var ft=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,n)=>(
495
495
 
496
496
  if (previewState.id >= 0 && previewState.id !== id) revertPreview();
497
497
 
498
- document.querySelectorAll('.abilyo-overlay').forEach(o => {
499
- o.classList.remove('abilyo-active');
500
- o.classList.add('abilyo-dimmed');
498
+ document.querySelectorAll('.webability-overlay').forEach(o => {
499
+ o.classList.remove('webability-active');
500
+ o.classList.add('webability-dimmed');
501
501
  });
502
502
  const ov = overlays[id];
503
503
  if (ov) {
504
- ov.classList.remove('abilyo-dimmed');
505
- ov.classList.add('abilyo-active');
504
+ ov.classList.remove('webability-dimmed');
505
+ ov.classList.add('webability-active');
506
506
  ov.scrollIntoView({ behavior: 'smooth', block: 'center' });
507
507
  }
508
508
 
@@ -656,7 +656,7 @@ var ft=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,n)=>(
656
656
  else if (alt.attribute) t.setAttribute(alt.attribute, alt.value);
657
657
  appliedFixes.add(issue.id); previewState.id = -1;
658
658
  const o = overlays[issue.id];
659
- if (o) { o.style.outlineColor = '#0052CC'; const l = o.querySelector('.abilyo-label'); if (l) { l.style.background = '#0052CC'; l.textContent = '\\u2713 Fixed'; } }
659
+ if (o) { o.style.outlineColor = '#0052CC'; const l = o.querySelector('.webability-label'); if (l) { l.style.background = '#0052CC'; l.textContent = '\\u2713 Fixed'; } }
660
660
  aBtn.classList.add('applied'); aBtn.textContent = 'Applied'; pBtn.style.display = 'none';
661
661
  });
662
662
  altActions.appendChild(pBtn);
@@ -680,7 +680,7 @@ var ft=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,n)=>(
680
680
 
681
681
  function clearSpotlight() {
682
682
  activeId = -1;
683
- document.querySelectorAll('.abilyo-overlay').forEach(o => { o.classList.remove('abilyo-active','abilyo-dimmed'); });
683
+ document.querySelectorAll('.webability-overlay').forEach(o => { o.classList.remove('webability-active','webability-dimmed'); });
684
684
  detailEl.style.display = 'none';
685
685
  renderList();
686
686
  }
@@ -690,14 +690,14 @@ var ft=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,n)=>(
690
690
  overlays.forEach((ov, i) => {
691
691
  if (!ov) return;
692
692
  const impact = issues[i] && issues[i].impact;
693
- ov.classList.toggle('abilyo-hidden', !activeFilters[impact]);
693
+ ov.classList.toggle('webability-hidden', !activeFilters[impact]);
694
694
  });
695
695
  renderList();
696
696
  }
697
697
 
698
698
  renderList();
699
699
  })();
700
- `}async function Fe(e,t){let n=$e[t.wcag.toUpperCase()]??$e.AA,o=["mobile","tablet","desktop"].includes(t.viewport??"")?t.viewport:"desktop",s=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}),c=await(await s.newContext({viewport:o==="mobile"?{width:390,height:844}:o==="tablet"?{width:820,height:1180}:{width:1280,height:720}})).newPage(),f=e.startsWith("http")?e:`https://${e}`;await c.goto(f,{waitUntil:"domcontentloaded",timeout:3e4});let u=await xt(c),h=await wt(c,{wcagTags:n,includeAxe:t.includeAxe??!0,dismissModals:!0,deep:t.deep,deepApiUrl:t.deep?"https://api.webability.io":void 0,deepApiKey:t.deep?t.apiKey:void 0});return t.show&&h.issues.length>0?(await c.evaluate(Ft(h.issues,t.apiKey,u.framework)),await c.evaluate(()=>window.scrollTo(0,0)),{...h,framework:u.framework,_browser:s}):(await s.close(),{...h,framework:u.framework})}async function ae(e){e._browser&&(await e._browser.close(),delete e._browser)}import{scan as Et,detectFramework as It}from"@webability/core";var Ee={A:["wcag2a"],AA:["wcag2a","wcag2aa","wcag21aa","wcag22aa"],AAA:["wcag2a","wcag2aa","wcag2aaa","wcag21aa","wcag22aa"]};async function Ie(e,t={}){let n=Ee[(t.wcag||"AA").toUpperCase()]??Ee.AA,o=e.startsWith("http")?e:`https://${e}`,i=Date.now(),r=await(await import("playwright")).chromium.launch({headless:!1}),f=await(await r.newContext({viewport:{width:1280,height:720}})).newPage(),u=[],h="plain-css",l=!1,y=!1,b=new Set,x=async()=>{if(l){y=!0;return}l=!0;try{await f.waitForLoadState("domcontentloaded",{timeout:5e3}).catch(()=>{}),await f.waitForTimeout(800);let a=f.url();if(b.has(a)){l=!1;return}b.add(a),(!h||h==="plain-css")&&(h=(await It(f).catch(()=>({framework:"plain-css"}))).framework);let p=await Et(f,{wcagTags:n,includeAxe:!0,dismissModals:!1});u.push({url:a,issues:p.issues,scannedAt:new Date().toISOString()}),t.onPageScan?.(a,p.issues.length)}catch{}finally{l=!1,y&&(y=!1,setTimeout(x,100))}};f.on("framenavigated",a=>{a===f.mainFrame()&&setTimeout(x,600)}),await f.goto(o,{waitUntil:"domcontentloaded",timeout:3e4}),await new Promise(a=>{r.on("disconnected",()=>a()),process.once("SIGINT",()=>a())}),await r.close().catch(()=>{});let F=new Map;for(let a of u)for(let p of a.issues){let g=`${p.type}::${p.selector}::${p.wcag}`,v=F.get(g);v?v.foundOn.includes(a.url)||(v.foundOn.push(a.url),v.pageCount=v.foundOn.length):F.set(g,{...p,foundOn:[a.url],pageCount:1})}return{pages:u,uniqueIssues:Array.from(F.values()).sort((a,p)=>{let g={critical:0,serious:1,moderate:2,minor:3},v=g[a.impact]??2,S=g[p.impact]??2;return v!==S?v-S:p.pageCount-a.pageCount}),framework:h,totalDuration:Date.now()-i}}import{writeFileSync as Rt,existsSync as Nt}from"fs";import le from"chalk";var Lt=`project: my-project
700
+ `}async function Fe(e,t){let n=$e[t.wcag.toUpperCase()]??$e.AA,i=["mobile","tablet","desktop"].includes(t.viewport??"")?t.viewport:"desktop",s=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}),c=await(await s.newContext({viewport:i==="mobile"?{width:390,height:844}:i==="tablet"?{width:820,height:1180}:{width:1280,height:720}})).newPage(),f=e.startsWith("http")?e:`https://${e}`;await c.goto(f,{waitUntil:"domcontentloaded",timeout:3e4});let u=await xt(c),b=await vt(c,{wcagTags:n,includeAxe:t.includeAxe??!0,dismissModals:!0,deep:t.deep,deepApiUrl:t.deep?"https://api.webability.io":void 0,deepApiKey:t.deep?t.apiKey:void 0});return t.show&&b.issues.length>0?(await c.evaluate(Ft(b.issues,t.apiKey,u.framework)),await c.evaluate(()=>window.scrollTo(0,0)),{...b,framework:u.framework,_browser:s}):(await s.close(),{...b,framework:u.framework})}async function ae(e){e._browser&&(await e._browser.close(),delete e._browser)}import{scan as Et,detectFramework as It}from"@webability/core";var Ee={A:["wcag2a"],AA:["wcag2a","wcag2aa","wcag21aa","wcag22aa"],AAA:["wcag2a","wcag2aa","wcag2aaa","wcag21aa","wcag22aa"]};async function Ie(e,t={}){let n=Ee[(t.wcag||"AA").toUpperCase()]??Ee.AA,i=e.startsWith("http")?e:`https://${e}`,o=Date.now(),r=await(await import("playwright")).chromium.launch({headless:!1}),f=await(await r.newContext({viewport:{width:1280,height:720}})).newPage(),u=[],b="plain-css",l=!1,m=!1,w=new Set,v=async()=>{if(l){m=!0;return}l=!0;try{await f.waitForLoadState("domcontentloaded",{timeout:5e3}).catch(()=>{}),await f.waitForTimeout(800);let a=f.url();if(w.has(a)){l=!1;return}w.add(a),(!b||b==="plain-css")&&(b=(await It(f).catch(()=>({framework:"plain-css"}))).framework);let p=await Et(f,{wcagTags:n,includeAxe:!0,dismissModals:!1});u.push({url:a,issues:p.issues,scannedAt:new Date().toISOString()}),t.onPageScan?.(a,p.issues.length)}catch{}finally{l=!1,m&&(m=!1,setTimeout(v,100))}};f.on("framenavigated",a=>{a===f.mainFrame()&&setTimeout(v,600)}),await f.goto(i,{waitUntil:"domcontentloaded",timeout:3e4}),await new Promise(a=>{r.on("disconnected",()=>a()),process.once("SIGINT",()=>a())}),await r.close().catch(()=>{});let F=new Map;for(let a of u)for(let p of a.issues){let g=`${p.type}::${p.selector}::${p.wcag}`,y=F.get(g);y?y.foundOn.includes(a.url)||(y.foundOn.push(a.url),y.pageCount=y.foundOn.length):F.set(g,{...p,foundOn:[a.url],pageCount:1})}return{pages:u,uniqueIssues:Array.from(F.values()).sort((a,p)=>{let g={critical:0,serious:1,moderate:2,minor:3},y=g[a.impact]??2,S=g[p.impact]??2;return y!==S?y-S:p.pageCount-a.pageCount}),framework:b,totalDuration:Date.now()-o}}import{writeFileSync as Rt,existsSync as Nt}from"fs";import le from"chalk";var Lt=`project: my-project
701
701
  urls:
702
702
  - http://localhost:3000
703
703
  standard: WCAG2.1AA
@@ -706,16 +706,17 @@ ignore: []
706
706
  threshold:
707
707
  critical: 0
708
708
  serious: 5
709
- `;function Re(){let e=".webability.yml";if(Nt(e)){console.log(le.yellow(` ${e} already exists`));return}Rt(e,Lt),console.log(le.green(` Created ${e}`)),console.log(le.dim(" Edit it to configure your project URLs and thresholds"))}import{execFileSync as G}from"child_process";import{chmodSync as Te,existsSync as ce,mkdirSync as Tt,readFileSync as Be,writeFileSync as Pe}from"fs";import{homedir as de}from"os";import{dirname as Bt,join as ue}from"path";var $="webability",z="https://mcp.webability.io/mcp",Me="x-webability-token";function pe(e){return{url:z,headers:{[Me]:e}}}function ge(){let e=process.platform==="win32"?["claude.cmd","claude"]:["claude"];for(let t of e)try{return G(t,["--version"],{stdio:"ignore"}),t}catch{}return null}function Pt(){return ge()!==null}function Oe(e){try{return G(e,["mcp","get",$],{encoding:"utf-8",stdio:["ignore","pipe","ignore"]})}catch{return null}}function Mt(){return ce(ue(de(),".cursor"))}function Ne(e){let t=ge();if(!t)return{client:"claude",ok:!1,detail:"Claude Code CLI not found on PATH"};try{let n=Oe(t);if(n!==null&&!n.includes(z))return{client:"claude",ok:!1,detail:`Claude Code already has an MCP server named "${$}" that isn't ours \u2014 left untouched. Remove it (claude mcp remove -s user ${$}) and re-run \`${m} mcp install\` to switch to the hosted server.`};if(n!==null)try{G(t,["mcp","remove","-s","user",$],{stdio:"ignore"})}catch{}return G(t,["mcp","add","-s","user","--transport","http",$,z,"--header",`${Me}: ${e}`],{stdio:"ignore"}),{client:"claude",ok:!0,detail:"Claude Code"}}catch(n){return{client:"claude",ok:!1,detail:`claude mcp add failed: ${n.message}`}}}function Le(e){let t=ue(de(),".cursor","mcp.json");try{let n={};if(ce(t)){let i=Be(t,"utf-8").trim();i&&(n=JSON.parse(i))}let o=(n.mcpServers||{})[$];return o&&o.url!==z?{client:"cursor",ok:!1,detail:`~/.cursor/mcp.json already has an MCP server named "${$}" that isn't ours \u2014 left untouched. Remove that entry and re-run \`${m} mcp install\` to switch to the hosted server.`}:(n.mcpServers={...n.mcpServers||{},[$]:pe(e)},Tt(Bt(t),{recursive:!0}),Pe(t,`${JSON.stringify(n,null,2)}
710
- `,{mode:384}),Te(t,384),{client:"cursor",ok:!0,detail:"Cursor (~/.cursor/mcp.json)"})}catch(n){return{client:"cursor",ok:!1,detail:`could not write ${t}: ${n.message}`}}}function _e(){let e=[],t=ge();if(t){let o=Oe(t);if(o!==null)if(!o.includes(z))e.push({client:"claude",ok:!1,detail:`left "${$}" in Claude Code alone \u2014 it isn't ours`});else try{G(t,["mcp","remove","-s","user",$],{stdio:"ignore"}),e.push({client:"claude",ok:!0,detail:"Claude Code"})}catch(i){e.push({client:"claude",ok:!1,detail:`claude mcp remove failed: ${i.message}`})}}let n=ue(de(),".cursor","mcp.json");if(ce(n))try{let o=Be(n,"utf-8").trim(),i=o?JSON.parse(o):{},s=i.mcpServers?.[$];s&&s.url===z?(delete i.mcpServers[$],Pe(n,`${JSON.stringify(i,null,2)}
711
- `,{mode:384}),Te(n,384),e.push({client:"cursor",ok:!0,detail:"Cursor (~/.cursor/mcp.json)"})):s&&e.push({client:"cursor",ok:!1,detail:`left "${$}" in ~/.cursor/mcp.json alone \u2014 it isn't ours`})}catch(o){e.push({client:"cursor",ok:!1,detail:`could not update ${n}: ${o.message}`})}return e}function X(e,t){if(t==="claude")return[Ne(e)];if(t==="cursor")return[Le(e)];let n=[];return Pt()&&n.push(Ne(e)),Mt()&&n.push(Le(e)),n}import M from"chalk";var C=M.dim,je=M.bold,fe=M.blue,U=M.green,W=M.red,D=M.yellow,K=M.cyan,Ot=M.white;function k(){console.log(),console.log(` ${fe("\u25C6")} ${je("Abilyo")} ${C("WCAG Scanner")}`),console.log()}function me(e,t){let n=e>=80?U:e>=50?D:W,o=e>=80?U("\u2713"):e>=50?D("\u26A0"):W("\u2717"),i=e>=80?"Passing":e>=50?"Needs work":"Failing";console.log(` ${C("URL")} ${Ot(t)}`),console.log(` ${C("Score")} ${n.bold(e+"%")} ${o} ${C(i)}`),console.log(` ${C(" ")}${_t(e)}`),console.log()}function _t(e){let n=Math.round(e/100*32),o=32-n;return(e>=80?U:e>=50?D:W)("\u2501".repeat(n))+C("\u2501".repeat(o))}function H(e){let t=[];e.critical>0&&t.push(W.bold(`${e.critical} critical`)),e.serious>0&&t.push(D(`${e.serious} serious`)),e.moderate>0&&t.push(C(`${e.moderate} moderate`)),e.minor>0&&t.push(C(`${e.minor} minor`)),console.log(` ${je(String(e.total))} issues ${t.join(C(" \xB7 "))}`),console.log()}function Y(e){let t=e.severity==="critical"?W("\u2717"):e.severity==="serious"?D("\u25B2"):C("\u25CF"),n=C(`${e.wcag}`),o=e.count&&e.count>1?` \xD7${e.count}`:"",i=o?C(o):"",s=80-(6+e.wcag.length+o.length);if(console.log(` ${t} ${n}${i} ${e.problem.slice(0,s)}`),e.contrast?(console.log(` ${K("fix")} ${e.fix.slice(0,70)}`),console.log(` ${C(`${e.contrast.current}:1 \u2192 ${e.contrast.suggested}:1 (min ${e.contrast.required}:1)`)}`)):e.before&&e.after?console.log(` ${K("fix")} ${C(e.before)} ${C("\u2192")} ${U(e.after)}`):console.log(` ${K("fix")} ${e.fix.slice(0,70)}`),e.html&&e.count===1){let r=e.html.slice(0,60).replace(/\n/g," ");console.log(` ${C(r)}`)}if(e.selectors&&e.selectors.length>0&&e.count&&e.count>1){let r=e.selectors.slice(0,2).map(f=>f.slice(0,30)).join(C(", ")),c=e.count>2?C(` +${e.count-2} more`):"";console.log(` ${C(r+c)}`)}else e.selector&&console.log(` ${C(e.selector.slice(0,55))}`);console.log()}function De(e){if(e.length){for(let t of e)console.log(` ${D.bold("\u26A0 WARNING")} ${D(t)}`);console.log()}}function Q(e){console.log(C(" \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(` ${C("Next steps:")}`),console.log(` ${K(m+" scan "+(e.url||"<url>")+" --format json")} ${C("CI/CD output")}`),console.log(` ${K(m+" scan "+(e.url||"<url>")+" --format html > report.html")} ${C("share report")}`),console.log()),console.log(` ${fe("\u25C6")} ${C("Auto-fix at")} ${fe("abilyo.com")}`),console.log()}function I(e,t){console.error(W(` \u2717 Error: ${e}`)),t&&console.error(C(` ${t}`)),console.error()}function ze(e){console.log(JSON.stringify(e,null,2))}function he(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}"`)}}import{execFileSync as ne}from"child_process";import*as T from"fs";import*as O from"path";import L from"chalk";import{parse as jt,AST_NODE_TYPES as w}from"@typescript-eslint/typescript-estree";import{parse as zt,defaultTreeAdapter as N}from"parse5";function Dt(e){let t=[],n=null;try{n=jt(e,{jsx:!0,loc:!0,range:!0,errorOnUnknownASTType:!1,errorOnTypeScriptSyntacticAndSemanticIssues:!1})}catch(a){return t.push(a instanceof Error?a.message:String(a)),{root:null,all:[],parseErrors:t}}let o=[],i=null;function s(a){return{start:{line:a.start.line,column:a.start.column},end:{line:a.end.line,column:a.end.column}}}function r(a){switch(a.type){case w.JSXIdentifier:return a.name;case w.JSXMemberExpression:return`${r(a.object)}.${a.property.name}`;case w.JSXNamespacedName:return`${a.namespace.name}:${a.name.name}`;default:return"unknown"}}function c(a){let p=f(a.name);if(a.value==null)return{name:p,value:null,dynamic:!1};if(a.value.type===w.Literal&&typeof a.value.value=="string")return{name:p,value:a.value.value,dynamic:!1};if(a.value.type===w.JSXExpressionContainer){let g=a.value.expression;return g.type===w.Literal&&typeof g.value=="string"?{name:p,value:g.value,dynamic:!1}:g.type===w.Literal&&typeof g.value=="number"?{name:p,value:String(g.value),dynamic:!1}:g.type===w.UnaryExpression&&g.operator==="-"&&g.argument.type===w.Literal&&typeof g.argument.value=="number"?{name:p,value:String(-g.argument.value),dynamic:!1}:g.type===w.TemplateLiteral&&g.expressions.length===0?{name:p,value:g.quasis.map(v=>v.value.cooked).join(""),dynamic:!1}:{name:p,value:null,dynamic:!0}}return{name:p,value:null,dynamic:!0}}function f(a){return a.type===w.JSXIdentifier?a.name:`${a.namespace.name}-${a.name.name}`}function u(a){for(let p of a.children){if(p.type===w.JSXText&&p.value.trim().length>0)return!0;if(p.type===w.JSXExpressionContainer){if(p.expression.type===w.JSXEmptyExpression||p.expression.type===w.Literal&&p.expression.value===null)continue;return!0}if((p.type===w.JSXElement||p.type===w.JSXFragment)&&u(p))return!0}return!1}function h(a){let p=a.openingElement,g=r(p.name).toLowerCase(),v=p.attributes.filter(A=>A.type===w.JSXAttribute).map(c),S=v.find(A=>A.name==="alt"),_=v.find(A=>A.name==="aria-label");return g==="img"&&S&&!S.dynamic&&S.value&&S.value.trim().length>0||_&&!_.dynamic&&_.value&&_.value.trim().length>0||v.some(A=>A.name==="aria-labelledby")?!0:l(a)}function l(a){for(let p of a.children)if(p.type===w.JSXElement){if(h(p))return!0}else if(p.type===w.JSXFragment){if(l(p))return!0}else if(p.type===w.JSXExpressionContainer&&y(p.expression))return!0;return!1}function y(a){if(!a||typeof a!="object")return!1;if(a.type===w.JSXElement)return h(a);if(a.type===w.JSXFragment)return l(a);for(let p of Object.keys(a)){if(p==="parent"||p==="loc"||p==="range")continue;let g=a[p];if(Array.isArray(g)){for(let v of g)if(v&&typeof v=="object"&&"type"in v&&y(v))return!0}else if(g&&typeof g=="object"&&"type"in g&&y(g))return!0}return!1}function b(a,p){let g=a.openingElement,v=r(g.name),S=new Map,_=!1;for(let B of g.attributes)if(B.type===w.JSXAttribute){let j=c(B);S.set(j.name.toLowerCase(),j)}else _=!0;let A={tag:/^[a-z]/.test(v)?v.toLowerCase():v,attrs:S,children:[],parent:p,openTagRange:s(g.loc),hasTextContent:u(a),hasAccessibleChildContent:l(a),hasSpreadAttrs:_,isSynthesized:!1};o.push(A);for(let B of a.children)if(B.type===w.JSXElement)A.children.push(b(B,A));else if(B.type===w.JSXFragment)for(let j of B.children)j.type===w.JSXElement?A.children.push(b(j,A)):j.type===w.JSXExpressionContainer&&x(j.expression,A);else B.type===w.JSXExpressionContainer&&x(B.expression,A);return A}function x(a,p){if(!(!a||typeof a!="object")){if(a.type===w.JSXElement){let g=b(a,p);p?p.children.push(g):i||(i=g);return}if(a.type===w.JSXFragment){for(let g of a.children)if(g.type===w.JSXElement){let v=b(g,p);p?p.children.push(v):i||(i=v)}else g.type===w.JSXExpressionContainer&&x(g.expression,p);return}for(let g of Object.keys(a)){if(g==="parent"||g==="loc"||g==="range")continue;let v=a[g];if(Array.isArray(v))for(let S of v)S&&typeof S=="object"&&"type"in S&&x(S,p);else v&&typeof v=="object"&&"type"in v&&x(v,p)}}}function F(a){if(a.type===w.JSXElement){let p=b(a,null);i||(i=p);return}for(let p of Object.keys(a)){if(p==="parent"||p==="loc"||p==="range")continue;let g=a[p];if(Array.isArray(g))for(let v of g)v&&typeof v=="object"&&"type"in v&&F(v);else g&&typeof g=="object"&&"type"in g&&F(g)}}try{F(n)}catch(a){t.push(a instanceof Error?a.message:String(a))}return{root:i,all:o,parseErrors:t}}function Z(e){return N.isElementNode(e)}var Wt=/^\[(?:attr\.)?([^\]]+)\]$/,Jt=["[innerhtml]","[innertext]","[textcontent]"];function qt(e){let t=e.startTag??e;return{start:{line:t.startLine,column:t.startCol-1},end:{line:t.endLine,column:t.endCol-1}}}function Vt(e){return e.replace(/\{#[\s\S]*?#\}/g,t=>t.replace(/[^\n]/g," "))}function Gt(e){let t=[],n=zt(Vt(e),{sourceCodeLocationInfo:!0}),o=[],i=null;function s(l){let y=new Map;for(let b of l.attrs)y.set(b.name.toLowerCase(),{name:b.name.toLowerCase(),value:b.value,dynamic:!1});for(let b of l.attrs){let x=Wt.exec(b.name.toLowerCase());x&&!y.has(x[1])&&y.set(x[1],{name:x[1],value:null,dynamic:!0})}return y}function r(l){return N.isTextNode(l)?l.value:Z(l)?N.getChildNodes(l).map(r).join(""):""}function c(l){return Z(l)?l.attrs.some(y=>Jt.includes(y.name.toLowerCase()))?!0:N.getChildNodes(l).some(c):!1}function f(l){for(let y of N.getChildNodes(l)){if(!Z(y))continue;let b=N.getTagName(y).toLowerCase(),x=s(y),F=x.get("alt"),a=x.get("aria-label");if(b==="img"&&F?.value&&F.value.trim().length>0||a?.value&&a.value.trim().length>0||x.has("aria-labelledby")||f(y))return!0}return!1}function u(l,y){if(!Z(l)){if("childNodes"in l)for(let F of N.getChildNodes(l))u(F,y);return}let b=N.getNodeSourceCodeLocation(l),x={tag:N.getTagName(l).toLowerCase(),attrs:s(l),children:[],parent:y,openTagRange:b?qt(b):{start:{line:1,column:0},end:{line:1,column:0}},hasTextContent:r(l).trim().length>0||c(l),hasAccessibleChildContent:f(l),hasSpreadAttrs:!1,isSynthesized:b==null};o.push(x),i||(i=x),y&&y.children.push(x);for(let F of N.getChildNodes(l))u(F,x)}try{for(let l of N.getChildNodes(n))u(l,null)}catch(l){t.push(l instanceof Error?l.message:String(l))}return{root:o.find(l=>l.tag==="html")??i,all:o,parseErrors:t}}var R="Section 508 (2017 Revised Standards, 36 CFR Part 1194) incorporates WCAG 2.0 Level A/AA by reference (E205, E207). This finding maps to a WCAG 2.1 success criterion that is a superset of/consistent with the WCAG 2.0 baseline Section 508 requires.";function We(e,t){let n=e.attrs.get(t);return n?n.dynamic?!0:!!n.value&&n.value.trim().length>0:!1}function ee(e,t){return e.hasSpreadAttrs?!1:!e.attrs.has(t)}var Je={id:"img-alt",check(e){let t=[];for(let n of e.all)n.tag==="img"&&ee(n,"alt")&&t.push({ruleId:Je.id,message:'<img> is missing an "alt" attribute. Add alt text describing the image, or alt="" if it is purely decorative.',severity:"error",range:n.openTagRange,wcag:"WCAG 2.1 SC 1.1.1 Non-text Content (Level A)",section508:R});return t}},qe={id:"input-image-alt",check(e){let t=[];for(let n of e.all){if(n.tag!=="input")continue;let o=n.attrs.get("type");o&&!o.dynamic&&(o.value??"").toLowerCase()==="image"&&ee(n,"alt")&&t.push({ruleId:qe.id,message:'<input type="image"> is missing an "alt" attribute, which is required as its accessible name.',severity:"error",range:n.openTagRange,wcag:"WCAG 2.1 SC 1.1.1 Non-text Content (Level A)",section508:R})}return t}},Ve={id:"iframe-title",check(e){let t=[];for(let n of e.all){if(n.tag!=="iframe")continue;let o=n.attrs.get("title"),i=!!o&&!o.dynamic&&(!o.value||o.value.trim()==="");(ee(n,"title")||i)&&t.push({ruleId:Ve.id,message:'<iframe> is missing a "title" attribute describing its content or purpose.',severity:"error",range:n.openTagRange,wcag:"WCAG 2.1 SC 4.1.2 Name, Role, Value (Level A) / SC 2.4.1 Bypass Blocks (Level A)",section508:R})}return t}},Kt=new Set(["hidden","submit","reset","button","image"]);function Xt(e){let t=e.parent;for(;t;){if(t.tag==="label")return!0;t=t.parent}return!1}function Ge(e){for(let t of e.children)if(t.tag==="mat-label"||Ge(t))return!0;return!1}function Ut(e){if(!e.attrs.has("matinput")&&!e.attrs.has("matnativecontrol"))return!1;let t=e.parent;for(;t;){if(t.tag==="mat-form-field")return Ge(t);t=t.parent}return!1}function ye(e){return!!(We(e,"aria-label")||e.attrs.has("aria-labelledby")||We(e,"title")||Xt(e))}var Ke={id:"label-control",check(e){let t=[],n=new Set;for(let o of e.all){if(o.tag!=="label")continue;let i=o.attrs.get("for")??o.attrs.get("htmlfor");i&&!i.dynamic&&i.value&&n.add(i.value)}for(let o of e.all){if(!["input","textarea","select"].includes(o.tag)||o.hasSpreadAttrs)continue;let i=o.attrs.get("type");if(i&&!i.dynamic&&i.value&&Kt.has(i.value.toLowerCase())||Ut(o))continue;let s=o.attrs.get("id"),r=!!s&&!s.dynamic&&!!s.value&&n.has(s.value);!ye(o)&&!r&&t.push({ruleId:Ke.id,message:`<${o.tag}> has no accessible label. Add a <label>, aria-label, aria-labelledby, or wrap it in a <label> element.`,severity:"error",range:o.openTagRange,wcag:"WCAG 2.1 SC 1.3.1 Info and Relationships (Level A) / SC 4.1.2 Name, Role, Value (Level A)",section508:R})}return t}};function Xe(e){return e.hasTextContent||e.hasAccessibleChildContent}var Ue={id:"empty-link",check(e){let t=[];for(let n of e.all)n.tag==="a"&&(n.hasSpreadAttrs||n.attrs.has("href")&&(Xe(n)||ye(n)||t.push({ruleId:Ue.id,message:"<a> has no discernible text. Add link text, or an aria-label/aria-labelledby/title, so screen reader users know its purpose.",severity:"error",range:n.openTagRange,wcag:"WCAG 2.1 SC 2.4.4 Link Purpose (In Context) (Level A) / SC 4.1.2 Name, Role, Value (Level A)",section508:R})));return t}},He={id:"empty-button",check(e){let t=[];for(let n of e.all)n.tag==="button"&&(n.hasSpreadAttrs||Xe(n)||ye(n)||t.push({ruleId:He.id,message:"<button> has no discernible text. Add button text or an aria-label so screen reader users know what it does.",severity:"error",range:n.openTagRange,wcag:"WCAG 2.1 SC 4.1.2 Name, Role, Value (Level A)",section508:R}));return t}},Ht=new Set(["alert","alertdialog","application","article","banner","button","cell","checkbox","columnheader","combobox","complementary","contentinfo","definition","dialog","directory","document","feed","figure","form","grid","gridcell","group","heading","img","link","list","listbox","listitem","log","main","marquee","math","menu","menubar","menuitem","menuitemcheckbox","menuitemradio","navigation","none","note","option","presentation","progressbar","radio","radiogroup","region","row","rowgroup","rowheader","scrollbar","search","searchbox","separator","slider","spinbutton","status","switch","tab","table","tablist","tabpanel","term","textbox","timer","toolbar","tooltip","tree","treegrid","treeitem"]),Yt={button:"button",nav:"navigation",main:"main",header:"banner",footer:"contentinfo",aside:"complementary",form:"form",table:"table",ul:"list",ol:"list",li:"listitem",img:"img",h1:"heading",h2:"heading",h3:"heading",h4:"heading",h5:"heading",h6:"heading",progress:"progressbar",textarea:"textbox",dialog:"dialog"},Ye={id:"invalid-aria-role",check(e){let t=[];for(let n of e.all){let o=n.attrs.get("role");if(!o||o.dynamic||!o.value)continue;let s=o.value.trim().split(/\s+/).filter(Boolean).filter(r=>!Ht.has(r.toLowerCase()));s.length>0&&t.push({ruleId:Ye.id,message:`role="${o.value}" contains an unrecognized ARIA role (${s.join(", ")}). Assistive tech will ignore it, falling back to no semantic role.`,severity:"error",range:n.openTagRange,wcag:"WCAG 2.1 SC 4.1.2 Name, Role, Value (Level A)",section508:R})}return t}},Qe={id:"redundant-role",check(e){let t=[];for(let n of e.all){let o=n.attrs.get("role");if(!o||o.dynamic||!o.value)continue;let i=Yt[n.tag];i&&o.value.trim().toLowerCase()===i&&t.push({ruleId:Qe.id,message:`role="${i}" is redundant on <${n.tag}> \u2014 it already has that implicit role. Remove the attribute (or use the correct element if a different role was intended).`,severity:"information",range:n.openTagRange,wcag:"WCAG 2.1 SC 4.1.2 Name, Role, Value (Level A) \u2014 best practice",section508:R})}return t}},Ze={id:"positive-tabindex",check(e){let t=[];for(let n of e.all){let o=n.attrs.get("tabindex");if(!o||o.dynamic||o.value==null)continue;let i=Number.parseInt(o.value,10);Number.isFinite(i)&&i>0&&t.push({ruleId:Ze.id,message:`tabindex="${o.value}" is a positive value, which overrides natural tab order and creates confusing keyboard navigation. Use tabindex="0" or restructure the DOM order instead.`,severity:"warning",range:n.openTagRange,wcag:"WCAG 2.1 SC 2.4.3 Focus Order (Level A)",section508:R})}return t}},et={id:"html-lang",check(e){let t=[];for(let n of e.all){if(n.tag!=="html"||n.isSynthesized)continue;let o=n.attrs.get("lang"),i=!!o&&!o.dynamic&&(!o.value||o.value.trim()==="");(ee(n,"lang")||i)&&t.push({ruleId:et.id,message:'<html> is missing a "lang" attribute (e.g. lang="en"). Screen readers use it to select the correct pronunciation.',severity:"error",range:n.openTagRange,wcag:"WCAG 2.1 SC 3.1.1 Language of Page (Level A)",section508:R})}return t}},tt={id:"missing-title",check(e){if(e.sourceKind!=="html")return[];let t=e.all.find(s=>s.tag==="head"&&!s.isSynthesized),n=e.all.find(s=>s.tag==="html"&&!s.isSynthesized);if(!t&&!n)return[];let o=e.all.find(s=>s.tag==="title");if(o&&o.hasTextContent)return[];let i=t??n;return[{ruleId:tt.id,message:o?"<title> is present but empty. Give the page a descriptive title.":"Document is missing a <title> element. Every page needs a descriptive title for navigation and browser tabs/screen readers.",severity:"error",range:i.openTagRange,wcag:"WCAG 2.1 SC 2.4.2 Page Titled (Level A)",section508:R}]}},nt={id:"duplicate-id",check(e){let t=[],n=new Map;for(let o of e.all){let i=o.attrs.get("id");if(!i||i.dynamic||!i.value)continue;let s=(n.get(i.value)??0)+1;n.set(i.value,s),s>1&&t.push({ruleId:nt.id,message:`id="${i.value}" is used more than once in this file. Duplicate IDs break aria-labelledby/aria-describedby references and label "for" targeting \u2014 IDs must be unique.`,severity:"error",range:o.openTagRange,wcag:"WCAG 2.1 SC 4.1.1 Parsing (Level A)",section508:R})}return t}},Qt=[Je,qe,Ve,Ke,Ue,He,Ye,Qe,Ze,et,tt,nt],Zt=new Set([".jsx",".tsx",".js",".ts"]),en=new Set([".html",".htm",".twig"]);function te(e){let t=e.lastIndexOf(".");if(t===-1)return null;let n=e.slice(t).toLowerCase();return en.has(n)?"html":Zt.has(n)?"jsx":null}function ot(e,t){let n=t.sourceKind==="html"?Gt(e):Dt(e);if(n.all.length===0&&n.parseErrors.length>0)return{findings:[],parseErrors:n.parseErrors};let o=[];for(let i of Qt)o.push(...i.check({all:n.all,root:n.root,sourceKind:t.sourceKind}));return o.sort((i,s)=>i.range.start.line!==s.range.start.line?i.range.start.line-s.range.start.line:i.range.start.column!==s.range.start.column?i.range.start.column-s.range.start.column:i.ruleId.localeCompare(s.ruleId)),{findings:o,parseErrors:n.parseErrors}}var tn=new Set(["node_modules","dist","build",".git","coverage",".next","out","vendor"]);function nn(){try{return ne("git",["rev-parse","--show-toplevel"],{encoding:"utf8"}).trim()}catch{return null}}function on(e,t,n){let o=n?"--cached":`${t}...HEAD`,i=ne("git",["diff","--name-only",o],{encoding:"utf8",cwd:e}).split(`
709
+ `;function Re(){let e=".webability.yml";if(Nt(e)){console.log(le.yellow(` ${e} already exists`));return}Rt(e,Lt),console.log(le.green(` Created ${e}`)),console.log(le.dim(" Edit it to configure your project URLs and thresholds"))}import{execFileSync as G}from"child_process";import{chmodSync as Te,existsSync as ce,mkdirSync as Tt,readFileSync as Be,writeFileSync as Pe}from"fs";import{homedir as de}from"os";import{dirname as Bt,join as ue}from"path";var $="webability",W="https://mcp.webability.io/mcp",Me="x-webability-token";function pe(e){return{url:W,headers:{[Me]:e}}}function ge(){let e=process.platform==="win32"?["claude.cmd","claude"]:["claude"];for(let t of e)try{return G(t,["--version"],{stdio:"ignore"}),t}catch{}return null}function Pt(){return ge()!==null}function Oe(e){try{return G(e,["mcp","get",$],{encoding:"utf-8",stdio:["ignore","pipe","ignore"]})}catch{return null}}function Mt(){return ce(ue(de(),".cursor"))}function Ne(e){let t=ge();if(!t)return{client:"claude",ok:!1,detail:"Claude Code CLI not found on PATH"};try{let n=Oe(t);if(n!==null&&!n.includes(W))return{client:"claude",ok:!1,detail:`Claude Code already has an MCP server named "${$}" that isn't ours \u2014 left untouched. Remove it (claude mcp remove -s user ${$}) and re-run \`${h} mcp install\` to switch to the hosted server.`};if(n!==null)try{G(t,["mcp","remove","-s","user",$],{stdio:"ignore"})}catch{}return G(t,["mcp","add","-s","user","--transport","http",$,W,"--header",`${Me}: ${e}`],{stdio:"ignore"}),{client:"claude",ok:!0,detail:"Claude Code"}}catch(n){return{client:"claude",ok:!1,detail:`claude mcp add failed: ${n.message}`}}}function Le(e){let t=ue(de(),".cursor","mcp.json");try{let n={};if(ce(t)){let o=Be(t,"utf-8").trim();o&&(n=JSON.parse(o))}let i=(n.mcpServers||{})[$];return i&&i.url!==W?{client:"cursor",ok:!1,detail:`~/.cursor/mcp.json already has an MCP server named "${$}" that isn't ours \u2014 left untouched. Remove that entry and re-run \`${h} mcp install\` to switch to the hosted server.`}:(n.mcpServers={...n.mcpServers||{},[$]:pe(e)},Tt(Bt(t),{recursive:!0}),Pe(t,`${JSON.stringify(n,null,2)}
710
+ `,{mode:384}),Te(t,384),{client:"cursor",ok:!0,detail:"Cursor (~/.cursor/mcp.json)"})}catch(n){return{client:"cursor",ok:!1,detail:`could not write ${t}: ${n.message}`}}}function _e(){let e=[],t=ge();if(t){let i=Oe(t);if(i!==null)if(!i.includes(W))e.push({client:"claude",ok:!1,detail:`left "${$}" in Claude Code alone \u2014 it isn't ours`});else try{G(t,["mcp","remove","-s","user",$],{stdio:"ignore"}),e.push({client:"claude",ok:!0,detail:"Claude Code"})}catch(o){e.push({client:"claude",ok:!1,detail:`claude mcp remove failed: ${o.message}`})}}let n=ue(de(),".cursor","mcp.json");if(ce(n))try{let i=Be(n,"utf-8").trim(),o=i?JSON.parse(i):{},s=o.mcpServers?.[$];s&&s.url===W?(delete o.mcpServers[$],Pe(n,`${JSON.stringify(o,null,2)}
711
+ `,{mode:384}),Te(n,384),e.push({client:"cursor",ok:!0,detail:"Cursor (~/.cursor/mcp.json)"})):s&&e.push({client:"cursor",ok:!1,detail:`left "${$}" in ~/.cursor/mcp.json alone \u2014 it isn't ours`})}catch(i){e.push({client:"cursor",ok:!1,detail:`could not update ${n}: ${i.message}`})}return e}function X(e,t){if(t==="claude")return[Ne(e)];if(t==="cursor")return[Le(e)];let n=[];return Pt()&&n.push(Ne(e)),Mt()&&n.push(Le(e)),n}import M from"chalk";var C=M.dim,je=M.bold,fe=M.blue,U=M.green,z=M.red,D=M.yellow,K=M.cyan,Ot=M.white;function k(){console.log(),console.log(` ${fe("\u25C6")} ${je("WebAbility")} ${C("WCAG Scanner")}`),console.log()}function me(e,t){let n=e>=80?U:e>=50?D:z,i=e>=80?U("\u2713"):e>=50?D("\u26A0"):z("\u2717"),o=e>=80?"Passing":e>=50?"Needs work":"Failing";console.log(` ${C("URL")} ${Ot(t)}`),console.log(` ${C("Score")} ${n.bold(e+"%")} ${i} ${C(o)}`),console.log(` ${C(" ")}${_t(e)}`),console.log()}function _t(e){let n=Math.round(e/100*32),i=32-n;return(e>=80?U:e>=50?D:z)("\u2501".repeat(n))+C("\u2501".repeat(i))}function H(e){let t=[];e.critical>0&&t.push(z.bold(`${e.critical} critical`)),e.serious>0&&t.push(D(`${e.serious} serious`)),e.moderate>0&&t.push(C(`${e.moderate} moderate`)),e.minor>0&&t.push(C(`${e.minor} minor`)),console.log(` ${je(String(e.total))} issues ${t.join(C(" \xB7 "))}`),console.log()}function Y(e){let t=e.severity==="critical"?z("\u2717"):e.severity==="serious"?D("\u25B2"):C("\u25CF"),n=C(`${e.wcag}`),i=e.count&&e.count>1?` \xD7${e.count}`:"",o=i?C(i):"",s=80-(6+e.wcag.length+i.length);if(console.log(` ${t} ${n}${o} ${e.problem.slice(0,s)}`),e.contrast?(console.log(` ${K("fix")} ${e.fix.slice(0,70)}`),console.log(` ${C(`${e.contrast.current}:1 \u2192 ${e.contrast.suggested}:1 (min ${e.contrast.required}:1)`)}`)):e.before&&e.after?console.log(` ${K("fix")} ${C(e.before)} ${C("\u2192")} ${U(e.after)}`):console.log(` ${K("fix")} ${e.fix.slice(0,70)}`),e.html&&e.count===1){let r=e.html.slice(0,60).replace(/\n/g," ");console.log(` ${C(r)}`)}if(e.selectors&&e.selectors.length>0&&e.count&&e.count>1){let r=e.selectors.slice(0,2).map(f=>f.slice(0,30)).join(C(", ")),c=e.count>2?C(` +${e.count-2} more`):"";console.log(` ${C(r+c)}`)}else e.selector&&console.log(` ${C(e.selector.slice(0,55))}`);console.log()}function De(e){if(e.length){for(let t of e)console.log(` ${D.bold("\u26A0 WARNING")} ${D(t)}`);console.log()}}function Q(e){console.log(C(" \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(` ${C("Next steps:")}`),console.log(` ${K(h+" scan "+(e.url||"<url>")+" --format json")} ${C("CI/CD output")}`),console.log(` ${K(h+" scan "+(e.url||"<url>")+" --format html > report.html")} ${C("share report")}`),console.log()),console.log(` ${fe("\u25C6")} ${C("Auto-fix at")} ${fe("webability.io")}`),console.log()}function I(e,t){console.error(z(` \u2717 Error: ${e}`)),t&&console.error(C(` ${t}`)),console.error()}function We(e){console.log(JSON.stringify(e,null,2))}function he(e){console.log("severity,wcag,message,element");for(let t of e){let n=t.message.replace(/"/g,'""'),i=(t.element||"").replace(/"/g,'""');console.log(`${t.severity},${t.wcag},"${n}","${i}"`)}}import{execFileSync as ne}from"child_process";import*as T from"fs";import*as O from"path";import L from"chalk";import{parse as jt,AST_NODE_TYPES as x}from"@typescript-eslint/typescript-estree";import{parse as Wt,defaultTreeAdapter as N}from"parse5";function Dt(e){let t=[],n=null;try{n=jt(e,{jsx:!0,loc:!0,range:!0,errorOnUnknownASTType:!1,errorOnTypeScriptSyntacticAndSemanticIssues:!1})}catch(a){return t.push(a instanceof Error?a.message:String(a)),{root:null,all:[],parseErrors:t}}let i=[],o=null;function s(a){return{start:{line:a.start.line,column:a.start.column},end:{line:a.end.line,column:a.end.column}}}function r(a){switch(a.type){case x.JSXIdentifier:return a.name;case x.JSXMemberExpression:return`${r(a.object)}.${a.property.name}`;case x.JSXNamespacedName:return`${a.namespace.name}:${a.name.name}`;default:return"unknown"}}function c(a){let p=f(a.name);if(a.value==null)return{name:p,value:null,dynamic:!1};if(a.value.type===x.Literal&&typeof a.value.value=="string")return{name:p,value:a.value.value,dynamic:!1};if(a.value.type===x.JSXExpressionContainer){let g=a.value.expression;return g.type===x.Literal&&typeof g.value=="string"?{name:p,value:g.value,dynamic:!1}:g.type===x.Literal&&typeof g.value=="number"?{name:p,value:String(g.value),dynamic:!1}:g.type===x.UnaryExpression&&g.operator==="-"&&g.argument.type===x.Literal&&typeof g.argument.value=="number"?{name:p,value:String(-g.argument.value),dynamic:!1}:g.type===x.TemplateLiteral&&g.expressions.length===0?{name:p,value:g.quasis.map(y=>y.value.cooked).join(""),dynamic:!1}:{name:p,value:null,dynamic:!0}}return{name:p,value:null,dynamic:!0}}function f(a){return a.type===x.JSXIdentifier?a.name:`${a.namespace.name}-${a.name.name}`}function u(a){for(let p of a.children){if(p.type===x.JSXText&&p.value.trim().length>0)return!0;if(p.type===x.JSXExpressionContainer){if(p.expression.type===x.JSXEmptyExpression||p.expression.type===x.Literal&&p.expression.value===null)continue;return!0}if((p.type===x.JSXElement||p.type===x.JSXFragment)&&u(p))return!0}return!1}function b(a){let p=a.openingElement,g=r(p.name).toLowerCase(),y=p.attributes.filter(A=>A.type===x.JSXAttribute).map(c),S=y.find(A=>A.name==="alt"),_=y.find(A=>A.name==="aria-label");return g==="img"&&S&&!S.dynamic&&S.value&&S.value.trim().length>0||_&&!_.dynamic&&_.value&&_.value.trim().length>0||y.some(A=>A.name==="aria-labelledby")?!0:l(a)}function l(a){for(let p of a.children)if(p.type===x.JSXElement){if(b(p))return!0}else if(p.type===x.JSXFragment){if(l(p))return!0}else if(p.type===x.JSXExpressionContainer&&m(p.expression))return!0;return!1}function m(a){if(!a||typeof a!="object")return!1;if(a.type===x.JSXElement)return b(a);if(a.type===x.JSXFragment)return l(a);for(let p of Object.keys(a)){if(p==="parent"||p==="loc"||p==="range")continue;let g=a[p];if(Array.isArray(g)){for(let y of g)if(y&&typeof y=="object"&&"type"in y&&m(y))return!0}else if(g&&typeof g=="object"&&"type"in g&&m(g))return!0}return!1}function w(a,p){let g=a.openingElement,y=r(g.name),S=new Map,_=!1;for(let B of g.attributes)if(B.type===x.JSXAttribute){let j=c(B);S.set(j.name.toLowerCase(),j)}else _=!0;let A={tag:/^[a-z]/.test(y)?y.toLowerCase():y,attrs:S,children:[],parent:p,openTagRange:s(g.loc),hasTextContent:u(a),hasAccessibleChildContent:l(a),hasSpreadAttrs:_,isSynthesized:!1};i.push(A);for(let B of a.children)if(B.type===x.JSXElement)A.children.push(w(B,A));else if(B.type===x.JSXFragment)for(let j of B.children)j.type===x.JSXElement?A.children.push(w(j,A)):j.type===x.JSXExpressionContainer&&v(j.expression,A);else B.type===x.JSXExpressionContainer&&v(B.expression,A);return A}function v(a,p){if(!(!a||typeof a!="object")){if(a.type===x.JSXElement){let g=w(a,p);p?p.children.push(g):o||(o=g);return}if(a.type===x.JSXFragment){for(let g of a.children)if(g.type===x.JSXElement){let y=w(g,p);p?p.children.push(y):o||(o=y)}else g.type===x.JSXExpressionContainer&&v(g.expression,p);return}for(let g of Object.keys(a)){if(g==="parent"||g==="loc"||g==="range")continue;let y=a[g];if(Array.isArray(y))for(let S of y)S&&typeof S=="object"&&"type"in S&&v(S,p);else y&&typeof y=="object"&&"type"in y&&v(y,p)}}}function F(a){if(a.type===x.JSXElement){let p=w(a,null);o||(o=p);return}for(let p of Object.keys(a)){if(p==="parent"||p==="loc"||p==="range")continue;let g=a[p];if(Array.isArray(g))for(let y of g)y&&typeof y=="object"&&"type"in y&&F(y);else g&&typeof g=="object"&&"type"in g&&F(g)}}try{F(n)}catch(a){t.push(a instanceof Error?a.message:String(a))}return{root:o,all:i,parseErrors:t}}function Z(e){return N.isElementNode(e)}var zt=/^\[(?:attr\.)?([^\]]+)\]$/,Jt=["[innerhtml]","[innertext]","[textcontent]"];function qt(e){let t=e.startTag??e;return{start:{line:t.startLine,column:t.startCol-1},end:{line:t.endLine,column:t.endCol-1}}}function Vt(e){return e.replace(/\{#[\s\S]*?#\}/g,t=>t.replace(/[^\n]/g," "))}function Gt(e){let t=[],n=Wt(Vt(e),{sourceCodeLocationInfo:!0}),i=[],o=null;function s(l){let m=new Map;for(let w of l.attrs)m.set(w.name.toLowerCase(),{name:w.name.toLowerCase(),value:w.value,dynamic:!1});for(let w of l.attrs){let v=zt.exec(w.name.toLowerCase());v&&!m.has(v[1])&&m.set(v[1],{name:v[1],value:null,dynamic:!0})}return m}function r(l){return N.isTextNode(l)?l.value:Z(l)?N.getChildNodes(l).map(r).join(""):""}function c(l){return Z(l)?l.attrs.some(m=>Jt.includes(m.name.toLowerCase()))?!0:N.getChildNodes(l).some(c):!1}function f(l){for(let m of N.getChildNodes(l)){if(!Z(m))continue;let w=N.getTagName(m).toLowerCase(),v=s(m),F=v.get("alt"),a=v.get("aria-label");if(w==="img"&&F?.value&&F.value.trim().length>0||a?.value&&a.value.trim().length>0||v.has("aria-labelledby")||f(m))return!0}return!1}function u(l,m){if(!Z(l)){if("childNodes"in l)for(let F of N.getChildNodes(l))u(F,m);return}let w=N.getNodeSourceCodeLocation(l),v={tag:N.getTagName(l).toLowerCase(),attrs:s(l),children:[],parent:m,openTagRange:w?qt(w):{start:{line:1,column:0},end:{line:1,column:0}},hasTextContent:r(l).trim().length>0||c(l),hasAccessibleChildContent:f(l),hasSpreadAttrs:!1,isSynthesized:w==null};i.push(v),o||(o=v),m&&m.children.push(v);for(let F of N.getChildNodes(l))u(F,v)}try{for(let l of N.getChildNodes(n))u(l,null)}catch(l){t.push(l instanceof Error?l.message:String(l))}return{root:i.find(l=>l.tag==="html")??o,all:i,parseErrors:t}}var R="Section 508 (2017 Revised Standards, 36 CFR Part 1194) incorporates WCAG 2.0 Level A/AA by reference (E205, E207). This finding maps to a WCAG 2.1 success criterion that is a superset of/consistent with the WCAG 2.0 baseline Section 508 requires.";function ze(e,t){let n=e.attrs.get(t);return n?n.dynamic?!0:!!n.value&&n.value.trim().length>0:!1}function ee(e,t){return e.hasSpreadAttrs?!1:!e.attrs.has(t)}var Je={id:"img-alt",check(e){let t=[];for(let n of e.all)n.tag==="img"&&ee(n,"alt")&&t.push({ruleId:Je.id,message:'<img> is missing an "alt" attribute. Add alt text describing the image, or alt="" if it is purely decorative.',severity:"error",range:n.openTagRange,wcag:"WCAG 2.1 SC 1.1.1 Non-text Content (Level A)",section508:R});return t}},qe={id:"input-image-alt",check(e){let t=[];for(let n of e.all){if(n.tag!=="input")continue;let i=n.attrs.get("type");i&&!i.dynamic&&(i.value??"").toLowerCase()==="image"&&ee(n,"alt")&&t.push({ruleId:qe.id,message:'<input type="image"> is missing an "alt" attribute, which is required as its accessible name.',severity:"error",range:n.openTagRange,wcag:"WCAG 2.1 SC 1.1.1 Non-text Content (Level A)",section508:R})}return t}},Ve={id:"iframe-title",check(e){let t=[];for(let n of e.all){if(n.tag!=="iframe")continue;let i=n.attrs.get("title"),o=!!i&&!i.dynamic&&(!i.value||i.value.trim()==="");(ee(n,"title")||o)&&t.push({ruleId:Ve.id,message:'<iframe> is missing a "title" attribute describing its content or purpose.',severity:"error",range:n.openTagRange,wcag:"WCAG 2.1 SC 4.1.2 Name, Role, Value (Level A) / SC 2.4.1 Bypass Blocks (Level A)",section508:R})}return t}},Kt=new Set(["hidden","submit","reset","button","image"]);function Xt(e){let t=e.parent;for(;t;){if(t.tag==="label")return!0;t=t.parent}return!1}function Ge(e){for(let t of e.children)if(t.tag==="mat-label"||Ge(t))return!0;return!1}function Ut(e){if(!e.attrs.has("matinput")&&!e.attrs.has("matnativecontrol"))return!1;let t=e.parent;for(;t;){if(t.tag==="mat-form-field")return Ge(t);t=t.parent}return!1}function be(e){return!!(ze(e,"aria-label")||e.attrs.has("aria-labelledby")||ze(e,"title")||Xt(e))}var Ke={id:"label-control",check(e){let t=[],n=new Set;for(let i of e.all){if(i.tag!=="label")continue;let o=i.attrs.get("for")??i.attrs.get("htmlfor");o&&!o.dynamic&&o.value&&n.add(o.value)}for(let i of e.all){if(!["input","textarea","select"].includes(i.tag)||i.hasSpreadAttrs)continue;let o=i.attrs.get("type");if(o&&!o.dynamic&&o.value&&Kt.has(o.value.toLowerCase())||Ut(i))continue;let s=i.attrs.get("id"),r=!!s&&!s.dynamic&&!!s.value&&n.has(s.value);!be(i)&&!r&&t.push({ruleId:Ke.id,message:`<${i.tag}> has no accessible label. Add a <label>, aria-label, aria-labelledby, or wrap it in a <label> element.`,severity:"error",range:i.openTagRange,wcag:"WCAG 2.1 SC 1.3.1 Info and Relationships (Level A) / SC 4.1.2 Name, Role, Value (Level A)",section508:R})}return t}};function Xe(e){return e.hasTextContent||e.hasAccessibleChildContent}var Ue={id:"empty-link",check(e){let t=[];for(let n of e.all)n.tag==="a"&&(n.hasSpreadAttrs||n.attrs.has("href")&&(Xe(n)||be(n)||t.push({ruleId:Ue.id,message:"<a> has no discernible text. Add link text, or an aria-label/aria-labelledby/title, so screen reader users know its purpose.",severity:"error",range:n.openTagRange,wcag:"WCAG 2.1 SC 2.4.4 Link Purpose (In Context) (Level A) / SC 4.1.2 Name, Role, Value (Level A)",section508:R})));return t}},He={id:"empty-button",check(e){let t=[];for(let n of e.all)n.tag==="button"&&(n.hasSpreadAttrs||Xe(n)||be(n)||t.push({ruleId:He.id,message:"<button> has no discernible text. Add button text or an aria-label so screen reader users know what it does.",severity:"error",range:n.openTagRange,wcag:"WCAG 2.1 SC 4.1.2 Name, Role, Value (Level A)",section508:R}));return t}},Ht=new Set(["alert","alertdialog","application","article","banner","button","cell","checkbox","columnheader","combobox","complementary","contentinfo","definition","dialog","directory","document","feed","figure","form","grid","gridcell","group","heading","img","link","list","listbox","listitem","log","main","marquee","math","menu","menubar","menuitem","menuitemcheckbox","menuitemradio","navigation","none","note","option","presentation","progressbar","radio","radiogroup","region","row","rowgroup","rowheader","scrollbar","search","searchbox","separator","slider","spinbutton","status","switch","tab","table","tablist","tabpanel","term","textbox","timer","toolbar","tooltip","tree","treegrid","treeitem"]),Yt={button:"button",nav:"navigation",main:"main",header:"banner",footer:"contentinfo",aside:"complementary",form:"form",table:"table",ul:"list",ol:"list",li:"listitem",img:"img",h1:"heading",h2:"heading",h3:"heading",h4:"heading",h5:"heading",h6:"heading",progress:"progressbar",textarea:"textbox",dialog:"dialog"},Ye={id:"invalid-aria-role",check(e){let t=[];for(let n of e.all){let i=n.attrs.get("role");if(!i||i.dynamic||!i.value)continue;let s=i.value.trim().split(/\s+/).filter(Boolean).filter(r=>!Ht.has(r.toLowerCase()));s.length>0&&t.push({ruleId:Ye.id,message:`role="${i.value}" contains an unrecognized ARIA role (${s.join(", ")}). Assistive tech will ignore it, falling back to no semantic role.`,severity:"error",range:n.openTagRange,wcag:"WCAG 2.1 SC 4.1.2 Name, Role, Value (Level A)",section508:R})}return t}},Qe={id:"redundant-role",check(e){let t=[];for(let n of e.all){let i=n.attrs.get("role");if(!i||i.dynamic||!i.value)continue;let o=Yt[n.tag];o&&i.value.trim().toLowerCase()===o&&t.push({ruleId:Qe.id,message:`role="${o}" is redundant on <${n.tag}> \u2014 it already has that implicit role. Remove the attribute (or use the correct element if a different role was intended).`,severity:"information",range:n.openTagRange,wcag:"WCAG 2.1 SC 4.1.2 Name, Role, Value (Level A) \u2014 best practice",section508:R})}return t}},Ze={id:"positive-tabindex",check(e){let t=[];for(let n of e.all){let i=n.attrs.get("tabindex");if(!i||i.dynamic||i.value==null)continue;let o=Number.parseInt(i.value,10);Number.isFinite(o)&&o>0&&t.push({ruleId:Ze.id,message:`tabindex="${i.value}" is a positive value, which overrides natural tab order and creates confusing keyboard navigation. Use tabindex="0" or restructure the DOM order instead.`,severity:"warning",range:n.openTagRange,wcag:"WCAG 2.1 SC 2.4.3 Focus Order (Level A)",section508:R})}return t}},et={id:"html-lang",check(e){let t=[];for(let n of e.all){if(n.tag!=="html"||n.isSynthesized)continue;let i=n.attrs.get("lang"),o=!!i&&!i.dynamic&&(!i.value||i.value.trim()==="");(ee(n,"lang")||o)&&t.push({ruleId:et.id,message:'<html> is missing a "lang" attribute (e.g. lang="en"). Screen readers use it to select the correct pronunciation.',severity:"error",range:n.openTagRange,wcag:"WCAG 2.1 SC 3.1.1 Language of Page (Level A)",section508:R})}return t}},tt={id:"missing-title",check(e){if(e.sourceKind!=="html")return[];let t=e.all.find(s=>s.tag==="head"&&!s.isSynthesized),n=e.all.find(s=>s.tag==="html"&&!s.isSynthesized);if(!t&&!n)return[];let i=e.all.find(s=>s.tag==="title");if(i&&i.hasTextContent)return[];let o=t??n;return[{ruleId:tt.id,message:i?"<title> is present but empty. Give the page a descriptive title.":"Document is missing a <title> element. Every page needs a descriptive title for navigation and browser tabs/screen readers.",severity:"error",range:o.openTagRange,wcag:"WCAG 2.1 SC 2.4.2 Page Titled (Level A)",section508:R}]}},nt={id:"duplicate-id",check(e){let t=[],n=new Map;for(let i of e.all){let o=i.attrs.get("id");if(!o||o.dynamic||!o.value)continue;let s=(n.get(o.value)??0)+1;n.set(o.value,s),s>1&&t.push({ruleId:nt.id,message:`id="${o.value}" is used more than once in this file. Duplicate IDs break aria-labelledby/aria-describedby references and label "for" targeting \u2014 IDs must be unique.`,severity:"error",range:i.openTagRange,wcag:"WCAG 2.1 SC 4.1.1 Parsing (Level A)",section508:R})}return t}},Qt=[Je,qe,Ve,Ke,Ue,He,Ye,Qe,Ze,et,tt,nt],Zt=new Set([".jsx",".tsx",".js",".ts"]),en=new Set([".html",".htm",".twig"]);function te(e){let t=e.lastIndexOf(".");if(t===-1)return null;let n=e.slice(t).toLowerCase();return en.has(n)?"html":Zt.has(n)?"jsx":null}function it(e,t){let n=t.sourceKind==="html"?Gt(e):Dt(e);if(n.all.length===0&&n.parseErrors.length>0)return{findings:[],parseErrors:n.parseErrors};let i=[];for(let o of Qt)i.push(...o.check({all:n.all,root:n.root,sourceKind:t.sourceKind}));return i.sort((o,s)=>o.range.start.line!==s.range.start.line?o.range.start.line-s.range.start.line:o.range.start.column!==s.range.start.column?o.range.start.column-s.range.start.column:o.ruleId.localeCompare(s.ruleId)),{findings:i,parseErrors:n.parseErrors}}var tn=new Set(["node_modules","dist","build",".git","coverage",".next","out","vendor"]);function nn(){try{return ne("git",["rev-parse","--show-toplevel"],{encoding:"utf8"}).trim()}catch{return null}}function on(e,t,n){let i=n?"--cached":`${t}...HEAD`,o=ne("git",["diff","--name-only",i],{encoding:"utf8",cwd:e}).split(`
712
712
  `).map(r=>r.trim()).filter(Boolean),s=n?[]:ne("git",["ls-files","--others","--exclude-standard"],{encoding:"utf8",cwd:e}).split(`
713
- `).map(r=>r.trim()).filter(Boolean);return[...new Set([...i,...s])]}function sn(e,t,n,o){let i=new Map;if(o.length===0)return i;let s="";try{let c=n?["--cached"]:[`${t}...HEAD`];s=ne("git",["diff","-U0",...c,"--",...o],{encoding:"utf8",maxBuffer:64*1024*1024,cwd:e})}catch{return i}let r="";for(let c of s.split(`
714
- `)){let f=/^diff --git a\/(.*) b\/\1$/.exec(c);if(f){r=f[1],i.set(r,[]);continue}let u=/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/.exec(c);if(u&&r){let h=Number(u[1]),l=u[2]===void 0?1:Number(u[2]);l>0&&i.get(r)?.push([h,h+l-1])}}return i}function st(e,t){if(T.statSync(e).isFile()){t.push(e);return}for(let o of T.readdirSync(e,{withFileTypes:!0})){if(o.name.startsWith(".")&&o.name!==".webability"||tn.has(o.name))continue;let i=O.join(e,o.name);o.isDirectory()?st(i,t):o.isFile()&&t.push(i)}}function rn(e,t){return{file:e,line:t.range.start.line,column:t.range.start.column,ruleId:t.ruleId,message:t.message,severity:t.severity,wcag:t.wcag,source:"static"}}function rt(e){let t=nn();if(e.paths.length>0){let s=[];for(let c of e.paths){let f=O.resolve(c);if(!T.existsSync(f))throw new Error(`Path not found: ${c}`);st(f,s)}let r=[];for(let c of s){if(!te(c))continue;let f=t?O.relative(t,c):c;r.push({abs:c,display:f.startsWith("..")?c:f})}return{targets:r,ranges:new Map,root:t}}if(!t)throw new Error("Not a git repository. Pass explicit file or directory paths to review.");let n=on(t,e.base,e.staged).filter(s=>{let r=O.join(t,s);return T.existsSync(r)&&te(s)!==null}),o=n.map(s=>({abs:O.join(t,s),display:s})),i=e.all?new Map:sn(t,e.base,e.staged,n);return{targets:o,ranges:i,root:t}}function at(e){let{targets:t,ranges:n}=rt(e),o=[];for(let i of t){let s=te(i.abs);if(!s)continue;let{findings:r}=ot(T.readFileSync(i.abs,"utf8"),{sourceKind:s}),c=n.get(i.display),f=e.paths.length>0||e.all||!c||c.length===0?r:r.filter(u=>c.some(([h,l])=>u.range.start.line>=h&&u.range.start.line<=l));o.push(...f.map(u=>rn(i.display,u)))}return o.sort((i,s)=>i.file<s.file?-1:i.file>s.file?1:i.line-s.line||i.column-s.column),{findings:o,filesScanned:t.length}}var an=10,it=2e4,ln=6e4;async function lt(e,t,n){let{targets:o}=rt(e),i=[],s=0,r=!1;for(let u of o){if(i.length>=an){r=!0;break}let h=T.readFileSync(u.abs,"utf8");if(h.length>it){let l=`
715
- /* [webability] truncated: showing the head of ${h.length} chars. Code past this point is NOT visible \u2014 never claim an attribute is missing near the end of the shown code. */
716
- `,y=h.slice(0,it-l.length),b=y.lastIndexOf(`
717
- `);h=y.slice(0,b>0?b:y.length)+l,r=!0}if(s+h.length>ln){r=!0;break}s+=h.length,i.push({path:u.display,content:h})}if(i.length===0)return{findings:[],filesSent:0,truncated:r};let f=((await n({files:i,staticFindings:t.slice(0,200).map(u=>({file:u.file,line:u.line,ruleId:u.ruleId,message:u.message}))})).findings??[]).map(u=>({file:u.file,line:u.line,column:1,ruleId:u.rule,message:u.message,severity:u.severity==="error"?"error":"warning",wcag:u.wcag??"",source:"agent",fix:u.fix??""}));return f.sort((u,h)=>u.file<h.file?-1:u.file>h.file?1:u.line-h.line),{findings:f,filesSent:i.length,truncated:r}}var cn=e=>e==="error"?L.red("\u2717"):e==="warning"?L.yellow("\u25B2"):L.dim("\u25CF");function ct(e,t){if(e.length===0){console.log(L.green(`\u2713 No accessibility issues in ${t} file(s).`));return}let n="";for(let s of e){s.file!==n&&(n=s.file,console.log(L.bold(`
718
- ${s.file}`)));let r=s.source==="agent"?L.dim(" agent"):"";console.log(` ${L.dim(`${s.line}:${s.column}`)} ${cn(s.severity)} ${L.dim(`[${s.ruleId}]`)}${r} ${s.message}`),s.wcag&&console.log(` ${L.dim(` ${s.wcag}`)}`),s.source==="agent"&&s.fix&&console.log(` ${L.dim(` fix: ${s.fix.slice(0,160)}`)}`)}let o=e.filter(s=>s.severity==="error").length,i=e.filter(s=>s.severity==="warning").length;console.log(L.dim(`
719
- ${e.length} finding(s) in ${t} file(s): ${o} error(s), ${i} warning(s).`))}function dt(e,t,n){console.log(JSON.stringify(n?{filesScanned:t,findings:e,warning:n}:{filesScanned:t,findings:e},null,2))}var mn=pn(un(import.meta.url)),pt=JSON.parse(dn(gn(mn,"../package.json"),"utf-8")),E=new fn;E.name(m).description("Abilyo by WebAbility \u2014 WCAG accessibility scanner").version(pt.version);E.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, free with login)").option("--remote","Run scan on WebAbility servers instead of locally").option("--job <id>","Resume polling a previous --remote scan by job id, instead of starting a new one").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&&!P())return t.format==="table"&&k(),I(`--deep needs a login (free). Run \`${m} login\`.`),process.exit(1);if(t.remote||t.job){await hn(e,t);return}t.format==="table"&&k();let n=Date.now(),o=J({text:"Scanning...",prefixText:" "}).start(),i;try{i=await Fe(e,{wcag:t.wcag,deep:t.deep??!1,apiKey:P()??void 0,show:t.show??!1,viewport:t.viewport,includeAxe:t.axe!==!1});let s=((Date.now()-n)/1e3).toFixed(1),r=i.issues.length;if(o.succeed(r===0?`No issues found in ${s}s`:`Found ${r} issue${r===1?"":"s"} in ${s}s`),t.format==="json"){let c={...i,issues:i.issues.map(({source:f,...u})=>u)};console.log(JSON.stringify(c,null,2))}else if(t.format==="csv")he(i.issues.map(c=>({severity:c.impact,wcag:c.wcag,message:c.message,element:c.selector})));else if(t.format==="sarif")console.log(JSON.stringify(vn(i),null,2));else if(t.format==="html"){let{toHtmlReport:c}=await import("@webability/core");console.log(c(i))}else{if(De(i.engineWarnings??[]),me(i.summary.total===0?100:Math.max(0,100-i.summary.total),e),H(i.summary),i.issues.length>0){let{generateDevSuggestion:c}=await import("@webability/core"),f=i.framework||"plain-css",u=[],h=new Map;for(let l of i.issues){let y=c(l,f),b=`${l.type}|${y.fix.slice(0,50)}`,x=h.get(b);x!==void 0?(u[x].count++,u[x].selectors.length<3&&u[x].selectors.push(l.selector.slice(0,50))):(h.set(b,u.length),u.push({suggestion:y,issue:l,count:1,selectors:[l.selector.slice(0,50)]}))}u.sort((l,y)=>{let b={critical:0,serious:1,moderate:2,minor:3};return(b[l.issue.impact]??2)-(b[y.issue.impact]??2)});for(let l of u.slice(0,20))Y({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});u.length>20&&(console.log(d.dim(` ... +${u.length-20} more`)),console.log())}Q({hasIssues:i.issues.length>0,url:e.replace("https://","")})}t.show&&i.issues.length>0&&(console.log(d.cyan(" \u25C6 Browser open \u2014 issues highlighted. Press Enter to close.")),console.log(),await new Promise(c=>{process.stdin.resume(),process.stdin.once("data",()=>{process.stdin.pause(),c()})}),await ae(i)),t.exit&&i.summary.total>0&&process.exit(1)}catch(s){o.stop(),i&&await ae(i).catch(()=>{}),I(s.message.split(`
720
- `)[0],"Check the URL loads in a browser, then run the scan again."),process.exit(1)}});async function hn(e,t){let n=P();if(!n)return t.format==="table"&&k(),I(`--remote requires login. Run \`${m} login\` first.`),process.exit(1);t.format==="table"&&k();let o=J({text:"Scanning (remote)...",prefixText:" "}).start();try{let i=t.job?await ie(t.job,n,c=>{o.text=c}):await Ce(e,n,c=>{o.text=c});o.text="Fetching report...";let s=await Ae(i.key,n);if(o.stop(),!s)return I("Report not found.","Try again, or drop --remote to scan locally."),process.exit(1);let r=[];if(s.axe?.violations)for(let c of s.axe.violations)for(let f of c.nodes||[])r.push({severity:c.impact||"moderate",wcag:c.id,message:c.description,element:f.target?.[0]});if(s.ByFunctions)for(let c of s.ByFunctions)for(let f of c.issues||[])r.push({severity:f.impact||"moderate",wcag:f.code||"",message:f.description||"",element:f.element});if(t.format==="json")ze({url:e,score:s.score,totalElements:s.totalElements,issues:r});else if(t.format==="csv")he(r);else{me(s.score||0,e),H({critical:0,serious:r.filter(c=>c.severity==="serious").length,moderate:r.filter(c=>c.severity==="moderate").length,minor:0,total:r.length});for(let c of r.slice(0,20))Y({severity:c.severity,wcag:c.wcag,problem:c.message,fix:"Review this issue",selector:c.element});Q()}t.exit&&r.length>0&&process.exit(1)}catch(i){o.stop(),i instanceof q?I(i.message,`Resume it any time: ${m} scan "${e}" --remote --job ${i.jobId}`):I(i.message),process.exit(1)}}E.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}`),k(),console.log(d.bold(" Flow Scan")),console.log(),console.log(` Starting at: ${d.cyan(e)}`),console.log(d.dim(" Click through your user journey. Close the browser when done.")),console.log();let n=0,o;try{o=await Ie(e,{wcag:t.wcag,onPageScan:(r,c)=>{n++,console.log(` ${d.green("\u2713")} ${d.dim(String(n).padStart(2))} ${r.replace(/^https?:\/\//,"").slice(0,60)} ${d.dim("\u2014 "+c+" issues")}`)}})}catch(r){return I(r.message.split(`
721
- `)[0],`Check the URL loads in a browser, then run \`${m} flow\` again.`),process.exit(1)}console.log(),console.log(d.bold(" \u2500\u2500\u2500 Flow Report \u2500\u2500\u2500")),console.log(),console.log(` Pages scanned: ${d.cyan(o.pages.length)}`),console.log(` Unique issues: ${d.cyan(o.uniqueIssues.length)}`),console.log(` Total duration: ${d.dim((o.totalDuration/1e3).toFixed(1)+"s")}`),console.log();let i={critical:0,serious:0,moderate:0,minor:0};o.uniqueIssues.forEach(r=>{i[r.impact]++}),H({...i,total:o.uniqueIssues.length});let s=o.uniqueIssues.slice(0,15);for(let r of s){let c=r.pageCount>1?d.yellow(` \xD7${r.pageCount} pages`):"";if(Y({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(d.dim(` Found on${c}:`));for(let f of r.foundOn.slice(0,3))console.log(d.dim(" "+f.replace(/^https?:\/\//,"")))}}o.uniqueIssues.length>15&&console.log(d.dim(` ... +${o.uniqueIssues.length-15} more unique issues`)),console.log(),Q({hasIssues:o.uniqueIssues.length>0,url:e.replace("https://","")})});E.command("review [paths...]").description("Review code for accessibility issues (static, offline, free)").option("--base <ref>","Diff base for changed-files mode","origin/main").option("--staged","Review staged changes only").option("--all","Review whole files, not just added lines").option("-f, --format <format>","Output format: table, json","table").option("--strict","Exit 1 on warnings too (default: errors only)").option("--agent","Add one AI review turn via our API (needs login)").action(async(e,t)=>{let n=t.format==="json"?"json":"table";n==="table"&&k();try{let o={paths:e??[],base:t.base,all:!!t.all,staged:!!t.staged,format:n,strict:!!t.strict,agent:!!t.agent},{findings:i,filesScanned:s}=at(o),r=i,c;if(t.agent){let u=P();if(!u){let l="Agent review needs login. Run `webability login` first (or set WEBABILITY_API_KEY).";n==="table"?I(l):console.log(JSON.stringify({error:l})),process.exit(2)}let h=n==="table"?J("Running agent review\u2026").start():null;try{let{findings:l,truncated:y}=await lt(o,i,b=>xe(b,u));h?.succeed(`Agent review done (${l.length} finding(s))`),y&&n==="table"&&console.log(d.dim(" Note: code exceeded the agent budget (10 files / 60k chars) \u2014 extra files skipped.")),r=[...i,...l].sort((b,x)=>b.file<x.file?-1:b.file>x.file?1:b.line-x.line)}catch(l){h?.fail("Agent review failed");let y=`Agent review unavailable (${l.message}) \u2014 showing static results only.`;n==="table"?console.log(d.yellow(` ${y}`)):c=y}}n==="json"?dt(r,s,c):ct(r,s),(t.strict?r.some(u=>u.severity==="error"||u.severity==="warning"):r.some(u=>u.severity==="error"))&&process.exit(1)}catch(o){n==="table"?I(o.message):console.log(JSON.stringify({error:o.message})),process.exit(2)}});E.command("init").description("Create .webability.yml config file").action(()=>{k(),Re(),console.log()});E.command("login").description("Authenticate with your WebAbility account").option("-k, --key <key>","API key directly (for CI/CD)").action(async e=>{if(k(),e.key){se(e.key),console.log(d.green(" \u2713 API key saved.")),console.log(),be(X(e.key)),ve();return}let t=J({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)return t.fail(`Could not start login (server returned ${n.status}).`),console.log(d.dim(` Try again in a moment: \`${m} login\``)),console.log(d.dim(" Still stuck? support@webability.io")),console.log(),process.exit(1);let{deviceCode:o,userCode:i,verificationUrl:s,expiresIn:r}=await n.json();t.stop(),console.log(` ${d.bold("Login to WebAbility")}`),console.log(),console.log(` Open: ${d.cyan(s)}`),console.log(` Code: ${d.bold.yellow(i)}`),console.log(),bn(s);let c=J({text:"Waiting for approval...",prefixText:" "}).start(),f=3e3,u=Math.floor((r||300)*1e3/f);for(let h=0;h<u;h++){await new Promise(l=>setTimeout(l,f));try{let l=await fetch("https://api.webability.io/cli/device-token",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({deviceCode:o})});if(l.ok){let{token:b}=await l.json();se(b),c.succeed("Logged in."),console.log(),be(X(b)),ve();return}if((await l.json().catch(()=>({}))).error==="expired")return c.fail(`Login expired. Run \`${m} login\` again.`),process.exit(1)}catch{}}c.fail("Login timed out."),process.exit(1)}catch{return t.fail("Could not reach api.webability.io."),console.log(d.dim(` Check your connection, then run \`${m} login\` again.`)),console.log(),process.exit(1)}});var ut=d.dim(" "+"\u2500".repeat(58)),yn=["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 be(e){let t=e.filter(n=>n.ok);if(!t.length){console.log(d.yellow(" No supported editor found for automatic setup.")),console.log(` Install Claude Code or Cursor, then run ${d.cyan(`${m} mcp install`)}.`),console.log(d.dim(` Any other MCP client: ${d.cyan(`${m} mcp print`)} gives you the config block.`)),console.log();for(let n of e.filter(o=>!o.ok))console.log(d.dim(` \u26A0 ${n.client}: ${n.detail}`));e.some(n=>!n.ok)&&console.log();return}console.log(ut),console.log(d.bold(" Abilyo is now inside your editor")),console.log();for(let n of t)console.log(` ${d.green("\u2713")} ${n.detail}`);console.log(),console.log(d.dim(" Restart the editor, then ask it:"));for(let n of yn)console.log(` ${d.cyan(`"${n}"`)}`);console.log(ut),console.log();for(let n of e.filter(o=>!o.ok))console.log(d.dim(` \u26A0 ${n.client}: ${n.detail}`))}function ve(){console.log(` ${d.green("\u2726 Abilyo Pro")} ${d.dim("\u2014 included free with your account")}`),console.log(d.dim(" Deep scans, AI fixes, CI reports and unlimited MCP calls are unlocked.")),console.log()}function bn(e){try{let{execFile:t}=ft("child_process"),n=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";t(n,[e],()=>{})}catch{}}var gt=E.command("mcp").description("Connect Abilyo to your AI editor (Claude Code, Cursor)");gt.command("install").description("Register the Abilyo MCP server with every editor found").option("-c, --client <client>","Only one client: claude, cursor").action(e=>{k();let t=P();if(!t)return I(`Not logged in. Run \`${m} login\` \u2014 it connects your editor for you.`),process.exit(1);if(e.client&&e.client!=="claude"&&e.client!=="cursor")return I(`Unknown client "${e.client}". Use claude or cursor.`),process.exit(1);be(X(t,e.client))});gt.command("print").description("Print the MCP config block (contains your token \u2014 do not share)").action(()=>{let e=P();if(!e)return k(),I(`Not logged in. Run \`${m} login\` first.`),process.exit(1);console.log(JSON.stringify({mcpServers:{[$]:pe(e)}},null,2))});E.command("upgrade").description("Abilyo Pro is free \u2014 this just tells you so").action(()=>{k(),console.log(` ${d.green("Nothing to buy.")} Abilyo Pro is included free with every account.`),console.log(` Run ${d.cyan(`${m} login`)} once and ${d.cyan(`${m} scan --deep`)} works.`),console.log(),console.log(d.dim(" Widget, PDF remediation and managed plans: https://webability.io/pricing")),console.log()});E.command("whoami").description("Show auth status and plan").action(async()=>{let e=P();if(k(),!e){console.log(d.yellow(" Not signed in.")),console.log(` Run ${d.cyan(`${m} login`)} \u2014 it signs you in and connects your editor.`),console.log();return}console.log(d.green(" Signed in")),console.log(d.dim(` Key: ${e.slice(0,12)}\u2026`)),console.log(),ve()});E.command("logout").description("Remove saved API key and disconnect editors").action(()=>{let e=_e();Se(),k(),console.log(d.green(" Logged out. API key removed."));for(let t of e)console.log(t.ok?d.dim(` Disconnected ${t.detail}`):d.yellow(` ${t.detail}`));console.log(d.dim(` Sign back in any time: \`${m} login\``)),console.log()});E.command("activate <code>").description("Activate Abilyo with your early access code").action(async e=>{k();let t=J({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){re(e.trim()),t.succeed("Abilyo activated."),console.log(),console.log(` Next: ${d.cyan(`${m} scan <url>`)} ${d.dim("\u2014 your first scan takes seconds")}`),console.log();return}}catch{}e.trim().length>=8?(re(e.trim()),t.succeed("Abilyo activated."),console.log(),console.log(` Next: ${d.cyan(`${m} scan <url>`)} ${d.dim("\u2014 your first scan takes seconds")}`),console.log()):(t.fail("Invalid access code."),console.log(d.dim(" Request access at https://abilyo.com")),console.log(),process.exit(1))});E.argument("[url]","URL to scan").action(e=>{e?E.parse(["node",m,"scan",e,...process.argv.slice(3)]):(k(),ke()?(console.log(d.bold(" Commands:")),console.log(),console.log(` ${d.cyan(`${m} scan`)} <url> Scan a page for accessibility issues`),console.log(` ${d.cyan(`${m} scan --deep`)} <url> Dynamic content, focus order, keyboard traps ${d.dim("(needs login, free)")}`),console.log(` ${d.cyan(`${m} scan --exit`)} <url> CI mode \u2014 exit 1 if issues found`),console.log(` ${d.cyan(`${m} flow`)} <url> Scan a whole journey, one report`),console.log(` ${d.cyan(`${m} login`)} Sign in and connect your AI editor`),console.log(` ${d.cyan(`${m} mcp install`)} Re-connect Claude Code / Cursor`),console.log(` ${d.cyan(`${m} init`)} Create .webability.yml config`),console.log(` ${d.cyan(`${m} whoami`)} Show auth status and plan`),console.log(` ${d.cyan(`${m} upgrade`)} Pro is free \u2014 see what login unlocks`),console.log(),console.log(d.bold(" Examples:")),console.log(),console.log(d.dim(` ${m} scan localhost:3000 your dev server`)),console.log(d.dim(` ${m} scan example.com --show highlight issues in a browser`)),console.log(d.dim(` ${m} scan example.com --format sarif upload to GitHub code scanning`)),console.log(d.dim(` ${m} scan example.com --exit fail the build on issues`)),console.log(d.dim(` ${m} scan example.com --deep the checks axe-core cannot do`)),console.log(),console.log(d.dim(` In your editor, after \`${m} login\`:`)),console.log(d.dim(' "scan staging.example.com and fix the contrast failures"'))):(console.log(d.yellow(" Abilyo is invite-only during early access.")),console.log(),console.log(` ${d.cyan(`${m} login`)} Already have a WebAbility account? Start here`),console.log(` ${d.cyan(`${m} activate`)} <code> Activate with an early-access code`),console.log(),console.log(d.dim(" Request access at https://abilyo.com"))),console.log())});E.parse();function vn(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:pt.version,informationUri:"https://webability.io",rules:Array.from(t.values())}},results:e.issues.map(o=>{let s={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&&(s.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:s}}]}})}]}}
713
+ `).map(r=>r.trim()).filter(Boolean);return[...new Set([...o,...s])]}function sn(e,t,n,i){let o=new Map;if(i.length===0)return o;let s="";try{let c=n?["--cached"]:[`${t}...HEAD`];s=ne("git",["diff","-U0",...c,"--",...i],{encoding:"utf8",maxBuffer:64*1024*1024,cwd:e})}catch{return o}let r="";for(let c of s.split(`
714
+ `)){let f=/^diff --git a\/(.*) b\/\1$/.exec(c);if(f){r=f[1],o.set(r,[]);continue}let u=/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/.exec(c);if(u&&r){let b=Number(u[1]),l=u[2]===void 0?1:Number(u[2]);l>0&&o.get(r)?.push([b,b+l-1])}}return o}function st(e,t){if(T.statSync(e).isFile()){t.push(e);return}for(let i of T.readdirSync(e,{withFileTypes:!0})){if(i.name.startsWith(".")&&i.name!==".webability"||tn.has(i.name))continue;let o=O.join(e,i.name);i.isDirectory()?st(o,t):i.isFile()&&t.push(o)}}function rn(e,t){return{file:e,line:t.range.start.line,column:t.range.start.column,ruleId:t.ruleId,message:t.message,severity:t.severity,wcag:t.wcag,source:"static"}}function rt(e){let t=nn();if(e.paths.length>0){let s=[];for(let c of e.paths){let f=O.resolve(c);if(!T.existsSync(f))throw new Error(`Path not found: ${c}`);st(f,s)}let r=[];for(let c of s){if(!te(c))continue;let f=t?O.relative(t,c):c;r.push({abs:c,display:f.startsWith("..")?c:f})}return{targets:r,ranges:new Map,root:t}}if(!t)throw new Error("Not a git repository. Pass explicit file or directory paths to review.");let n=on(t,e.base,e.staged).filter(s=>{let r=O.join(t,s);return T.existsSync(r)&&te(s)!==null}),i=n.map(s=>({abs:O.join(t,s),display:s})),o=e.all?new Map:sn(t,e.base,e.staged,n);return{targets:i,ranges:o,root:t}}function at(e){let{targets:t,ranges:n}=rt(e),i=[];for(let o of t){let s=te(o.abs);if(!s)continue;let{findings:r}=it(T.readFileSync(o.abs,"utf8"),{sourceKind:s}),c=n.get(o.display),f=e.paths.length>0||e.all||!c||c.length===0?r:r.filter(u=>c.some(([b,l])=>u.range.start.line>=b&&u.range.start.line<=l));i.push(...f.map(u=>rn(o.display,u)))}return i.sort((o,s)=>o.file<s.file?-1:o.file>s.file?1:o.line-s.line||o.column-s.column),{findings:i,filesScanned:t.length}}var an=10,ot=2e4,ln=6e4;async function lt(e,t,n){let{targets:i}=rt(e),o=[],s=0,r=!1;for(let u of i){if(o.length>=an){r=!0;break}let b=T.readFileSync(u.abs,"utf8");if(b.length>ot){let l=`
715
+ /* [webability] truncated: showing the head of ${b.length} chars. Code past this point is NOT visible \u2014 never claim an attribute is missing near the end of the shown code. */
716
+ `,m=b.slice(0,ot-l.length),w=m.lastIndexOf(`
717
+ `);b=m.slice(0,w>0?w:m.length)+l,r=!0}if(s+b.length>ln){r=!0;break}s+=b.length,o.push({path:u.display,content:b})}if(o.length===0)return{findings:[],filesSent:0,truncated:r};let f=((await n({files:o,staticFindings:t.slice(0,200).map(u=>({file:u.file,line:u.line,ruleId:u.ruleId,message:u.message}))})).findings??[]).map(u=>({file:u.file,line:u.line,column:1,ruleId:u.rule,message:u.message,severity:u.severity==="error"?"error":"warning",wcag:u.wcag??"",source:"agent",fix:u.fix??""}));return f.sort((u,b)=>u.file<b.file?-1:u.file>b.file?1:u.line-b.line),{findings:f,filesSent:o.length,truncated:r}}var cn=e=>e==="error"?L.red("\u2717"):e==="warning"?L.yellow("\u25B2"):L.dim("\u25CF");function ct(e,t){if(e.length===0){console.log(L.green(`\u2713 No accessibility issues in ${t} file(s).`));return}let n="";for(let s of e){s.file!==n&&(n=s.file,console.log(L.bold(`
718
+ ${s.file}`)));let r=s.source==="agent"?L.dim(" agent"):"";console.log(` ${L.dim(`${s.line}:${s.column}`)} ${cn(s.severity)} ${L.dim(`[${s.ruleId}]`)}${r} ${s.message}`),s.wcag&&console.log(` ${L.dim(` ${s.wcag}`)}`),s.source==="agent"&&s.fix&&console.log(` ${L.dim(` fix: ${s.fix.slice(0,160)}`)}`)}let i=e.filter(s=>s.severity==="error").length,o=e.filter(s=>s.severity==="warning").length;console.log(L.dim(`
719
+ ${e.length} finding(s) in ${t} file(s): ${i} error(s), ${o} warning(s).`))}function dt(e,t,n){console.log(JSON.stringify(n?{filesScanned:t,findings:e,warning:n}:{filesScanned:t,findings:e},null,2))}var hn=gn(pn(import.meta.url)),gt=JSON.parse(un(fn(hn,"../package.json"),"utf-8")),E=new mn;E.name(h).description("WebAbility \u2014 WCAG accessibility scanner").version(gt.version);E.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, free with login)").option("--remote","Run scan on WebAbility servers instead of locally").option("--job <id>","Resume polling a previous --remote scan by job id, instead of starting a new one").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&&!P())return t.format==="table"&&k(),I(`--deep needs a login (free). Run \`${h} login\`.`),process.exit(1);if(t.remote||t.job){await bn(e,t);return}t.format==="table"&&k();let n=Date.now(),i=J({text:"Scanning...",prefixText:" "}).start(),o;try{o=await Fe(e,{wcag:t.wcag,deep:t.deep??!1,apiKey:P()??void 0,show:t.show??!1,viewport:t.viewport,includeAxe:t.axe!==!1});let s=((Date.now()-n)/1e3).toFixed(1),r=o.issues.length;if(i.succeed(r===0?`No issues found in ${s}s`:`Found ${r} issue${r===1?"":"s"} in ${s}s`),t.format==="json"){let c={...o,issues:o.issues.map(({source:f,...u})=>u)};console.log(JSON.stringify(c,null,2))}else if(t.format==="csv")he(o.issues.map(c=>({severity:c.impact,wcag:c.wcag,message:c.message,element:c.selector})));else if(t.format==="sarif")console.log(JSON.stringify(vn(o),null,2));else if(t.format==="html"){let{toHtmlReport:c}=await import("@webability/core");console.log(c(o))}else{if(De(o.engineWarnings??[]),me(o.summary.total===0?100:Math.max(0,100-o.summary.total),e),H(o.summary),o.issues.length>0){let{generateDevSuggestion:c}=await import("@webability/core"),f=o.framework||"plain-css",u=[],b=new Map;for(let l of o.issues){let m=c(l,f),w=`${l.type}|${m.fix.slice(0,50)}`,v=b.get(w);v!==void 0?(u[v].count++,u[v].selectors.length<3&&u[v].selectors.push(l.selector.slice(0,50))):(b.set(w,u.length),u.push({suggestion:m,issue:l,count:1,selectors:[l.selector.slice(0,50)]}))}u.sort((l,m)=>{let w={critical:0,serious:1,moderate:2,minor:3};return(w[l.issue.impact]??2)-(w[m.issue.impact]??2)});for(let l of u.slice(0,20))Y({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});u.length>20&&(console.log(d.dim(` ... +${u.length-20} more`)),console.log())}Q({hasIssues:o.issues.length>0,url:e.replace("https://","")})}t.show&&o.issues.length>0&&(console.log(d.cyan(" \u25C6 Browser open \u2014 issues highlighted. Press Enter to close.")),console.log(),await new Promise(c=>{process.stdin.resume(),process.stdin.once("data",()=>{process.stdin.pause(),c()})}),await ae(o)),t.exit&&o.summary.total>0&&process.exit(1)}catch(s){i.stop(),o&&await ae(o).catch(()=>{}),I(s.message.split(`
720
+ `)[0],"Check the URL loads in a browser, then run the scan again."),process.exit(1)}});async function bn(e,t){let n=P();if(!n)return t.format==="table"&&k(),I(`--remote requires login. Run \`${h} login\` first.`),process.exit(1);t.format==="table"&&k();let i=J({text:"Scanning (remote)...",prefixText:" "}).start();try{let o=t.job?await oe(t.job,n,c=>{i.text=c}):await Ce(e,n,c=>{i.text=c});i.text="Fetching report...";let s=await Ae(o.key,n);if(i.stop(),!s)return I("Report not found.","Try again, or drop --remote to scan locally."),process.exit(1);let r=[];if(s.axe?.violations)for(let c of s.axe.violations)for(let f of c.nodes||[])r.push({severity:c.impact||"moderate",wcag:c.id,message:c.description,element:f.target?.[0]});if(s.ByFunctions)for(let c of s.ByFunctions)for(let f of c.issues||[])r.push({severity:f.impact||"moderate",wcag:f.code||"",message:f.description||"",element:f.element});if(t.format==="json")We({url:e,score:s.score,totalElements:s.totalElements,issues:r});else if(t.format==="csv")he(r);else{me(s.score||0,e),H({critical:0,serious:r.filter(c=>c.severity==="serious").length,moderate:r.filter(c=>c.severity==="moderate").length,minor:0,total:r.length});for(let c of r.slice(0,20))Y({severity:c.severity,wcag:c.wcag,problem:c.message,fix:"Review this issue",selector:c.element});Q()}t.exit&&r.length>0&&process.exit(1)}catch(o){i.stop(),o instanceof q?I(o.message,`Resume it any time: ${h} scan "${e}" --remote --job ${o.jobId}`):I(o.message),process.exit(1)}}E.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}`),k(),console.log(d.bold(" Flow Scan")),console.log(),console.log(` Starting at: ${d.cyan(e)}`),console.log(d.dim(" Click through your user journey. Close the browser when done.")),console.log();let n=0,i;try{i=await Ie(e,{wcag:t.wcag,onPageScan:(r,c)=>{n++,console.log(` ${d.green("\u2713")} ${d.dim(String(n).padStart(2))} ${r.replace(/^https?:\/\//,"").slice(0,60)} ${d.dim("\u2014 "+c+" issues")}`)}})}catch(r){return I(r.message.split(`
721
+ `)[0],`Check the URL loads in a browser, then run \`${h} flow\` again.`),process.exit(1)}console.log(),console.log(d.bold(" \u2500\u2500\u2500 Flow Report \u2500\u2500\u2500")),console.log(),console.log(` Pages scanned: ${d.cyan(i.pages.length)}`),console.log(` Unique issues: ${d.cyan(i.uniqueIssues.length)}`),console.log(` Total duration: ${d.dim((i.totalDuration/1e3).toFixed(1)+"s")}`),console.log();let o={critical:0,serious:0,moderate:0,minor:0};i.uniqueIssues.forEach(r=>{o[r.impact]++}),H({...o,total:i.uniqueIssues.length});let s=i.uniqueIssues.slice(0,15);for(let r of s){let c=r.pageCount>1?d.yellow(` \xD7${r.pageCount} pages`):"";if(Y({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(d.dim(` Found on${c}:`));for(let f of r.foundOn.slice(0,3))console.log(d.dim(" "+f.replace(/^https?:\/\//,"")))}}i.uniqueIssues.length>15&&console.log(d.dim(` ... +${i.uniqueIssues.length-15} more unique issues`)),console.log(),Q({hasIssues:i.uniqueIssues.length>0,url:e.replace("https://","")})});E.command("review [paths...]").description("Review code for accessibility issues (static, offline, free)").option("--base <ref>","Diff base for changed-files mode","origin/main").option("--staged","Review staged changes only").option("--all","Review whole files, not just added lines").option("-f, --format <format>","Output format: table, json","table").option("--strict","Exit 1 on warnings too (default: errors only)").option("--agent","Add one AI review turn via our API (needs login)").action(async(e,t)=>{let n=t.format==="json"?"json":"table";n==="table"&&k();try{let i={paths:e??[],base:t.base,all:!!t.all,staged:!!t.staged,format:n,strict:!!t.strict,agent:!!t.agent},{findings:o,filesScanned:s}=at(i),r=o,c;if(t.agent){let u=P();if(!u){let l="Agent review needs login. Run `webability login` first (or set WEBABILITY_API_KEY).";n==="table"?I(l):console.log(JSON.stringify({error:l})),process.exit(2)}let b=n==="table"?J("Running agent review\u2026").start():null;try{let{findings:l,truncated:m}=await lt(i,o,w=>xe(w,u));b?.succeed(`Agent review done (${l.length} finding(s))`),m&&n==="table"&&console.log(d.dim(" Note: code exceeded the agent budget (10 files / 60k chars) \u2014 extra files skipped.")),r=[...o,...l].sort((w,v)=>w.file<v.file?-1:w.file>v.file?1:w.line-v.line)}catch(l){b?.fail("Agent review failed");let m=`Agent review unavailable (${l.message}) \u2014 showing static results only.`;n==="table"?console.log(d.yellow(` ${m}`)):c=m}}n==="json"?dt(r,s,c):ct(r,s),(t.strict?r.some(u=>u.severity==="error"||u.severity==="warning"):r.some(u=>u.severity==="error"))&&process.exit(1)}catch(i){n==="table"?I(i.message):console.log(JSON.stringify({error:i.message})),process.exit(2)}});E.command("init").description("Create .webability.yml config file").action(()=>{k(),Re(),console.log()});E.command("login").description("Authenticate with your WebAbility account").option("-k, --key <key>","API key directly (for CI/CD)").action(async e=>{if(k(),e.key){se(e.key),console.log(d.green(" \u2713 API key saved.")),console.log(),ye(X(e.key)),we();return}let t=J({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)return t.fail(`Could not start login (server returned ${n.status}).`),console.log(d.dim(` Try again in a moment: \`${h} login\``)),console.log(d.dim(" Still stuck? support@webability.io")),console.log(),process.exit(1);let{deviceCode:i,userCode:o,verificationUrl:s,expiresIn:r}=await n.json();t.stop(),console.log(` ${d.bold("Login to WebAbility")}`),console.log(),console.log(` Open: ${d.cyan(s)}`),console.log(` Code: ${d.bold.yellow(o)}`),console.log(),pt(s)?console.log(d.dim(" Browser opened \u2014 approve the code there.")):(console.log(d.dim(" Could not open a browser \u2014 open the link above manually.")),await wn(" Press Enter to try opening it again..."),pt(s)),console.log();let c=J({text:"Waiting for approval...",prefixText:" "}).start(),f=5e3,u=Math.floor((r||300)*1e3/f),b=!1;for(let l=0;l<u;l++){await new Promise(m=>setTimeout(m,f));try{let m=await fetch("https://api.webability.io/cli/device-token",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({deviceCode:i})});if(m.ok){let{token:v}=await m.json();se(v),c.succeed("Logged in."),console.log(),ye(X(v)),we();return}if(m.status===429){b||(b=!0,c.text="Rate limited \u2014 waiting for the limit to reset..."),await new Promise(v=>setTimeout(v,3e4));continue}if(b=!1,(await m.json().catch(()=>({}))).error==="expired")return c.fail(`Login expired. Run \`${h} login\` again.`),process.exit(1)}catch{}}c.fail("Login timed out."),process.exit(1)}catch{return t.fail("Could not reach api.webability.io."),console.log(d.dim(` Check your connection, then run \`${h} login\` again.`)),console.log(),process.exit(1)}});var ut=d.dim(" "+"\u2500".repeat(58)),yn=["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 ye(e){let t=e.filter(n=>n.ok);if(!t.length){console.log(d.yellow(" No supported editor found for automatic setup.")),console.log(` Install Claude Code or Cursor, then run ${d.cyan(`${h} mcp install`)}.`),console.log(d.dim(` Any other MCP client: ${d.cyan(`${h} mcp print`)} gives you the config block.`)),console.log();for(let n of e.filter(i=>!i.ok))console.log(d.dim(` \u26A0 ${n.client}: ${n.detail}`));e.some(n=>!n.ok)&&console.log();return}console.log(ut),console.log(d.bold(" WebAbility is now inside your editor")),console.log();for(let n of t)console.log(` ${d.green("\u2713")} ${n.detail}`);console.log(),console.log(d.dim(" Restart the editor, then ask it:"));for(let n of yn)console.log(` ${d.cyan(`"${n}"`)}`);console.log(ut),console.log();for(let n of e.filter(i=>!i.ok))console.log(d.dim(` \u26A0 ${n.client}: ${n.detail}`))}function we(){console.log(` ${d.green("\u2726 WebAbility Pro")} ${d.dim("\u2014 included free with your account")}`),console.log(d.dim(" Deep scans, AI fixes, CI reports and unlimited MCP calls are unlocked.")),console.log()}function wn(e){return process.stdin.isTTY?new Promise(t=>{process.stdout.write(d.dim(e+`
722
+ `)),process.stdin.resume(),process.stdin.once("data",()=>{process.stdin.pause(),t()})}):Promise.resolve()}function pt(e){try{let t=process.platform==="darwin"?"open":process.platform==="win32"?"cmd":"xdg-open",n=process.platform==="win32"?["/c","start","",e]:[e],i=dn(t,n,()=>{});return i.on("error",()=>{}),typeof i.pid=="number"}catch{return!1}}var ft=E.command("mcp").description("Connect WebAbility to your AI editor (Claude Code, Cursor)");ft.command("install").description("Register the WebAbility MCP server with every editor found").option("-c, --client <client>","Only one client: claude, cursor").action(e=>{k();let t=P();if(!t)return I(`Not logged in. Run \`${h} login\` \u2014 it connects your editor for you.`),process.exit(1);if(e.client&&e.client!=="claude"&&e.client!=="cursor")return I(`Unknown client "${e.client}". Use claude or cursor.`),process.exit(1);ye(X(t,e.client))});ft.command("print").description("Print the MCP config block (contains your token \u2014 do not share)").action(()=>{let e=P();if(!e)return k(),I(`Not logged in. Run \`${h} login\` first.`),process.exit(1);console.log(JSON.stringify({mcpServers:{[$]:pe(e)}},null,2))});E.command("upgrade").description("WebAbility Pro is free \u2014 this just tells you so").action(()=>{k(),console.log(` ${d.green("Nothing to buy.")} WebAbility Pro is included free with every account.`),console.log(` Run ${d.cyan(`${h} login`)} once and ${d.cyan(`${h} scan --deep`)} works.`),console.log(),console.log(d.dim(" Widget, PDF remediation and managed plans: https://webability.io/pricing")),console.log()});E.command("whoami").description("Show auth status and plan").action(async()=>{let e=P();if(k(),!e){console.log(d.yellow(" Not signed in.")),console.log(` Run ${d.cyan(`${h} login`)} \u2014 it signs you in and connects your editor.`),console.log();return}console.log(d.green(" Signed in")),console.log(d.dim(` Key: ${e.slice(0,12)}\u2026`)),console.log(),we()});E.command("logout").description("Remove saved API key and disconnect editors").action(()=>{let e=_e();Se(),k(),console.log(d.green(" Logged out. API key removed."));for(let t of e)console.log(t.ok?d.dim(` Disconnected ${t.detail}`):d.yellow(` ${t.detail}`));console.log(d.dim(` Sign back in any time: \`${h} login\``)),console.log()});E.command("activate <code>").description("Activate WebAbility with your early access code").action(async e=>{k();let t=J({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){re(e.trim()),t.succeed("WebAbility activated."),console.log(),console.log(` Next: ${d.cyan(`${h} scan <url>`)} ${d.dim("\u2014 your first scan takes seconds")}`),console.log();return}}catch{}e.trim().length>=8?(re(e.trim()),t.succeed("WebAbility activated."),console.log(),console.log(` Next: ${d.cyan(`${h} scan <url>`)} ${d.dim("\u2014 your first scan takes seconds")}`),console.log()):(t.fail("Invalid access code."),console.log(d.dim(" Request access at https://webability.io")),console.log(),process.exit(1))});E.argument("[url]","URL to scan").action(e=>{e?E.parse(["node",h,"scan",e,...process.argv.slice(3)]):(k(),ke()?(console.log(d.bold(" Commands:")),console.log(),console.log(` ${d.cyan(`${h} scan`)} <url> Scan a page for accessibility issues`),console.log(` ${d.cyan(`${h} scan --deep`)} <url> Dynamic content, focus order, keyboard traps ${d.dim("(needs login, free)")}`),console.log(` ${d.cyan(`${h} scan --exit`)} <url> CI mode \u2014 exit 1 if issues found`),console.log(` ${d.cyan(`${h} flow`)} <url> Scan a whole journey, one report`),console.log(` ${d.cyan(`${h} login`)} Sign in and connect your AI editor`),console.log(` ${d.cyan(`${h} mcp install`)} Re-connect Claude Code / Cursor`),console.log(` ${d.cyan(`${h} init`)} Create .webability.yml config`),console.log(` ${d.cyan(`${h} whoami`)} Show auth status and plan`),console.log(` ${d.cyan(`${h} upgrade`)} Pro is free \u2014 see what login unlocks`),console.log(),console.log(d.bold(" Examples:")),console.log(),console.log(d.dim(` ${h} scan localhost:3000 your dev server`)),console.log(d.dim(` ${h} scan example.com --show highlight issues in a browser`)),console.log(d.dim(` ${h} scan example.com --format sarif upload to GitHub code scanning`)),console.log(d.dim(` ${h} scan example.com --exit fail the build on issues`)),console.log(d.dim(` ${h} scan example.com --deep the checks axe-core cannot do`)),console.log(),console.log(d.dim(` In your editor, after \`${h} login\`:`)),console.log(d.dim(' "scan staging.example.com and fix the contrast failures"'))):(console.log(d.yellow(" WebAbility is invite-only during early access.")),console.log(),console.log(` ${d.cyan(`${h} login`)} Already have a WebAbility account? Start here`),console.log(` ${d.cyan(`${h} activate`)} <code> Activate with an early-access code`),console.log(),console.log(d.dim(" Request access at https://webability.io"))),console.log())});E.parse();function vn(e){let t=new Map;for(let i of e.issues)t.has(i.type)||t.set(i.type,{id:i.type,shortDescription:{text:i.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(i=>({level:"warning",message:{text:i}}))}]}:{},tool:{driver:{name:"WebAbility",version:gt.version,informationUri:"https://webability.io",rules:Array.from(t.values())}},results:e.issues.map(i=>{let s={startLine:Number.isInteger(i.line)&&i.line>0?i.line:1,snippet:{text:i.html||i.selector}};return Number.isInteger(i.column)&&i.column>0&&(s.startColumn=i.column),{ruleId:i.type,level:i.impact==="critical"||i.impact==="serious"?"error":"warning",message:{text:i.message},locations:[{physicalLocation:{artifactLocation:{uri:e.url},region:s}}]}})}]}}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@webability/cli",
3
- "version": "1.5.0",
4
- "description": "Abilyo by WebAbility — WCAG accessibility scanner for your terminal",
3
+ "version": "1.5.1",
4
+ "description": "WebAbility — WCAG accessibility scanner for your terminal",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "webability": "./dist/cli.js",
@@ -48,7 +48,7 @@
48
48
  "engines": {
49
49
  "node": ">=18"
50
50
  },
51
- "homepage": "https://abilyo.com",
51
+ "homepage": "https://webability.io",
52
52
  "scripts": {
53
53
  "build": "tsup",
54
54
  "dev": "tsx src/cli.ts",