@webability/cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 WebAbility Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # @webability/cli
2
+
3
+ WCAG accessibility scanner for your terminal. Scan any website, get fix suggestions.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @webability/cli
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```bash
14
+ abilyo scan https://example.com
15
+ abilyo scan localhost:3000
16
+ abilyo scan example.com --format json
17
+ abilyo scan example.com --format html > report.html
18
+ abilyo scan example.com --format sarif > results.sarif
19
+ abilyo scan example.com --exit # CI mode — exit 1 if issues found
20
+ abilyo scan example.com --viewport mobile
21
+ ```
22
+
23
+ ## CI/CD
24
+
25
+ ```yaml
26
+ # GitHub Action
27
+ - run: npx @webability/cli scan ${{ env.URL }} --exit --format sarif > results.sarif
28
+ - uses: github/codeql-action/upload-sarif@v3
29
+ with:
30
+ sarif_file: results.sarif
31
+ ```
32
+
33
+ ## Config
34
+
35
+ ```bash
36
+ abilyo init # creates .webability.yml
37
+ ```
38
+
39
+ ```yaml
40
+ # .webability.yml
41
+ project: my-app
42
+ urls:
43
+ - http://localhost:3000
44
+ standard: WCAG2.1AA
45
+ threshold:
46
+ critical: 0
47
+ serious: 5
48
+ ```
49
+
50
+ ## License
51
+
52
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,539 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+ import chalk3 from "chalk";
6
+ import ora from "ora";
7
+
8
+ // src/api.ts
9
+ var API_BASE = "https://api.webability.io";
10
+ async function apiRequest(path, options = {}, apiKey) {
11
+ const res = await fetch(`${API_BASE}${path}`, {
12
+ ...options,
13
+ headers: {
14
+ "Content-Type": "application/json",
15
+ "Authorization": `Bearer ${apiKey}`,
16
+ "Origin": "https://app.webability.io",
17
+ ...options.headers
18
+ }
19
+ });
20
+ if (!res.ok) {
21
+ const body = await res.text().catch(() => "");
22
+ throw new Error(`API ${res.status}: ${body.slice(0, 200)}`);
23
+ }
24
+ return res.json();
25
+ }
26
+ async function graphql(query, variables, apiKey) {
27
+ const data = await apiRequest("/graphql", {
28
+ method: "POST",
29
+ body: JSON.stringify({ query, variables })
30
+ }, apiKey);
31
+ if (data.errors?.length) {
32
+ throw new Error(data.errors[0].message);
33
+ }
34
+ return data.data;
35
+ }
36
+ async function scan(url, apiKey, onStatus) {
37
+ onStatus?.("Starting scan...");
38
+ const { startAccessibilityReportJob } = await graphql(
39
+ `query($url: String!) { startAccessibilityReportJob(url: $url, use_cache: false) { jobId } }`,
40
+ { url },
41
+ apiKey
42
+ );
43
+ const jobId = startAccessibilityReportJob.jobId;
44
+ onStatus?.(`Job ${jobId.slice(0, 8)}... created`);
45
+ for (let i = 0; i < 60; i++) {
46
+ await new Promise((r) => setTimeout(r, 3e3));
47
+ const { getAccessibilityReportByJobId: job } = await graphql(
48
+ `query($jobId: String!) { getAccessibilityReportByJobId(jobId: $jobId) { status error result { savedReport { key } } } }`,
49
+ { jobId },
50
+ apiKey
51
+ );
52
+ if (job.status === "done") {
53
+ return { key: job.result.savedReport.key };
54
+ }
55
+ if (job.status === "error") {
56
+ throw new Error(job.error || "Scan failed");
57
+ }
58
+ onStatus?.(`Scanning... (${(i + 1) * 3}s)`);
59
+ }
60
+ throw new Error("Scan timed out after 3 minutes");
61
+ }
62
+ async function getReport(r2Key, apiKey) {
63
+ const { fetchReportByR2Key } = await graphql(
64
+ `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 } } } } }`,
65
+ { key: r2Key },
66
+ apiKey
67
+ );
68
+ return fetchReportByR2Key;
69
+ }
70
+ async function login(email, password) {
71
+ const { login: result } = await graphql(
72
+ `mutation($email: String!, $password: String!) { login(email: $email, password: $password) { token } }`,
73
+ { email, password },
74
+ "public"
75
+ // login doesn't need a key
76
+ );
77
+ return result.token;
78
+ }
79
+
80
+ // src/config.ts
81
+ import Conf from "conf";
82
+ var config = new Conf({
83
+ projectName: "webability",
84
+ schema: {
85
+ apiKey: { type: "string", default: "" },
86
+ accessCode: { type: "string", default: "" },
87
+ defaultFormat: { type: "string", default: "table", enum: ["table", "json", "csv"] }
88
+ }
89
+ });
90
+ function getApiKey() {
91
+ return process.env.WEBABILITY_API_KEY || config.get("apiKey") || "";
92
+ }
93
+ function setApiKey(key) {
94
+ config.set("apiKey", key);
95
+ }
96
+ function getAccessCode() {
97
+ return config.get("accessCode") || "";
98
+ }
99
+ function setAccessCode(code) {
100
+ config.set("accessCode", code);
101
+ }
102
+ function isActivated() {
103
+ return !!getAccessCode();
104
+ }
105
+ function clearConfig() {
106
+ config.clear();
107
+ }
108
+
109
+ // src/local-scan.ts
110
+ import { scan as scan2 } from "@webability/core";
111
+ var WCAG_TAG_MAP = {
112
+ "A": ["wcag2a"],
113
+ "AA": ["wcag2a", "wcag2aa", "wcag21aa", "wcag22aa"],
114
+ "AAA": ["wcag2a", "wcag2aa", "wcag2aaa", "wcag21aa", "wcag22aa"]
115
+ };
116
+ async function localScan(url, options) {
117
+ const wcagTags = WCAG_TAG_MAP[options.wcag.toUpperCase()] ?? WCAG_TAG_MAP["AA"];
118
+ const viewport = ["mobile", "tablet", "desktop"].includes(options.viewport ?? "") ? options.viewport : "desktop";
119
+ return scan2(url, {
120
+ wcagTags,
121
+ includeAxe: true,
122
+ dismissModals: true,
123
+ deep: options.deep,
124
+ deepApiUrl: options.deep ? "https://api.webability.io" : void 0,
125
+ deepApiKey: options.deep ? options.apiKey : void 0,
126
+ viewport,
127
+ browser: { headless: true, timeout: 3e4 }
128
+ });
129
+ }
130
+
131
+ // src/init.ts
132
+ import { writeFileSync, existsSync } from "fs";
133
+ import chalk from "chalk";
134
+ var DEFAULT_CONFIG = `project: my-project
135
+ urls:
136
+ - http://localhost:3000
137
+ standard: WCAG2.1AA
138
+ deep: false
139
+ ignore: []
140
+ threshold:
141
+ critical: 0
142
+ serious: 5
143
+ `;
144
+ function initConfig() {
145
+ const filename = ".webability.yml";
146
+ if (existsSync(filename)) {
147
+ console.log(chalk.yellow(` ${filename} already exists`));
148
+ return;
149
+ }
150
+ writeFileSync(filename, DEFAULT_CONFIG);
151
+ console.log(chalk.green(` Created ${filename}`));
152
+ console.log(chalk.dim(" Edit it to configure your project URLs and thresholds"));
153
+ }
154
+
155
+ // src/display.ts
156
+ import chalk2 from "chalk";
157
+ function header() {
158
+ console.log();
159
+ console.log(chalk2.bold.blue(" Abilyo") + chalk2.dim(" by WebAbility"));
160
+ console.log(chalk2.dim(" https://abilyo.com"));
161
+ console.log();
162
+ }
163
+ function scoreCard(score, url) {
164
+ const color = score >= 80 ? chalk2.green : score >= 50 ? chalk2.yellow : chalk2.red;
165
+ const label = score >= 80 ? "COMPLIANT" : score >= 50 ? "NEEDS WORK" : "NON-COMPLIANT";
166
+ const bar = renderBar(score);
167
+ console.log(chalk2.bold(` ${url}`));
168
+ console.log();
169
+ console.log(` Score: ${color.bold(score + "%")} ${chalk2.dim(label)}`);
170
+ console.log(` ${bar}`);
171
+ console.log();
172
+ }
173
+ function renderBar(score) {
174
+ const width = 40;
175
+ const filled = Math.round(score / 100 * width);
176
+ const empty = width - filled;
177
+ const color = score >= 80 ? chalk2.green : score >= 50 ? chalk2.yellow : chalk2.red;
178
+ return color("\u2588".repeat(filled)) + chalk2.dim("\u2591".repeat(empty));
179
+ }
180
+ function issueTable(issues) {
181
+ if (issues.length === 0) {
182
+ console.log(chalk2.green(" No issues found!"));
183
+ return;
184
+ }
185
+ const severityColor = (s) => {
186
+ switch (s.toLowerCase()) {
187
+ case "high":
188
+ case "critical":
189
+ case "serious":
190
+ return chalk2.red;
191
+ case "medium":
192
+ case "moderate":
193
+ return chalk2.yellow;
194
+ case "low":
195
+ case "minor":
196
+ return chalk2.dim;
197
+ default:
198
+ return chalk2.white;
199
+ }
200
+ };
201
+ const high = issues.filter((i) => ["high", "critical", "serious"].includes(i.severity.toLowerCase()));
202
+ const medium = issues.filter((i) => ["medium", "moderate"].includes(i.severity.toLowerCase()));
203
+ const low = issues.filter((i) => ["low", "minor"].includes(i.severity.toLowerCase()));
204
+ console.log(chalk2.bold(" Issues"));
205
+ console.log(` ${chalk2.red.bold(high.length + " High")} ${chalk2.yellow.bold(medium.length + " Medium")} ${chalk2.dim.bold(low.length + " Low")}`);
206
+ console.log();
207
+ for (const issue of issues.slice(0, 30)) {
208
+ const sev = severityColor(issue.severity);
209
+ const tag = sev(`[${issue.severity.toUpperCase().slice(0, 4).padEnd(4)}]`);
210
+ const wcag = chalk2.cyan(issue.wcag.padEnd(8));
211
+ const msg = issue.message.slice(0, 80);
212
+ console.log(` ${tag} ${wcag} ${msg}`);
213
+ }
214
+ if (issues.length > 30) {
215
+ console.log(chalk2.dim(` ... and ${issues.length - 30} more`));
216
+ }
217
+ console.log();
218
+ }
219
+ function summary(stats) {
220
+ console.log(chalk2.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
221
+ console.log(` Elements scanned: ${chalk2.bold(stats.scanned)}`);
222
+ console.log(` Axe violations: ${chalk2.bold(stats.errors)}`);
223
+ console.log(` Advanced checks: ${chalk2.bold(stats.advanced)}`);
224
+ console.log();
225
+ }
226
+ function ctaBox() {
227
+ console.log(chalk2.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
228
+ console.log(chalk2.bold.blue(" Fix these issues automatically"));
229
+ console.log(chalk2.dim(" https://abilyo.com"));
230
+ console.log();
231
+ }
232
+ function errorMsg(msg) {
233
+ console.log(chalk2.red(` Error: ${msg}`));
234
+ console.log();
235
+ }
236
+ function jsonOutput(data) {
237
+ console.log(JSON.stringify(data, null, 2));
238
+ }
239
+ function csvOutput(issues) {
240
+ console.log("severity,wcag,message,element");
241
+ for (const i of issues) {
242
+ const msg = i.message.replace(/"/g, '""');
243
+ const el = (i.element || "").replace(/"/g, '""');
244
+ console.log(`${i.severity},${i.wcag},"${msg}","${el}"`);
245
+ }
246
+ }
247
+
248
+ // src/cli.ts
249
+ var program = new Command();
250
+ program.name("abilyo").description("Abilyo by WebAbility \u2014 WCAG accessibility scanner").version("1.0.0");
251
+ program.command("scan <url>").description("Scan a website for accessibility issues").option("-f, --format <format>", "Output format: table, json, csv, sarif, html", "table").option("--wcag <level>", "WCAG level: A, AA, AAA", "AA").option("--deep", "Run a deeper scan with additional checks (Pro plan)").option("--remote", "Run scan on WebAbility servers instead of locally").option("--upload", "Upload results to dashboard (requires login)").option("--exit", "Exit with code 1 if issues found (CI mode)").option("--no-axe", "Skip axe-core (use only WebAbility detectors)").option("--viewport <size>", "Viewport: mobile, tablet, desktop", "desktop").action(async (url, opts) => {
252
+ if (!isActivated()) {
253
+ header();
254
+ errorMsg("Abilyo is currently invite-only. Run `abilyo activate <code>` with your access code.");
255
+ console.log(chalk3.dim(" Request access at https://abilyo.com/early-access"));
256
+ return process.exit(1);
257
+ }
258
+ if (!url.startsWith("http")) url = `https://${url}`;
259
+ if (opts.deep || opts.upload) {
260
+ const apiKey = getApiKey();
261
+ if (!apiKey) {
262
+ if (opts.format === "table") header();
263
+ errorMsg("--deep and --upload require a Pro plan. Run `abilyo login` first.");
264
+ return process.exit(1);
265
+ }
266
+ try {
267
+ const verify = await fetch("https://api.webability.io/cli/verify", {
268
+ headers: { "Authorization": `Bearer ${apiKey}` }
269
+ });
270
+ if (verify.ok) {
271
+ const data = await verify.json();
272
+ if (!data.active) {
273
+ if (opts.format === "table") header();
274
+ errorMsg("Your account does not have an active Pro plan.");
275
+ console.log(chalk3.dim(" Upgrade at https://abilyo.com/pricing"));
276
+ return process.exit(1);
277
+ }
278
+ }
279
+ } catch {
280
+ }
281
+ }
282
+ if (opts.remote) {
283
+ await remoteScan(url, opts);
284
+ return;
285
+ }
286
+ if (opts.format === "table") header();
287
+ const spinner = ora({ text: "Scanning...", prefixText: " " }).start();
288
+ try {
289
+ const result = await localScan(url, {
290
+ wcag: opts.wcag,
291
+ deep: opts.deep ?? false,
292
+ apiKey: getApiKey() ?? void 0
293
+ });
294
+ spinner.stop();
295
+ if (opts.format === "json") {
296
+ const sanitized = {
297
+ ...result,
298
+ issues: result.issues.map(({ source, ...rest }) => rest)
299
+ };
300
+ console.log(JSON.stringify(sanitized, null, 2));
301
+ } else if (opts.format === "csv") {
302
+ csvOutput(result.issues.map((i) => ({
303
+ severity: i.impact,
304
+ wcag: i.wcag,
305
+ message: i.message,
306
+ element: i.selector
307
+ })));
308
+ } else if (opts.format === "sarif") {
309
+ console.log(JSON.stringify(toSarif(result), null, 2));
310
+ } else if (opts.format === "html") {
311
+ const { toHtmlReport } = await import("@webability/core");
312
+ console.log(toHtmlReport(result));
313
+ } else {
314
+ scoreCard(result.summary.total === 0 ? 100 : Math.max(0, 100 - result.summary.total), url);
315
+ if (result.issues.length > 0) {
316
+ issueTable(result.issues.map((i) => ({
317
+ severity: i.impact,
318
+ wcag: i.wcag,
319
+ message: i.message,
320
+ element: i.selector
321
+ })));
322
+ }
323
+ console.log();
324
+ summary({
325
+ scanned: 0,
326
+ errors: result.summary.critical + result.summary.serious,
327
+ warnings: result.summary.moderate,
328
+ notices: result.summary.minor,
329
+ advanced: 0
330
+ });
331
+ ctaBox();
332
+ }
333
+ if (opts.exit && result.summary.total > 0) {
334
+ process.exit(1);
335
+ }
336
+ } catch (err) {
337
+ spinner.stop();
338
+ errorMsg(err.message);
339
+ process.exit(1);
340
+ }
341
+ });
342
+ async function remoteScan(url, opts) {
343
+ const apiKey = getApiKey();
344
+ if (!apiKey) {
345
+ if (opts.format === "table") header();
346
+ errorMsg("--remote requires login. Run `wa login` first.");
347
+ return process.exit(1);
348
+ }
349
+ if (opts.format === "table") header();
350
+ const spinner = ora({ text: "Scanning (remote)...", prefixText: " " }).start();
351
+ try {
352
+ const result = await scan(url, apiKey, (status) => {
353
+ spinner.text = status;
354
+ });
355
+ spinner.text = "Fetching report...";
356
+ const report = await getReport(result.key, apiKey);
357
+ spinner.stop();
358
+ if (!report) {
359
+ errorMsg("Report not found");
360
+ return process.exit(1);
361
+ }
362
+ const issues = [];
363
+ if (report.axe?.violations) {
364
+ for (const v of report.axe.violations) {
365
+ for (const node of v.nodes || []) {
366
+ issues.push({ severity: v.impact || "moderate", wcag: v.id, message: v.description, element: node.target?.[0] });
367
+ }
368
+ }
369
+ }
370
+ if (report.ByFunctions) {
371
+ for (const fn of report.ByFunctions) {
372
+ for (const issue of fn.issues || []) {
373
+ issues.push({ severity: issue.impact || "moderate", wcag: issue.code || "", message: issue.description || "", element: issue.element });
374
+ }
375
+ }
376
+ }
377
+ if (opts.format === "json") {
378
+ jsonOutput({ url, score: report.score, totalElements: report.totalElements, issues });
379
+ } else if (opts.format === "csv") {
380
+ csvOutput(issues);
381
+ } else {
382
+ scoreCard(report.score || 0, url);
383
+ issueTable(issues);
384
+ summary({
385
+ scanned: report.totalElements || 0,
386
+ errors: report.axe?.violations?.length || 0,
387
+ warnings: 0,
388
+ notices: 0,
389
+ advanced: (report.ByFunctions || []).reduce((sum, f) => sum + (f.count || 0), 0)
390
+ });
391
+ ctaBox();
392
+ }
393
+ if (opts.exit && issues.length > 0) process.exit(1);
394
+ } catch (err) {
395
+ spinner.stop();
396
+ errorMsg(err.message);
397
+ process.exit(1);
398
+ }
399
+ }
400
+ program.command("init").description("Create .webability.yml config file").action(() => {
401
+ header();
402
+ initConfig();
403
+ console.log();
404
+ });
405
+ program.command("login").description("Authenticate with your WebAbility account").option("-k, --key <key>", "API key (or set WEBABILITY_API_KEY env var)").action(async (opts) => {
406
+ header();
407
+ if (opts.key) {
408
+ setApiKey(opts.key);
409
+ console.log(chalk3.green(" API key saved."));
410
+ console.log();
411
+ return;
412
+ }
413
+ const readline = await import("readline");
414
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
415
+ const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
416
+ console.log(chalk3.bold(" Login to WebAbility"));
417
+ console.log();
418
+ const email = await ask(" Email: ");
419
+ const password = await ask(" Password: ");
420
+ rl.close();
421
+ const spinner = ora({ text: "Authenticating...", prefixText: " " }).start();
422
+ try {
423
+ const token = await login(email, password);
424
+ setApiKey(token);
425
+ spinner.succeed("Logged in! API key saved.");
426
+ console.log();
427
+ } catch (err) {
428
+ spinner.fail(err.message);
429
+ process.exit(1);
430
+ }
431
+ });
432
+ program.command("whoami").description("Show current authentication status").action(() => {
433
+ const key = getApiKey();
434
+ header();
435
+ if (key) {
436
+ console.log(chalk3.green(" Authenticated"));
437
+ console.log(chalk3.dim(` Key: ${key.slice(0, 20)}...`));
438
+ } else {
439
+ console.log(chalk3.yellow(" Not authenticated"));
440
+ console.log(chalk3.dim(" Run `wa login` to authenticate"));
441
+ }
442
+ console.log();
443
+ });
444
+ program.command("logout").description("Remove saved API key").action(() => {
445
+ clearConfig();
446
+ header();
447
+ console.log(chalk3.green(" Logged out. API key removed."));
448
+ console.log();
449
+ });
450
+ program.command("activate <code>").description("Activate Abilyo with your early access code").action(async (code) => {
451
+ header();
452
+ const spinner = ora({ text: "Validating access code...", prefixText: " " }).start();
453
+ try {
454
+ const res = await fetch("https://api.webability.io/cli/activate", {
455
+ method: "POST",
456
+ headers: { "Content-Type": "application/json" },
457
+ body: JSON.stringify({ code: code.trim() })
458
+ });
459
+ if (res.ok) {
460
+ setAccessCode(code.trim());
461
+ spinner.succeed("Abilyo activated!");
462
+ console.log();
463
+ console.log(chalk3.dim(" Run `abilyo scan <url>` to get started"));
464
+ console.log();
465
+ return;
466
+ }
467
+ } catch {
468
+ }
469
+ if (code.trim().length >= 8) {
470
+ setAccessCode(code.trim());
471
+ spinner.succeed("Abilyo activated!");
472
+ console.log();
473
+ console.log(chalk3.dim(" Run `abilyo scan <url>` to get started"));
474
+ console.log();
475
+ } else {
476
+ spinner.fail("Invalid access code");
477
+ console.log(chalk3.dim(" Request access at https://abilyo.com/early-access"));
478
+ process.exit(1);
479
+ }
480
+ });
481
+ program.argument("[url]", "URL to scan").action((url) => {
482
+ if (url) {
483
+ program.parse(["node", "abilyo", "scan", url, ...process.argv.slice(3)]);
484
+ } else {
485
+ header();
486
+ if (!isActivated()) {
487
+ console.log(chalk3.yellow(" Abilyo is invite-only during early access."));
488
+ console.log();
489
+ console.log(` ${chalk3.cyan("abilyo activate")} <code> Activate with your access code`);
490
+ console.log();
491
+ console.log(chalk3.dim(" Request access at https://abilyo.com/early-access"));
492
+ } else {
493
+ console.log(chalk3.bold(" Commands:"));
494
+ console.log();
495
+ console.log(` ${chalk3.cyan("abilyo scan")} <url> Scan for accessibility issues`);
496
+ console.log(` ${chalk3.cyan("abilyo scan --deep")} <url> Deeper scan with additional checks (Pro)`);
497
+ console.log(` ${chalk3.cyan("abilyo scan --exit")} <url> CI mode \u2014 exit 1 if issues found`);
498
+ console.log(` ${chalk3.cyan("abilyo init")} Create .webability.yml config`);
499
+ console.log(` ${chalk3.cyan("abilyo login")} Authenticate with your account`);
500
+ console.log(` ${chalk3.cyan("abilyo whoami")} Show auth status`);
501
+ console.log();
502
+ console.log(chalk3.dim(" Example: abilyo scan localhost:3000"));
503
+ console.log(chalk3.dim(" Example: abilyo scan example.com --format json"));
504
+ console.log(chalk3.dim(" Example: abilyo scan example.com --deep --exit"));
505
+ }
506
+ console.log();
507
+ }
508
+ });
509
+ program.parse();
510
+ function toSarif(result) {
511
+ return {
512
+ $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
513
+ version: "2.1.0",
514
+ runs: [{
515
+ tool: {
516
+ driver: {
517
+ name: "WebAbility",
518
+ version: "1.0.0",
519
+ informationUri: "https://webability.io",
520
+ rules: result.issues.map((i) => ({
521
+ id: i.type,
522
+ shortDescription: { text: i.message }
523
+ }))
524
+ }
525
+ },
526
+ results: result.issues.map((i) => ({
527
+ ruleId: i.type,
528
+ level: i.impact === "critical" || i.impact === "serious" ? "error" : "warning",
529
+ message: { text: i.message },
530
+ locations: [{
531
+ physicalLocation: {
532
+ artifactLocation: { uri: result.url },
533
+ region: { snippet: { text: i.html || i.selector } }
534
+ }
535
+ }]
536
+ }))
537
+ }]
538
+ };
539
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@webability/cli",
3
+ "version": "1.0.0",
4
+ "description": "Abilyo by WebAbility — WCAG accessibility scanner for your terminal",
5
+ "type": "module",
6
+ "bin": {
7
+ "abilyo": "./dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsup src/cli.ts --format esm --dts --clean",
11
+ "dev": "tsx src/cli.ts",
12
+ "typecheck": "tsc --noEmit",
13
+ "clean": "rm -rf dist"
14
+ },
15
+ "keywords": [
16
+ "accessibility",
17
+ "wcag",
18
+ "ada",
19
+ "a11y",
20
+ "audit",
21
+ "scanner",
22
+ "cli"
23
+ ],
24
+ "author": "WebAbility <support@webability.io>",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/snayyar00/webability-packages",
29
+ "directory": "cli"
30
+ },
31
+ "dependencies": {
32
+ "@webability/core": "workspace:*",
33
+ "chalk": "^5.3.0",
34
+ "commander": "^12.1.0",
35
+ "conf": "^13.0.1",
36
+ "ora": "^8.1.0",
37
+ "playwright": "^1.49.0",
38
+ "yaml": "^2.6.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^25.6.0",
42
+ "tsup": "^8.3.0",
43
+ "tsx": "^4.19.0",
44
+ "typescript": "^5.6.0"
45
+ },
46
+ "engines": {
47
+ "node": ">=18"
48
+ }
49
+ }